- AWSのデータ漏えいリスクでは、S3バケットへの不正アクセスが繰り返し登場し、古いAPI設計と例外的な挙動のため、単純に「公開/非公開」と判断しにくい
- 多くのS3操作は一般的なAWSエンドポイントではなく、バケットURLそのものに対して呼び出され、誤ったバケットポリシーでは認証なしの
curlリクエストでも危険な操作が可能になる
s3:ListBucketを防いだだけで安全とは言い切れず、ListBucketVersions、ListMultipartUploads、fetch-ownerのような経路からオブジェクトキーとアカウント識別子が露出しうる
- アップローダーはストレージクラス、タグ、Object Lock、一部のリダイレクト関連ヘッダーなどオブジェクト属性に影響できるため、IAM条件やライフサイクルポリシーのような追加制御が必要になる
- ACLとパブリックアクセスブロック設定だけを見て非公開と判断すると見落とす経路があり、CloudFrontディストリビューションやCognito identity pool経由でインターネット利用者がS3オブジェクトにアクセスできる
古いS3 API設計と匿名呼び出し
- S3はAWS初期のサービスの1つであり、堅牢で十分にテストされているが、標準化された設計パターン以前の名残のため、他のAWSサービスとは異なるAPIの形を持つ
- 一部のS3 APIは
s3.us-east-2.amazonaws.comのような一般エンドポイントを使うが、多くの操作は対象のバケットURLに直接リクエストする必要がある
- バケット一覧取得の例は
Host: [bucketname].s3.amazonaws.com形式のGET /
- バケットタグ取得もそのバケットホストに
GET /?taggingを送る
- EC2やDynamoDBのような多くのAWSサービスは一般エンドポイントを使い、対象リソースをHTTPヘッダーやパラメータで渡す方式が一般的
- S3バケットはパブリックアクセスと認証付きアクセスの両方をサポートするため、どのAPI操作が認証なしで可能かは常に明確とは限らない
- 例のバケットポリシーが
Principal: "*"とAction: "s3:*"をバケットリソースに許可していると、認証なしリクエストでもバケット削除が可能になる
- 一部の操作は匿名リクエストをサポートせず、
s3:GetBucketOwnershipControls does not support Anonymous requests!のようなエラーが返る
- 匿名APIリクエストはCloudTrailにanonymousアカウントとして記録される
- 認証されていないリクエストだと、誰がバケットを削除したのか、暗号化設定やロギング状態を確認したのかを特定できない
/?logging、/?tagging、/?encryptionのようなパスはブラウザでもテストできる
GetObjectTorrentのように、ドキュメントには残っているがもはや実行できない操作もある
ListBucketを防ぐだけではオブジェクトキー露出を防ぎにくい
- S3オブジェクトをダウンロードするには各オブジェクトのキーが必要で、キーはファイルパスのように使われる
- ルートバケットへの
GETリクエストは条件次第でバケット内容を返すため、s3:ListBucket拒否がよくある防御策のように見える
public-read ACLとs3:ListBucket拒否ポリシーを併用しても、オブジェクトキーを取得する経路は残る
GET /?versions、つまりs3:ListBucketVersionsは、バケット内オブジェクトバージョンのメタデータを返す
GET /?uploads、つまりs3:ListMultipartUploadsは、進行中のmultipart upload一覧を返す
HeadBucketのドキュメントにはバケットの存在有無とアクセス権確認に関する記述があるが、実際にはListBucket操作の実行権限があるかを確認している
ListBucket拒否だけを検証すると、S3オブジェクトキー露出の可能性を見落としうる
未完了のmultipart uploadのコストと露出
- multipart uploadは
create-multipart-uploadで開始し、upload-partでパートをアップロードする
- 未完了のmultipart uploadはWebコンソールでは確認しにくく、
/?uploadsまたはaws s3api list-multipart-uploads --bucket [bucket-name]で確認できる
- 完了リクエストが正常に送信されなかった場合、Amazon S3はパートを組み立てず、オブジェクトも作成しない
- アップロード済みのパートはmultipart uploadが完了または中断されるまでアカウントに残る
- 保存されたパートにはS3ストレージ料金が発生する
- 完了前のオブジェクトのパートをダウンロードする方法は見つからなかったが、削除は可能
- AWSは、一定日数後に未完了アップロードを削除するライフサイクルルールの適用を推奨している
/?uploadsで未完了のmultipart uploadを列挙すると、アップロードを開始したprincipalのARNが返される
- アカウントIDやARNのような識別子を機微ではないとみなす立場なら問題ではないかもしれない
- 攻撃者に有用な識別子の公開を避けたいなら、露出と見なせる
ACLとメールアドレスベースのアカウント確認
- S3 ACLのドキュメントには、AWSアカウントをルートユーザーのメールアドレスで識別していた時代の名残がある
PutBucketACL操作ではgranteeをメールアドレスで指定できる
Type: AmazonCustomerByEmailとEmailAddressを使う
- 指定したメールアドレスに紐づくAWSアカウントがなければ
UnresolvableGrantByEmailAddressエラーが発生する
- エラーメッセージは「提供されたメールアドレスは記録上どのアカウントにも一致しない」という形になる
- この挙動により、特定のメールアドレスが登録済みのAWSアカウントを持っているか確認できる
アップローダーが選べるストレージクラスとオブジェクトメタデータ
- S3のストレージクラスはバケットではなくオブジェクトに適用される
- バケット単位で希望するストレージクラスを固定する設定はなく、アップロード主体がオブジェクトのストレージクラスを指定できる
- 例は
aws s3 cp "my.txt" "s3://mybucket/myobject.txt" --storage-class [CLASS]
- アップローダーは、あらかじめ定義された一覧の中で、バケット所有者が負担するGBあたりの保存・アクセスコストに影響を与えられる
- IAMポリシーで
s3:x-amz-storage-class条件キーを使えば、許可するストレージクラスを制限できる
- 例のポリシーは
s3:PutObjectに対してSTANDARDのみ許可する
- ライフサイクルポリシーを設定すれば、一定期間後にすべてのオブジェクトを特定のストレージクラスへ移行できる
- pre-signed URLを使うアップロードでは、AWS Signature Version 4が
X-Amz-で始まるすべてのヘッダーの署名を要求する
- ストレージクラスは
x-amz-storage-classヘッダーで指定される
- アプリケーションがよほどひどく実装されていない限り、即座に改ざんする明確な方法はない
タグ、Object Lock、リダイレクトもアップローダーの影響下にある
- S3オブジェクトに関する多くの属性はアップローダーが制御する
- オブジェクトタグはアップロード時に指定できる
- 例は
--tagging "AllYourTags=AreBelong&To=Us"
- タグ値を基準に自動化を行うシステムは、アップローダーが作ったタグ値の影響を受けうる
- Object Lockは、バケットでobject lockingが有効な場合、オブジェクト保持やlegal holdを設定できる
- 例のコマンドは
--object-lock-retain-until-date "2099-01-01T00:00:00+0000"、--object-lock-legal-hold-status "ON"、--object-lock-mode "COMPLIANCE"を使う
- 静的Webサイトホスティングが有効なバケットでは、アップロードされたファイル設定を使ったオープンリダイレクトが可能
PutObjectがサポートする全ヘッダー一覧にも注意が必要
- pre-signed URLには制限がある
- Cognito identityとIAMポリシーに依存する構成では、認証済みCognitoコンテキストでリクエストに署名できる
バケット所有者とアカウント識別子の露出
- 特定のアカウントIDがアクセス可能なバケットの所有者か確認するには、
ListBucketリクエストにx-amz-expected-bucket-ownerヘッダーを入れられる
- 間違ったアカウントIDを入れると
AccessDeniedが返る
- 正しいアカウントIDを入れ、呼び出し元に
ListBucket権限があれば正常応答が返る
ListBucket APIのfetch-owner=trueパラメータを使うと、応答の各キーにOwner要素が含まれる
Owner内のIDは、AWSドキュメントでcanonical user IDと呼ばれる64文字の16進文字列
- canonical user IDをIAMポリシーの
PrincipalにCanonicalUserとして入れて保存し、再読み込みするとAWSアカウントIDとして解釈される
ListBucketVersionsとListMultipartUploadsもfetch-ownerなしで同様に動作する
S3オブジェクトキーはファイル名のようでいて違う動作をする
- S3のオブジェクトキーは大文字小文字を区別する
- 同じ名前に見えても大文字小文字が違えば複数のオブジェクトをアップロードできる
- アプリケーションがS3オブジェクトキーを大文字小文字を区別しないファイル名のように扱うと問題が起きうる
- 例のアプリケーションはユーザーのパスワードをS3ファイルに保存し、ファイル名としてユーザー名を使う
- サインアップ時にはファイルの存在有無だけを確認し、パスワード変更時にはユーザー名を小文字化してファイルに書き込む
jeffがすでに存在していてもJEFFとして登録でき、JEFFユーザーがパスワード変更でjeffのファイルを上書きできる
- S3オブジェクトキーにはあらゆるUTF-8文字を使える
- 特定の文字は一部のアプリケーションやプロトコルで問題を引き起こすことがある
- 空白、スラッシュ、パーセント文字などもオブジェクトキーとして有効
「非公開バケット」に見えてもアクセス経路が残ることがある
- ACLが無効で、リソースポリシーが狭く設定され、block public accessが有効でも、バケットが公開アクセス可能なことがある
- 最も一般的な経路はAmazon CloudFrontディストリビューション
- S3バケットの前段にCDNを置く場合、通常はインターネット向け配信を意図している
- セキュリティツールは、バケットのリソースポリシーがCloudFrontに限定されていると公開ではないと判断しうる
- 例では
get-bucket-policy-statusはIsPublic: falseを返す
- バケットへ直接リクエストすると
AccessDeniedが返る
- CloudFrontディストリビューションのドメインへ同じリクエストを送るとオブジェクト内容が返る
- Cognito identity poolも、制限されたリソースポリシーを持つバケットを露出させうる
- Cognitoはログイン成功後、事前設定されたロールの一時的なAWS認証情報を提供する
- そのロールに
s3:ListBucketとs3:GetObject権限があれば、利用者はS3 APIを呼び出せる
- 公開アクセスに該当しうるCognito設定は2つある
- Self-registration: インターネット利用者がアプリに登録してログインできるなら、事実上公開アクセスになる
- Guest access: 認証していない利用者に一意の識別子とAWS認証情報を提供する
- ゲストアクセスの例は、
get-idでIdentityIdを取得し、get-credentials-for-identityで一時認証情報を受け取り、そのプロファイルでaws s3 lsを実行する流れ
- CloudFrontとCognito identity poolは実際にインターネットでよく使われているが、セキュリティツールではめったに表示されない公開アクセス経路である
1件のコメント
Hacker News の意見
興味深い点は多いが、ファイルシステムが大文字小文字を区別することを不満点と見るのには同意しにくい
本来そうあるべきだと思うし、macOS がそうしていない点のほうがむしろ苛立たしい
ファイル名の大文字小文字の区別は、非技術系ユーザーにとっても意外に感じられることがある。誰かが「Book Draft 1.docx」を送ったと言ったのに、メールボックスには「Book draft 1.docx」があったとして、普通は「別のファイルを送ったようですね?」とは言わない
文章でも大文字小文字はたいてい意味を変えない。「Hi, how are you?」と「hi, how are you?」は同じ意味で、大文字が意味を変えるのは固有名詞と普通名詞を区別する場合くらいだが、ファイル名ではめったに気にされない
個人の好みは別として、開発者やシステム管理者がファイルシステムの大文字小文字の区別に驚いたり苛立ったりするのは理解しにくい。必要なら、検索結果のようにエンドユーザー向けには開発者が抽象化してあげればよい
コーディングのようにコードスタイルを強制して可読性に役立つわけでもない。プログラミングでも、IDE が変数名のタイプミスを見つけられるほど賢くなるまでは、バグの原因になりがちだった。Pascal の良かった点の一つは、C と違って大文字小文字を気にしなくてよいことだった
ファイル名を望むスタイルで書けて、その表記は保持されるが、検索したり処理したりするときには、そのスタイルを正確に覚えていなくてもよい。検索は大文字小文字を区別しないからだ
大文字小文字の区別はまだ分かりやすいほうで、もっと直感的でないのはS3 のパスが見せかけだという点だ
S3 は「/builds/1/installer.exe」のアップロードを受け付け、/builds 配下の一覧も表示するが、実際には名前に「/」を含む「/builds/1/installer.exe」というキーを 1 つアップロードしているだけだ
そのため「/builds/1//installer.exe」や「/builds//1/installer.exe」もアップロード可能で、完全に別のファイルになる。単なるキー名であって、実際のディレクトリは存在しない
[1] https://docs.aws.amazon.com/AmazonS3/latest/userguide/direct...
今年、うちの本番システムも奇妙なバグに遭遇し、5人がかりでようやく見つけた。原因は名前が文字通り「/」のオブジェクトで、ソフトウェアがそれをファイルではなくパスのように扱おうとしていたことだった
S3 や他の AWS サービスを安心して使うのは難しい。直感的なものがなく、動く要素が多すぎ、読むべきドキュメントも多すぎる
それでも原文のように、うっかりすべてを世界中に公開してしまうことがあり得る。むしろ Hetzner Storage Boxes や DigitalOcean Spaces のような、本当に単純なサービスを使いたい
最近、数 MB より大きい動画ファイルをパイプでアップロードすると、返された Location から https:// が抜けることに気づいた。だからファイルをアップロードするたびに Location が https で始まっているか確認し、なければ付け足さなければならない
当然ながら S3 Node クライアントの GitHub issue では「DigitalOcean のバグのようですね」と言われ、DigitalOcean フォーラムでは「S3 Node クライアントのバグのようですね」と言われる
もともとは一部の特殊な状況を助けるために設計された多数の機能や癖が、今では一般的なプロトコルの一部になっている。ビジネス上、誰も背を向けないようにしようとした結果、そうなったように思える
数十億個のオブジェクトを削除する場合も注意が必要だ。削除 API を直接呼ぶと高くつくことがある。
代わりに、ワイルドカードやバケット全体に対して有効期限を now に設定するライフサイクルルールを無料で設定できる。そうすればストレージ課金は即座に止まり、削除は AWS が自動で処理してくれる。
S3 API サーバーが秒間リクエスト数で叩かれる事態も避けられる。
失敗したマルチパートアップロードが見えない形で残り、明示的にライフサイクル設定をしないとストレージ費用まで発生するという点は本当にひどい。
「Simple」の S は単純さを意味するものだと思っていた。
私の提案は、未完了アップロードのパートは最後のアクティビティから 24 時間だけ残し、その間のストレージ料金も請求しない、というものだった。ahenry@ はそれを却下した。
かなり古いサーバーで、ほぼ 10 年間、毎晩マルチパートアップロードを開始する cron スクリプトが動いていた。バックアップをバケットに押し込むためのものだったが、そのバケットはユーザーのアップロードコンテンツも保存していたので、毎日少しずつ大きくなるのは正常に見えた。
スクリプトは「動いていない」状態だったためバックアップデータには依存しておらず、S3 上にもファイルは見えず、バケットサイズは着実に、しかし過度ではない程度に増えていた。ところがこの春に確認したところ、ほぼ 3TB の未完了マルチパートアップロードを保存していた。
もちろん、この逸話が悪い慣行だらけなのは分かっている。
大文字・小文字を区別する/しないという議論は、概してあまりに英語中心的だと感じる。
言い換えると、特に IT では、言語に関する議論が過度に英語中心的であることがあまりにも多い。
英語を母語としない立場から言うと、プログラミングにはすでに概念や要素が多すぎる。そこに 101 種類もの別の言語まで考慮して複雑さを増やさないほうがいい。
Unicode とタイムゾーンは、プログラミングでより多くの言語や文化を考慮しようとする代表的な要素だが、結果として英語圏以外のプログラマーも含め、全員にとって最大級の苦痛を生んでいる。
自分の母語でプログラムを書きたいとは思わない。その代償として、プログラミング時に主要な言語すべてを考慮しなければならないならなおさらだ。IT の議論が英語中心的なのは問題ない。多様性は複雑性であり、英語は誰かが所有する言語ではなく、人々がコミュニケーションに使う道具にすぎない。
その共通語のおかげで、インド、中国、日本、南米など多くの人に自分の考えを伝えられる。彼らが英語で話すと決めた瞬間、彼らも英語を所有する。IT に多様性の政治を持ち込む必要はなく、技術的なままにしておくほうがいい。
ある意味では、この 2 種類の音節文字は大文字/小文字のアルファベットのように感じられる。
ほかにもいくつかある
マルチパートアップロードは、インスタンス認証情報を持つ複数のマシンからは実行できない。主体が異なるため、互いのマルチパートアップロードにアクセスできないからだ。複数のマシンで1つのマルチパートアップロードを組み立てるには、実際の IAM ユーザーが必要になる
LIST リクエストは遅いだけでなく、大量に行うと非常に高くつく。「bucket inventory」のような回避策はあるが、便利でも安価でもない
バケット作成は内部的に DNS を使うため、書き込み後読み取りの一貫性がない。そのため、バケットを作った直後にアクセスできなかったり、変更の伝播を十分に待つ前には作成したばかりのバケットを削除できなかったりすることがある。https://github.com/julik/talks/blob/master/euruko-2019-no-su... を参照
「foo」というオブジェクトと「foo/bar」というオブジェクトを同時に作成できる。すると、ファイルがディレクトリを上書きする構造になり、バケットのデータをファイルシステム構造に移植できなくなる
S3 は大文字小文字を区別するため、ファイルシステム構造に移植できないオブジェクトを作成できる。Rails のファイルストレージは大文字小文字を区別するストレージを前提としていたため macOS で大きく壊れ、識別子を常に小文字にするよう修正された
ほとんどの S3 設定では GET は許可するが HEAD は許可しない。オブジェクトの存在確認を防ぐための方法のようだが、確かではない。いずれにせよ、HEAD リクエストでオブジェクトサイズを確認するキャッシュフレンドリーなフローは、特に pre-signed URL では動作しない。代わりに Range をごく小さく指定した GET、たとえば最初の1バイトだけを取得するような方法で回避する必要がある
pre-signed URL を大量に生成するなら、生成速度を10〜40倍高められる可能性がある: https://github.com/WeTransfer/wt_s3_signer
未完了のマルチパートアップロードのストレージ料金も依然として支払うことになる。ユーザーがこのようなアップロードを開始できる構造なら特に注意が必要だ。一定時間が経過した未完了のマルチパートアップロードを自動削除する設定があるので、苦労したくなければ有効にすべきだ
逆説的だが、S3 は革命的だったし、今でもさまざまな層で優れた製品だ。ただ、機能が多い分、落とし穴も多い
Elixir で Stream.transform(https://hexdocs.pm/elixir/Stream.html#transform/3) を使い、列を修正して注入するストリーミング CSV 後処理パイプラインを作った。Elixir の AWS と CSV モジュールは入力のストリーミングデータは処理するが、出力ストリームの総量が 5MiB より小さいと AWS モジュールがマルチパートアップロードを使うため、S3 でエラーになって悲しかった
同僚と数日かけて分析し、診断した別の面白い問題がある。S3 は単一の TCP 接続が HTTP リクエスト100個を送ると、それ以降のリクエストを静かに捨てる
https://github.com/aws/aws-sdk-go/issues/2825
パフォーマンスのために keep-alive は欲しいが、クライアントが長く接続しすぎてロードバランサにホットスポットを作るのは防ぎたい、という場合によくあるパターンだ
S3 が標準ストレージクラスではレイテンシが高く、Web 配信に適していないという点もある
多くの人は、画像やフォントのような Web サイトリソースを S3 から直接ホストすればよいと考えるが、ユーザー体験が悪くなる可能性がある
“applications can achieve consistent small object latencies (and first-byte-out latencies for larger objects) of roughly 100–200 milliseconds.”
出典: https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimi...
CloudFront signed cookies を使えば、特定のユーザーに対して S3 上の本人所有コンテンツだけに CDN アクセスを与えることもできる。かなりよくできている
頻繁にアクセスされるアセットをキャッシュしてレイテンシを下げ、コストもかなり削減できる
アップローダーがルールを決めるというのはかなり荒っぽい。設定の甘い Web サイトなら、動機さえ十分なユーザーがユーザーコンテンツを Amazon Glacier にアップロードさせ、後でそこから配信させることもできるということだろうか?
https://docs.aws.amazon.com/service-authorization/latest/ref...
特に条件キーはここにあり、ストレージクラスやタグ付けなどへのアクセスを制御するキーを確認できる
https://docs.aws.amazon.com/service-authorization/latest/ref...