こんにちは、サーバーサイドエンジニアのiwamu(@k_iwamu)です。
CloudFrontからS3のプライベートなコンテンツへのアクセス制御はどのように管理されているでしょうか。
アクセス制御の主要な方法の1つとしては、CloudFrontの署名付きURLを作成し、一時的に権限を与える方法が挙げられるかと思います。
しかし、Lambda@edgeを用いれば、アクセス制御をプログラムでもっと柔軟に、かつCloudFront+Lambda@edgeの特性上レイテンシも少なく実装することができます。
今回は、CloudFrontは署名付きURLを使用せず、Lambda@edgeとJWTを使ってアクセス制御を行うように実装したので共有します!
ボツになってしまった案とその課題
内部事情ですが、設計の前提としてCloudFrontのURLをクライアント側で保存している箇所がありました。
その前提のもとで署名付きURLを使おうといくつか設計を考えましたが、それぞれで課題が見つかりました。
1.API側で署名付きURLを作成してそれをクライアントで保存してもらう
課題: 期限があるため保存する意味も少なく、期限が切れると署名付きURLを作成しなおす必要がある。
2.クライアントからURLをAPIに送り、APIで署名付きURLを作成する。
課題: コンテンツの数だけAPIを利用する必要があり、負荷がコンテンツの量に依存する。
3.保存しているURLをもとにクライアント側で署名付きURLを作成する。
課題: キーをクライアントに渡すことで漏洩の可能性があり、セキュリティの観点から望ましくない。
そういった事情があり設計を考えるのに紆余曲折した結果、以下の設計にたどり着きました。
採用した案
APIでJWTを生成し、CloudFrontのOriginRequest時にLambda@edgeを起動し検証する案を採用しました。
この案を採用する大きなメリットとしては、以下があげられます。
- クライアントで保存しているCloudFrontのURLをそのまま利用できる。
- APIの負荷が少ない。JWTを生成するAPIを一度だけ利用するだけで、一定期間はそのJWTを使えばコンテンツ取得ができるので。
- セキュリティの懸念が少ない。機密情報(JWTを生成するためのキー)はサーバー側で保持するので。
- 画像取得のレイテンシを抑えられる。JWT検証時、CloudFrontと同じロケーションにあるLambda@edgeで検証を行うことに加え、Lambda@edgeから外部へのネットワーク通信がないので。
- 認証のロジックを柔軟にできる。JWTのペイロードに様々な値を組み込むことができ、認証もLambda@edgeのプログラムで行うので。
各サービスの役割
○ API
APIではJWTを生成しています。
アルゴリズムは秘密鍵/公開鍵を用いるRSA256を用いて、Lambda@edgeで公開鍵で検証できるようにしています。
payloadには認証に必要な情報(例: user_id)や期限を付与しています。※payload部分はただbase64エンコードするだけなので、機密情報は含まないようにしましょう。
また、JWT生成時の秘密鍵はSecrets Managerに設定し、ビルド時に取得してパッケージングしておく、もしくは処理時に取得するようにすることで、秘密鍵の漏洩を防ぐこともできます。
payloadの例
{ user_id: 1000 expire_at: 1619579877 }
○ CloudFront
CloudFront側では、Behaviorで以下の設定をします。
Origin Request(S3へのリクエスト)時にLambda@edgeを起動させる
JWTを送信するヘッダー(X-photo-auth)ごとでcacheする
設定時の注意点として2つ挙げます。
- アクセスを署名付きURLのみに制限する設定はOFFにしておくこと。セキュリティに関わることなので紛らわしいが、今回はLambda@edge側で認証を行うためこの設定は不要のため。
- cacheの期限とJWTの期限を合わせておくこと。cacheの期間を長くしておくとそれだけJWTが漏れた時にリスクが大きくなりますし、逆に短いとそれだけLambda@edgeが起動する回数も増えてしまうため。
○ Lambda@edge
Lambda@edgeでは、JWTを検証します。
また、検証に柔軟なロジックが加えられることから、例えばpayload内の値とS3のPathを突き合わせるなどのロジックを組むことが可能です。
Lambda@edgeの注意点
Lambda@edgeはLambdaと違って制限が多いので、いくつかの点で注意が必要です。(2021/05現在)。
- 利用できる言語やバージョンが少ない
- timeoutやメモリサイズも制限が厳しい
- 環境変数が使用できないこと
- etc...
特に今回は環境変数が使用できないことが大きなネックであり、公開鍵を取得するためにひと工夫する必要があります。
以下は公開鍵を取得するにあたり推奨しないパターンです。
- クレデンシャル情報をハードコーディングすること => セキュリティの懸念や変更のたびにコードへの変更が必要になるため。
- Lambda@edge起動時にSecrets Managerから取得するようにすること => ネットワークの通信が発生しレイテンシが発生するため。コンテンツの量が多くLambda@edgeが大量に起動すると、Secrets Manager側の上限にも引っかかる恐れがある。
今回はLamnda@edgeビルド時に環境変数ファイル(.env)を作成し、Secrets Managerに保存している値を保存してLambdaにパッケージングし、 アプリケーション側ではdotenvを用いて値を取得するようにしました。
.envを作成するためのスクリプトです。デプロイ時に実行します。
#!/bin/sh ENV=${1} SCRIPT_DIR=$(cd $(dirname $0); pwd) FILENAME="${SCRIPT_DIR}/.env" PUBLIC_KEY_BASE64_ENCODED=$(aws secretsmanager get-secret-value --secret-id famm-photo-jwt-${ENV}| jq -r '.SecretString' | jq -r '.PUBLIC_KEY_BASE64_ENCODED') echo "PUBLIC_KEY_BASE64_ENCODED=${PUBLIC_KEY_BASE64_ENCODED}" > ${FILENAME} // 他必要な環境変数があれば追記していく。
Lambda@edgeアプリケーション側です。dotenvを利用するとprocess.envから取得できます。
require("dotenv").config(); console.log(process.env.PUBLIC_KEY_BASE64_ENCODED) // => xxxxxxx...
Node.jsでLambda@edgeを作成する場合、JWTの検証はnode-jsonwebtokenを使うと便利です。
(補足)Lambda@edgeのログ
Lambda@edgeのログを探すのには一手間かかります。
Lambda@edgeはus-east-1に作成しますが、実際は様々なロケーションで起動するため、ログも各リージョンに保存されており、さらにはログのprefixは /aws/lambda/us-east-1/ となっているからです。
○ S3
S3はバケットポリシーでCloudFrontからのアクセスを許可しておきましょう。
まとめ
JWTはサービス間で認証認可する方法に使えるので、頭にいれておくと設計時などで役立つ時があるかと思います。
Lambda@edgeはLambdaよりも制限が多いので注意は必要ですが、4種類のCloudFrontイベントをトリガーにでき、様々な要件に応えられるのでぜひ活用例などを調べてみてください。
PR
子育て家族アプリFammを運営するTimers inc.では現在エンジニアを積極採用中! オンラインでの面談やカジュアルランチなどもやってますので是非お気軽にご連絡ください!