S3 Presigned URLをCloudFrontでリバプロする構成を検証してみた
こんにちは!製造ビジネステクノロジー部の石井です。
CloudFront 経由でファイルのアップロード/ダウンロードを実現する方法として、S3 Presigned URL をリバプロする方式と、CloudFront署名付きURL + OAC の2方式を実際に検証しました。
結論:新規設計なら CloudFront署名付きURL + OAC が基本になりそうです。リバプロ方式は動くものの、セキュリティとキャッシュ面でいくつか気になる点が見えてきました。この記事では、両方式を実際に試して見えてきたトレードオフをまとめます。
なお、CF署名付きURLの発行方法(キーグループの作成〜URL生成まで)については こちらの記事 が参考になります。
前提整理
S3 Presigned URL は便利ですが、URLが https://<bucket>.s3.amazonaws.com/... という S3 ドメインを指します。
許可ドメインが絞られている環境ではこのドメインにアクセスできず、せっかく発行した署名URLにクライアントから到達できません。
「CloudFront のカスタムドメインに差し替えて、CloudFront 経由で S3 に届くようにすればいいのでは?」というのが当初のアイデアでした。
ドメインを差し替えるだけなので、既存システムの改修コストが小さいのも魅力でした。
ただ、ここで CloudFront + S3 の署名URL方式について整理しておく必要があります。2系統あります。
| 方式 | 認証主体 | 何を守るか |
|---|---|---|
| S3 Presigned URL | IAMロール(S3が署名検証) | S3オブジェクトへのアクセス |
| CloudFront署名付きURL | CloudFrontキーグループ(CFが署名検証) | CloudFrontビューア入口 |
これに加えて、CloudFront → S3 のバイパス防止として OAC(Origin Access Control)があります。OACは CloudFront が S3 に向かう時に SigV4 で署名する仕組みで、S3 側で「CloudFront経由のリクエストだけ受け付ける」という制御ができます。
ここで重要な前提として、S3 Presigned URL と OAC は併用できません。
OACが付与する Authorization ヘッダー(SigV4)と、Presigned URLのクエリ文字列署名が同時に存在すると S3 が拒否します。InvalidArgument: Only one auth mechanism allowed というエラーが返ります。
なのでリバプロ方式を取るには、Presigned URL を使うビヘイビアは OACを外す必要があります。
検証1: S3 Presigned URL + リバプロ方式を組んでみた
最初に試したのはこの方式。CDK で以下の構成のディストリビューションを作りました。
| ビヘイビア | 用途 | OAC |
|---|---|---|
/upload/* |
Presigned URL でPUT | なし |
/data/* |
Presigned URL でGET | なし |
Presigned URL を使うビヘイビアは OAC なしになります。
検証項目は以下の6つで、全部OKでした。
| # | 検証項目 | 結果 |
|---|---|---|
| 1 | Presigned URL GET(署名検証の基本確認) | OK |
| 2 | Presigned URL PUT(ファイルアップロード) | OK |
| 3 | CORS: Access-Control-Allow-Origin | OK |
| 4 | CORS: Access-Control-Expose-Headers: ETag | OK |
| 5 | マルチパートPUT + ETag取得 | OK |
| 6 | S3直接アクセス(署名なし)が拒否される | OK (403) |
一応動いたのですが、改めてセキュリティ観点で整理してみると、いくつか気になる点が出てきます。
セキュリティ面の整理
リバプロ方式で気になった点は以下です。
-
CloudFrontバイパスが可能
S3 Presigned URL は S3 ドメインへ直接投げられる URL です。CloudFront を経由せず S3 に届くため、リクエスト経路を CloudFront に強制できません。
-
WAF / IP制限 / Shield Standard をバイパスされうる
CloudFront 側に WAF や IP 制限を設定していても、S3 直接経路では効きません。Presigned URL さえあれば CloudFront を迂回して S3 まで届きます。
-
キャッシュが事実上使えない
Presigned URL は
X-Amz-Signatureがクエリ文字列に乗ります。これが毎回違うのでキャッシュキーが変動し、CDN として機能しません。実質CachePolicy: CachingDisabledが必要で、CDN本来の役割を捨てる構成になります。
検証2: CloudFront署名付きURL + OAC でマルチパートアップロードを試してみた
リバプロ方式を選んでいた裏には、「もう一つの方式である CloudFront署名付きURL + OAC では、マルチパートアップロードが通らない」という思い込みがありました。
CreateMultipartUpload や CompleteMultipartUpload は S3 の API で、CloudFront 署名で発行できるのは「CloudFront 上の URL」だから、API 呼び出しは厳しいはずだと思っていました。
これ、実際にやってみたら普通に通りました。
ビヘイビアを1個追加して検証しました。CDK だとこういう設定です。
'/cf-signed-upload/*': {
origin: origins.S3BucketOrigin.withOriginAccessControl(uploadBucket, {
originAccessLevels: [
cloudfront.AccessLevel.READ,
cloudfront.AccessLevel.WRITE,
],
}),
allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL,
cachePolicy: cloudfront.CachePolicy.CACHING_DISABLED,
originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
trustedKeyGroups: [keyGroup],
},
- OACあり(
originAccessLevels: [READ, WRITE])でs3:PutObjectを許可 trustedKeyGroupsで CloudFront 公開鍵による署名を必須化OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADERでクエリパラメータを S3 に転送
この構成で、マルチパートアップロードのフルフローを実行しました。
| ステップ | メソッド | 結果 |
|---|---|---|
| CreateMultipartUpload | POST | 200 OK(UploadId取得) |
| UploadPart (Part 1: 5MB) | PUT | 200 OK + ETag |
| UploadPart (Part 2: 1KB) | PUT | 200 OK + ETag |
| CompleteMultipartUpload | POST | 200 OK |
全部通りました。
ハマりポイントとしては以下です:
OriginRequestPolicyを設定しないと?uploadsなどのクエリパラメータがS3に転送されず405 MethodNotAllowed- デフォルトの OAC は
s3:GetObjectのみなので、originAccessLevelsにWRITEを追加してs3:PutObjectを許可する
CloudFront 経由で S3 API を呼ぶ際、クエリパラメータをちゃんと S3 に転送する設定さえあれば動きます。AllViewerExceptHostHeader がその役割を果たしてくれます。
各ステップで呼ぶ URL は以下のイメージです。署名は @aws-sdk/cloudfront-signer の getSignedUrl で付与しています。
// CreateMultipartUpload
const createUrl = getSignedUrl({ url: `https://${cfDomain}/${key}?uploads`, ... });
await fetch(createUrl, { method: 'POST' });
// UploadPart(partNumber と uploadId をクエリに付ける)
const partUrl = getSignedUrl({ url: `https://${cfDomain}/${key}?partNumber=1&uploadId=${uploadId}`, ... });
await fetch(partUrl, { method: 'PUT', body: partData });
// CompleteMultipartUpload
const completeUrl = getSignedUrl({ url: `https://${cfDomain}/${key}?uploadId=${uploadId}`, ... });
await fetch(completeUrl, { method: 'POST', body: completeXml });
CF署名付きURLの発行方法(キーグループの作成〜URL生成まで)については、こちらの記事が参考になります。
CloudFrontの署名付きURL(signed URL)で、データのGetとPutを試す
当初の「CF署名+OACではマルチパート無理」という思い込みは、実際には問題ありませんでした。
両方式の比較と選び方
両方式を比較してみます。
| 観点 | S3 Presigned URL + リバプロ(OACなし) | CloudFront署名付きURL + OAC |
|---|---|---|
| OAC | 併用不可 | 併用可能 |
| マルチパートアップロード | OK | OK |
| 認証主体 | IAMロール(S3が署名検証) | CloudFrontキーグループ(CFが署名検証) |
| CloudFront経由の強制 | 不可(Presigned URLはS3ドメインへ直接届く) | 可能(CF署名URLはCloudFront経由でしか使えない) |
| WAF / IP制限 / Shield | バイパス可能(S3直接経路) | 全リクエストに効く |
| キャッシュ | 事実上不可 | 可能(CF署名のクエリをキャッシュキーから除外できる) |
| 既存コード変更量 | 小(ドメイン差し替えのみ) | 大(署名ロジックの全面変更) |
| 鍵管理 | 不要 | 必要(RSA鍵ペアのローテーション) |
セキュリティとキャッシュの観点では CF署名+OACが優位、というのが整理結果です。
選定の指針はざっくり以下です。
CF署名+OAC を選ぶケース:
- 新規設計
- セキュリティ要件あり(CloudFront経由の強制、WAF、IP制限)
- CDNキャッシュを活かしたい
リバプロ方式を選ぶケース:
- 既存システムが S3 Presigned URL前提でガッツリ動いており、CF署名への全面書き換えコストが見合わない
- RSA鍵ペアの管理運用を増やせない事情がある
なお、「ファイアウォール対策 = リバプロ必須」ではない点に注意です。CloudFront署名+OAC も CloudFront のカスタムドメインに通すので、S3 ドメインを直接叩く必要はありません。許可ドメインの制限回避だけが目的なら CF署名+OAC でも解決できます。
まとめ
リバプロ方式(S3 Presigned URL + OACなし)は一通り動くものの、CloudFrontバイパス・WAFバイパス・キャッシュ不可など弱点があります。新規設計なら CF署名+OAC を基本に考えるのが無難で、リバプロは「どうしても切り替えられない」時の逃げ道として覚えておくくらいでいいと思います。
「CF署名+OACではマルチパートが通らない」という思い込みは誤りでした。OriginRequestPolicy さえ設定すれば動きます。
思い込みで選択肢を潰したままにしないよう、前提は一度疑ってみるのが大事だと改めて感じました。
おまけ
クラスメソッド名古屋オフィス(伏見駅徒歩5分)で毎月開催している「なごやクラメソゆる勉強会」、今月(6月)も開催予定です!
5月は別イベントとの兼ね合いでお休みしましたが、6月は復活予定です。
詳細日程・テーマはconnpassで近日公開予定なので、興味ある方はぜひチェックしてみてください。
名前のとおりゆるめの雰囲気なので、気軽に参加できます!
前回(第2回)の内容はこちらです!






