この記事は、LITALICO Advent Calendar 2025 シリーズ3 4x日目の記事です。
はじめに
S3 / CloudFront、便利ですよね。
この記事では、S3でファイルを管理し、CloudFrontの署名付きCookieを使ってアクセス権を管理しようとしたときにハマったポイントを紹介します。
本記事は、執筆時点で絶賛対応中の話題であり、最終的な対応方針はまだ確定していません。
なお、本記事の作成にあたって使用した言語・フレームワークおよびライブラリは以下の通りです。
- PHP: 8.3
- Laravel: 12.40
- aws-sdk-php: 3.363
ユースケース
※実際の内容ではありません(説明を簡略化するための例です)
前提
- 部署の中に複数のプロジェクトがあるとします
- 部署ごとに管理者がいるものとします
やりたかったこと
- 管理者は、プロジェクトを指定してファイルをアップロードする
- ※複数のプロジェクトを指定することはない
- 管理者は、自身がアップロードしたファイルを参照できる
- 利用者は、自身が関与しているプロジェクトに向けてアップロードされたファイルを参照できる
なぜ署名付きURLではなく署名付きCookieを使おうとしたか
- 特定フォルダ配下に対してまとめて権限を付与できるため
- ただし、フォルダ構成に関しては検討が必要
- →逆に言えば、フォルダ構成さえしっかり検討できていれば、URL1件ごとに署名を作成する必要がない分、署名付きCookieのほうが運用・性能の面で有利になると考えました
ハマったポイント
1. ローカルでは動くのにデプロイするとエラーになる
結論
サーバのOpenSSL(3系)で sha1形式の署名が許可されていなかった
※署名付きURLでも同じ問題は発生します
詳細
サーバのエラーログには error:1E08010C:DECODER routines::unsupported というエラーが出ていました。
OpenSSL 3系では、セキュリティポリシーによりsha1を使った署名がデフォルトで無効化されている場合があります。
そこで、サーバでsha1形式の署名が無効化されているかどうかを確認するため、以下コマンドを実行しました。
echo "test" | openssl dgst -sha1 -sign private.pem -out outfile
※private.pemはCloudFrontのキーペアで使用している秘密鍵です
その結果、サーバで実行したコマンドではエラー(ローカルでは成功)になり、原因が「sha1形式の署名が無効化されている」と特定できました。
少しだけソースコードに視点を当てると、
openssl_sign() 関数の引数は第4引数まであり、省略するとsha1形式で署名を行います。
aws-sdk-phpの署名を行っている処理を見てみると、 openssl_sign() 関数を第3引数まで指定して実行しており、ここを外部から指定する術がありません。
対応
以下どちらかの対応を行う必要がありました。
- サーバのOpenSSL設定でsha1形式の署名を有効化する
-
openssl_sign()を使わずにsha1形式の署名を行う- → OpenSSLの制約を回避できるため、今回はこちらを採用
同様の課題を抱えている記事があったため、そちらを参考にphpseclibを使用して対応しました。
2. 特定の利用者でCloudFrontへのアクセスが必ず403になる
結論
カスタムポリシーのJSONには Statementを1つしか指定できなかった
詳細
簡略化していますが、S3に配置するファイルのフォルダ構成を以下のように設計していました。
/部署/ここにもうちょっと階層がある/プロジェクト/ファイル
このとき、複数のプロジェクトに関わっている利用者の場合、以下のようなアクセス権を期待しています。
-
/自部署/*/関わっているプロジェクトA/*→ アクセスできる -
/自部署/*/関わっているプロジェクトB/*→ アクセスできる -
/自部署/*/関わっていないプロジェクトC/*→ アクセスできない -
/他部署/*/*→ アクセスできない
これをCloudFrontのカスタムポリシーとして定義するとき、以下のようなJSONを作成しました。
{
"Statement": [
{
"Resource": "https://xxx.cloudfront.net/自部署/*/関わっているプロジェクトA/*",
"Condition": {
"DateLessThan": {
"AWS:EpochTime": 12345
}
}
},
{
"Resource": "https://xxx.cloudfront.net/自部署/*/関わっているプロジェクトB/*",
"Condition": {
"DateLessThan": {
"AWS:EpochTime": 12345
}
}
}
]
}
しかし、このポリシーから生成したCookieでリクエストすると、
curl -i 'https://xxx.cloudfront.net/自部署/xxx/関わっているプロジェクトA/ファイル' \
-b 'CloudFront-Key-Pair-Id=xxx; CloudFront-Signature=yyy; CloudFront-Policy=zzz'
HTTP/1.1 403 Forbidden
...
<?xml version="1.0" encoding="UTF-8"?><Error><Code>MalformedPolicy</Code><Message>Malformed Policy</Message></Error>
MalformedPolicy、つまりポリシーの構文に誤りがあるというエラーが返ってきます。
最初はなぜかわかりませんでしたが、よく公式ドキュメント
を読んでみると、
次の点に注意してください。
・1 つのステートメントのみを含めることができます。
という記載があります。
1 つのステートメントのみを含めることができます。
1 つのステートメントのみを含めることができます。
1 つのステートメントのみを含めることができます。
完全に筆者の見落としなのですが、Statementが配列形式であるため複数指定できそうに見えますが、
CloudFrontの仕様としては 1つのみ という制約がありました。
対応(予定)
フォルダ構成を工夫してもステートメントを1つにまとめることは難しく、署名付きCookieでは要件を満たせないと判断しました1。
そのため、署名付きURLにおける制限(有効期限・発行コスト)や性能面等を改めて整理したうえで、署名付きURLに切り替える予定です。
さいごに
CloudFrontの署名付きCookieは強力な仕組みですが、フォルダ設計とポリシーの仕様を正しく理解していないと、設計段階で詰む可能性があると痛感しました。
これから署名付きCookieを検討する方の参考になれば幸いです。
-
CookieのPath指定も考えましたが、与えたいアクセス権が前方一致ではなく途中にワイルドカードを挟むため断念しました。 ↩