はじめに
バックエンドにLambda関数URLを用いる場合のセキュリティについて考える機会がありました。
この場合、Lambda関数URLの認証タイプをNONEに設定すると、誰でも直接Lambda関数にアクセスできてしまいセキュリティ的によろしくありません。
そこで今回の記事では、CloudFrontにLambda関数URLオリジンへのアクセスを制限するためのオリジンアクセスコントロール (OAC) が用意されていますので、それを利用してセキュリティ担保する方法を検証してみました。
POST/PUTリクエストの場合は一工夫必要でしたので、その紹介もさせていただければと思います。
環境構成
Nuxt3 SSR
CloudFront(オリジンはS3とLambda関数URL)
S3
Lambda関数URL
前提条件
OAC を作成して設定する前に、Lambda 関数 URL をオリジンとして持つ CloudFront ディストリビューションが必要ですので、既にこれらのリソースが存在している事を前提とします。
やること
- Lambda関数URLにIAM認証を設定する
- Lambda関数URL へのアクセス許可を OAC に付与する
- OACが採用しているSigV4認証のためにリクエストボディのSHA-256ハッシュ値がx-amz-content-sha256ヘッダーに付与されるようにする
1. Lambda関数URLにIAM認証を設定する
OAC を使用するには、AWS_IAM を指定する必要があります。
以下スクショの通り、Lambda関数の認証タイプをAWS_IAMに更新します。
2. Lambda 関数 URL へのアクセス許可を OAC に付与する
CloudFront サービスプリンシパル (cloudfront.amazonaws.com) に Lambda 関数 URL へのアクセスを許可します。
OACを作成し、CloudFrontのオリジンにOACを設定し、表示されたコマンドをコピーしつつこのオリジン設定を保存します。
コピーしたコマンドの関数名部分を入れ込んだ上でコマンド実行すると、Lambda関数にポリシーが追加されます。
3. OACが採用しているSigV4認証のためにリクエストボディのSHA-256ハッシュ値がx-amz-content-sha256ヘッダーに付与されるようにする
2で終わりかと思いきや、POST/PUTリクエストにおいては、リクエスト本文のSHA256を計算し、本文のペイロードハッシュ値をx-amz-content-sha256ヘッダーに含めた上で、オリジンにリクエスト送信しないとLambda関数のIAM認証を突破できないようでした。
上記公式に以下記述がありました。
OAC認証の仕組み
調べてみると、OAC認証の仕組みは以下であることがわかりました。
- OAC認証の仕組みにAWS Signature Version 4 (SigV4) を使用している(Amazon CloudFront が Lambda 関数 URL オリジンのオリジンアクセスコントロール (OAC) を新たにサポート)
- POST/PUTの場合、リクエストボディの整合性を検証する必要があり、SigV4認証ではx-amz-content-sha256ヘッダーの値を利用する
- x-amz-content-sha256ヘッダーには、リクエストボディからSHA-256ハッシュを計算したものを含める (署名付き AWS API リクエストを作成する)
- SHA-256(Secure Hash Algorithm 256-bit)ハッシュとは暗号学的ハッシュ関数の一つで、一方向性・衝突性・雪崩効果 を特徴としてもつため、データの改ざんがあった場合に検出できる
一方向性:ハッシュ値から元のデータを復元することは実質的に不可能
衝突耐性:異なる入力から同じハッシュ値が生成される確率は極めて低い
雪崩効果:入力データのわずかな変更でも、出力ハッシュ値は大きく変わる
そこで、以下を参考にさせていただき、実際に対応してみました。
簡単に言うと、
Lambda@Edgeではbodyのハッシュ値だけ計算し、SigV4はOACで署名する
というアプローチになります。
Lambda関数を作成
Lambda@Edge関数を作成するには、バージニア北部(us-east-1)リージョンに設定されている必要がありますのでご注意ください。
また、arm64アーキテクチャで関数を作成すると、Lambda@Edgeのデプロイの際に非対応のエラーが出ますので、x86_64で作成します。
アクセス権限に関しては以下のように設定します。
関数コードを編集
以下のように編集し、Deployを押下します。
こちらにもコードを記載しておきます。
const hashPayload = async (payload) => {
const encoder = new TextEncoder().encode(payload);
const hash = await crypto.subtle.digest("SHA-256", encoder);
const hashArray = Array.from(new Uint8Array(hash));
return hashArray.map((bytes) => bytes.toString(16).padStart(2, "0")).join("");
};
export const handler = async (
event,
_context,
) => {
const request = event.Records[0].cf.request;
console.log("originalRequest", JSON.stringify(request));
if (!request.body?.data) {
return request;
}
const body = request.body.data;
// CloudFront経由でLambda@Edgeに渡されるリクエストは、Base64エンコードされた文字列としてボディデータが含まれる。
const decodedBody = Buffer.from(body, "base64").toString("utf-8");
request.headers["x-amz-content-sha256"] = [
{ key: "x-amz-content-sha256", value: await hashPayload(decodedBody) },
];
console.log("modifiedRequest", JSON.stringify(request));
return request;
};
Lambda@Edgeデプロイ
トリガーを追加を押下します。
トリガーの設定にてCloudFrontを選択し、Lambda@Edgeへのデプロイを押下します。
以下スクショの通りに選択し、デプロイします。
CloudFrontのビヘイビアを確認すると、Lambda@Edgeが登録されていることが確認できます。
これで、Lambdaオリジンへのリクエストの際にLambda@Edgeが実行される形となります。
動作確認
ブラウザからLambda関数が実行されるページにアクセスすると、
Chromeの開発者ツールのネットワークより、200が確認できました。
これにより、CloudFront経由でのLambda実行のみ許可されている事が確認できました。
まとめ
OACで対応できるのとても良いですね。
IaC版の記事も書かないと..