IAM ポリシー未設定の IAM ユーザーが生成した S3 署名付き URL (presigned URL) を使ってオブジェクトを Get できるのはなぜか

S3 アクセス用の署名付き URL について調べ直したらいろいろと面白かったです。

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

コンバンハ、千葉(幸)です。

突然ですが以下のようなケースを考えてください。

s3presigned-url

  • IAM ポリシーがアタッチされていない IAM ユーザーが存在する
  • 当該 IAM ユーザーが特定の S3 バケット内のオブジェクトに対する署名付き URL を生成する
  • 生成された URL を用いて任意の利用者がオブジェクトを Get する

上記を実現したいと考えたときに必要となる設定が何か、皆さんは思いつくでしょうか。なお、 IAM ユーザーと S3 バケットは同一の AWS アカウントに存在するものとします。

そもそも IAM ポリシーが無いのに署名付き URL を生成できるの?」という部分から引っかかる方もいるかと思いますが、実はできます。このエントリを読み終わった頃には、そこも含めて腹落ちできているかと思います。

先にまとめ

  • 署名付き URL の生成は API コールを伴わない
    • なんならオフライン(インターネットにつながらない)状態でも生成できる
    • 署名付き URL の生成に権限は必要ない
  • 署名付き URL によってまかなわれるのは認証である
  • リクエストがサービスエンドポイントに到達した後にその時点の権限構成をもって認可が行われる
    • URL 生成時の権限構成は関係ない

S3 の署名付き URL (presigned URL)とは

署名付き URL を利用することで、IAM の認証情報を持たないユーザーに S3 バケット内のオブジェクトへのアクセス権を与えることができます。

デフォルトでは S3 バケットはプライベート(非公開)な状態であり、バケット内のオブジェクトにアクセスさせたければ IAM エンティティや S3 バケットポリシーで必要なパーミッションを付与する必要があります。

例えば、「セミナーの参加者に対して一定期間のみ S3 バケット上の資料をダウンロードさせたい」「大きめのサイズのファイルをやり取りするために特定の S3 バケットに一時的にアップロードさせたい」といったケースで、都度それぞれの利用者に IAM ユーザーやロールを準備して引き渡すのは大変です。

そんな時に署名付き URL を使用すれば、バケットを非公開設定のまま多数の利用者にアクセス権を付与することができます。実態としては署名付き URL を発行した IAM エンティティによるアクション(オペレーション)として扱われます。

署名付き URL が対応しているのは以下のアクションです。

  • GetObject(ダウンロード、表示)
  • PutObject(アップロード)

基本的には AWS SDK を用いて署名付き URL の払い出しを行います。 AWS CLI では、ダウンロード用のみ、署名付き URL の発行に対応してます。

S3 における署名付き URL についての詳細は以下を参照してください。

権限を持つ IAM ユーザーで署名付き URL を試してみる

まずは署名付き URL を使用した場合の実際の動きを見て、イメージを膨らませてみましょう。

今回の構成は以下の通りです。

  • IAM ユーザー:chiba-cli
    • AdministratorAceessをアタッチ
  • S3 バケット:cm-chiba-hogehoge
    • バケットポリシーなし
  • オブジェクト:test.txt
    • 中身はhello

s3presigned-url_AdministratorAccess

AWS CLI の presign コマンドを使用してダウンロード用の署名付き URL を発行し、その URL を使用してオブジェクトを Get します。

まずは URL を生成します。オプションで有効期限を指定することもできますが、ここでは特に指定しません。

(なお、表示の都合上「?」が全角のものになっていますが、実際には半角です。以降の「?」も全て同様です。)

% aws s3 presign s3://cm-chiba-hogehoge/test.txt
https://cm-chiba-hogehoge.s3.ap-northeast-1.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ3BIIH733IWA5GNW%2F20210502%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20210502T105040Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=b2fa84d40fcedfc8cd2bxxxx8423c66f293b4ad3dc2bc57699f1e5xxxxxxxxxx

生成された URL は以下の構造になっています。(%2F/を表すため、変換したものを載せています。)

https://cm-chiba-hogehoge.s3.ap-northeast-1.amazonaws.com/test.txt\
?X-Amz-Algorithm=AWS4-HMAC-SHA256\
  &X-Amz-Credential=AKIAQ3BIIH733IWA5GNW/20210502/ap-northeast-1/s3/aws4_request\
  &X-Amz-Date=20210502T105040Z\
  &X-Amz-Expires=3600\
  &X-Amz-SignedHeaders=host\
  &X-Amz-Signature=b2fa84d40fcedfc8cd2bxxx28423c66f293b4ad3dc2bc57699f1e5xxxxxxxxxx

この URL を用いてブラウザからアクセスすると、問題なくオブジェクトを参照( Get )できました。

presigned-url

ポリシーをデタッチしたりアタッチしたりする

署名付き URL を発行したユーザーである chiba-cli から AdministratorAccess をデタッチし、先ほど発行済みの URL を用いて再度アクセスします。

同じ URL を用いましたが、アクセスが拒否されました。

presigned-url-9952872

再び chiba-cli ユーザーに AdministratorAccess をアタッチして、同じ URL を用いてアクセスします。(ブラウザで再読み込みをした。)

オブジェクトの中身を参照できるようになりました。

presigned-url

この挙動から、「署名付き URL を発行した時点の IAM エンティティの権限構成」は関係がなく、リクエストを実行した時点の権限がどうであるかが評価されていることが分かります。

S3 アクセスログではこう見える

上記の操作を実施した際の S3 のアクセスログは以下の通りです。(実際は一行で出力されますが、改行を入れています。)

xxxxx5d93a893e1296xxxxx5ec161876764435f7f067bcf180a50e6fc30c3b22 #バケット所有者
cm-chiba-hogehoge #バケット名
[02/May/2021:11:06:48 +0000] #アクセス時刻
202.xx.xx.xx #リモートIP
arn:aws:iam::012345678910:user/chiba-cli #リクエスタ
VJ4TVW22EX8JJ0QH #リクエストID
REST.GET.OBJECT #オペレーション
test.txt #キー
"GET /test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ3BIIH733IWA5GNW%2F20210502%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20210502T105204Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX HTTP/1.1" #リクエスト URI
403 #HTTPステータス
AccessDenied #エラーコード
243 #Bytes Sent
- #オブジェクトサイズ
10 #トータルタイム
- #ターンアラウンドタイム
"-" #リファラ
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36" #User-Agent
- #バージョン ID
xxxxxxPbbOrl9AlRyYjh+bDRjm/QH9mXfxCaTGeHLTwglBit12CMjeMHlxnffLpwa/ylq2QPZ8E= #ホストID
SigV4 #署名バージョン
ECDHE-RSA-AES128-GCM-SHA256 #暗号スイート
QueryString #認証タイプ
cm-chiba-hogehoge.s3.ap-northeast-1.amazonaws.com #ホストヘッダ
TLSv1.2 #tlsバージョン

ハイライト部から、以下が読み取れます。

  • リクエスタは署名付き URL を生成した IAM ユーザーである
  • リクエスト URI が署名付き URL (の一部)である
  • 認証タイプがクエリ文字列である

認証タイプについては別のタイプが後で出てきますので、ぼんやり覚えておいてください。

権限を持たない IAM ユーザーで署名付き URL を試してみる

冒頭で取り上げたケースと同じ条件にしてチャレンジします。

  • IAM ユーザー:chiba-cli
    • IAM ポリシーなし
  • S3 バケット:cm-chiba-hogehoge
    • バケットポリシーあり
  • オブジェクト:test.txt
    • 中身はhello

s3presigned-url_NoPolicy

結論から言うと、冒頭で述べた「必要となる設定」はここでのバケットポリシーのことを指します。

今回は以下を設定しました。

バケットポリシー

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::012345678910:user/chiba-cli"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cm-chiba-hogehoge/*"
        }
    ]
}

「署名付き URL によるアクセスかどうか」という部分を取っ払って 単に「同一アカウントにおける chiba-cli ユーザーと cm-chiba-hogehoge バケット」の関係性で考えれば、リソースベースポリシーで Allow が定義されているために問題なく GetObject が実現できる状態です。

この状態で再び presign コマンドで署名付き URL を生成します。

% aws s3 presign s3://cm-chiba-hogehoge/test.txt
https://cm-chiba-hogehoge.s3.ap-northeast-1.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAQ3BIIH733IWA5GNW%2F20210502%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Date=20210502T105204Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Signature=9342f0b50a9cd07b48b67223ddfcf55bc170af71366d24834aa004f346ff062e

IAM ユーザーには何の IAM ポリシーもアタッチされていませんが、問題なく上記のコマンドを実行できました。(ついでに手元の端末のネットワークを切断してから打鍵しても、問題なく署名付き URL が生成できました。)

ともあれここで生成した署名付き URL を用いてアクセスを試みます。問題なく Get できます

presigned-url-9953972

バケットポリシーを除外してからアクセスし直せば、この通り拒否されます。( IAM ポリシーでもバケットポリシーでも Allow が無いから。)

pre-signed-url

もちろんバケットポリシーを元に戻せば、再度正常にアクセスが成功します。

ここまでの流れで、ひとまず署名付き URL の以下の挙動のイメージがついたかと思います。

  • アクセスは「署名付き URL を生成した IAM エンティティ」によるものとして扱われる
  • 署名付き URL を生成した時点でなく、リクエストを実行した時点での「署名付き URL を生成した IAM エンティティ」の権限で評価される   

署名付き URL とは何か

動作イメージがついたところで、もう少し深掘りしていきましょう。「署名付き URL とは何か」をひとことで表すと、「予め署名された URL」です。そのまんま過ぎて捻りがなかったですね。

もう少し違う言い方をすると、「通常は署名付き URL の作成とほぼ同時にリクエストが行われるが、URL の作成だけされてリクエストが保留された状態」です。

ここで言う署名とは AWS API リクエストの署名のことです。深掘りしていきましょう。

AWS API リクエストの署名とは何か

皆さんは最後にリクエストに署名をしたのがいつか、覚えていますか? AWS CLI や SDK を使用して AWS リソースを操作されたことがある場合、その裏側では署名プロセスが働いています。勝手にやってくれるので意識したことがない方もいるかもしれません。

AWS API リクエストの署名については以下に詳しいですので、このリファレンスに記載されている内容を元に説明していきます。以降、単純に「署名」という場合は AWS API リクエストに付与する署名(バージョン 4 )を表します。

署名生成のイメージ

署名はいくつかのインプットと計算(ハッシュ、エンコード)から生成されます。

ざっくりイメージは以下です。

Sigv4-add

それぞれの値のサンプルを付与すると以下です。

Sigv4

AWS CLI を実行する際に--debugオプションを付与すると、裏側で署名プロセスが走っている様子がある程度具体的に確認できて楽しいのでオススメです。

署名により実現できること

詳細な説明は本エントリでは省略しますが、署名を生成する際のインプットとしてざっくり以下の情報が含まれます。

『「誰が」「いつ」「何に」「どんな内容で」リクエストするか』

リクエストに署名が含まれることで、以下の観点でセキュリティが確保されます。

  • アクセスキー が含まれているためリクエスタの ID が検証される
  • リクエストにハッシュが含まれるため転送中のデータが保護される(改ざんされていれば拒否される)
  • 署名の有効期限が短い(通常 5 分)ため、リプレイ攻撃から保護される

署名はどのようにリクエストに追加されるか

以下のいずれかのパターンで署名が付与された状態のリクエストが実現できます。

  • Authorizationヘッダーに署名を含める
  • クエリ文字列に署名を含める

前者の例は以下です。

Authorization: AWS4-HMAC-SHA256\
 Credential=AKIDEXAMPLE/20150830/us-east-1/iam/aws4_request,\
 SignedHeaders=content-type;host;x-amz-date,\
 Signature=5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7

後者の例は以下です。冒頭で確認した署名付き URL とほとんど同じですね。

https://iam.amazonaws.com\
 ?Action=ListUsers
  &Version=2010-05-08\
  &X-Amz-Algorithm=AWS4-HMAC-SHA256\
  &X-Amz-Credential=AKIDEXAMPLE%2F20150830%2Fus-east-1%2Fiam%2Faws4_request\
  &X-Amz-Date=20150830T123600Z\
  &X-Amz-Expires=60\
  &X-Amz-SignedHeaders=content-type%3Bhost\
  &X-Amz-Signature=37ac2f4fde00b0ac9bd9eadeb459b1bbee224158d66e7ae5fcadb70b2d181d02

「署名そのもの」と「署名の生成に使用したインプット情報の一部」を HTTP(S)リクエストに含め、そのリクエストがサービスエンドポイントに到達することで API コールが実現します。

余談:Authorization ヘッダータイプの署名

マネジメントコンソールから S3 操作(ここではロギングステータスの確認)をした際の S3 アクセスログのイメージが以下です。

xxxxxxxxxxxxxxxxxxxxa0ba501ad4f6e6513e8620f29b3f25cxxxx92a3a24a3 #バケット所有者
<バケット名> #バケット
[31/Dec/2020:23:56:19 +0000] #時間
202.xx.xx.xx  #リモートIP
arn:aws:sts::000000000000:assumed-role/RoleName/SessionName  #リクエスタ
xxxxx310EC0A884E  #リクエストID
REST.GET.LOGGING_STATUS  #オペレーション
- #キー
"GET /<バケット名>?logging= HTTP/1.1"  #リクエストURI
200  #HTTPステータス
-  #エラーコード
243  #Bytes Sent
-  #オブジェクトサイズ
8  #トータルタイム
-  #ターンアラウンドタイム
"-"  #リファラ
"S3Console/0.4, aws-internal/3 aws-sdk-java/1.11.920 Linux/4.9.230-0.1.ac.223.84.332.metal1.x86_64 OpenJDK_64-Bit_Server_VM/25.275-b01 java/1.8.0_275 vendor/Oracle_Corporation"  #User-Agent
-  #バージョンID
xxxxxxxxxIGx7EisJxh3RjWzDTOJs09ifXRQoM6M8W5/ZcrkweGQBzrCBcW61Is3OhZLV4s7uYI=  #ホストID
SigV4  #署名バージョン
ECDHE-RSA-AES128-GCM-SHA256  #暗号スイート
AuthHeader  #認証タイプ
s3.ap-northeast-1.amazonaws.com  #ホストヘッダ
TLSv1.2  #tlsバージョン

ここでは認証タイプが AuthHeaderとなっていることがわかります。マネジメントコンソールをポチポチしているだけでも、裏側で署名プロセスが働いていることに気づかされます。

S3 以外のサービスについても裏で署名してくれているだろうと捉えていますが、それを確認する術が思いつきませんでした。知っている方がいたら教えてください。

IAM における認証と認可

ここまでで、署名により認証が行われていることが分かりました。Black Belt の資料を引用すると、以下までが署名の担当範囲というイメージです。

IAMBlackbelt

署名付きのリクエストがサービスエンドポイントに到達し認証をクリアしたら、認可のプロセスが行われます。

アクションを実行できるかどうか、例えば今回のケースで言えば S3 上のオブジェクトを Get できるかどうかはここで評価されます。chiba-cli ユーザーが直接 AWS CLI で GetObject を実行しようが、chiba-cli ユーザーが生成した署名付き URL で任意のユーザーがアクセスしようが、途中のプロセスはあまり関係ありません。

リクエストが実行された時点の chiba-cli ユーザーが持つアイデンティティベースポリシーやリクエスト先バケットのバケットポリシーなどが評価され、そこで Allow の結果となればアクションは成功します。

プリンシパルが IAM ロールの場合

今回の例では IAM ユーザーで署名付き URL を生成しましたが、「 IAM ロールを引き受けたセッション」で生成することもできます。その場合、署名付き URL 自体の有効期限とセッションの有効期限を考慮する必要があります。

「ロールを引き受けたセッション」で生成できる URL の有効期限は IAM ユーザーのそれと比べて短いという仕様がまずあります。

その上で、署名付き URL の有効期限内であってもセッションの有効期限が切れていればアクションは成功しません。

role-session-sigv4

この辺りは「リクエストが実行された時点の権限構成が評価される」という流れをおさえていれば、「確かに」という感覚ですね。

おまけ:マネジメントコンソールで署名付き URL の生成

最後にちょっとした Tips です。実はマネジメントコンソール上の操作でも署名付き URL は生成できます。

オブジェクトを選択して「開く」を押下します。

S3_Management_Console

そうするとオブジェクトの中身が表示されます。(メタデータで Content-Type が binary/octet-stream の場合はダウンロードが始まってしまうので、あらかじめ text/plain などに変更が必要です。)

S3Sign

ここで開かれている URL は以下のような形式です。まさに署名付き URL ですね。有効期限は 5 分間と短いですが、この URL を他者に共有すれば同じようにアクセスでき(てしまい)ます。

https://cm-chiba-hogehoge.s3.ap-northeast-1.amazonaws.com/test.txt\
 ?response-content-disposition=inline\
  &X-Amz-Security-Token=IQoJb3JpZ2luX2VjEDEaDmFwLW5vcnR(略)\
  &X-Amz-Algorithm=AWS4-HMAC-SHA256\
  &X-Amz-Date=20210602T171918Z\
  &X-Amz-SignedHeaders=host\
  &X-Amz-Expires=300\
  &X-Amz-Credential=ASIAQ3BIIH73RJI4W64X%2F20210602%2Fap-northeast-1%2Fs3%2Faws4_request\
  &X-Amz-Signature=2db8c28a5c6e93613928d4c2d382c6934218dbf44fd0fbabcc5a208956072e03

これはこちらの記事を見て知りました。マネジメントコンソールって「よしなにやる」部分が結構多いですよね。

IAM ポリシー未設定の IAM ユーザーが生成した S3 署名付き URL を使ってオブジェクトを Get できるのはなぜか

長々と書いてきましたが、このエントリで言いたかったことは以下です。

  • 署名付き URL の生成には権限( IAM ポリシー)は必要ない
  • 署名付き URL を使用したアクセスは「生成したエンティティ( IAM ユーザーなど)によるもの」として扱われる
  • 署名付き URL を使おうが使わまいが、認可はリクエストを実行した時点の権限構成をもとに行われる
    • 生成した時点でのエンティティの権限は関係ない

これらをまるっと引っくるめたタイトルを付けたかったのですが、うまく一文に詰め込むことができませんでした。なので微妙にタイトルと本文がズレているな、と思われたかもしれません。でも皆さんは立派な大人なので、その辺りはうまく解釈していただければと思います。

そして「なぜか」に対する直接の回答は「 S3 側で許可が与えられているから 」なのですが、まぁ、それ自身が本筋じゃないというか、ここまで書いてきたことの総括が本当に言いたかったことというか、そういう感じなので、よし、やはり皆さんの大人力に委ねたいと思います。がんばれ!

以上、 チバユキ (@batchicchi) がお送りしました。

参考