Lambda関数のリソースベースポリシーでパブリックアクセスを禁止しない状況だと意図しないアクセスが発生しうる話

Lambda関数ではリソースベースポリシーでパブリックアクセスを禁止することがベストプラクティスにより推奨されています。この一例としてAWS:SourceAccount条件を指定しない場合にどのようなアクセスが発生しうるのかを考えてみました。
2022.05.31

はじめに

清水です。Lambda関数でトリガを利用する際になどに利用するリソースベースポリシー、マネジメントコンソールなどから設定する際には自動的に設定されS3がプリンシパルであればAWS:SourceAccount条件が付与されますが、AWS CLIなどで設定する場合はこのAWS:SourceAccount条件をうっかり忘れてしまうことが起こりえます。プリンシパルをS3にした場合でポリシーにこのAWS:SourceAccountを指定しない場合、パブリックアクセスを許可しているLambda関数となり、例えばSecurity Hubの「AWS 基礎セキュリティのベストプラクティス v1.0.0」では最も高い重要度でチェックがFAILEDとなってしまいます。

対応方法としては上記のSecurity Hubユーザガイドにもあるとおり、S3をプリンシパルとするリソースベースポリシーではAWS:SourceAccount条件を含めることとなるわけですが、AWS:SourceAccount条件を指定しない場合にどのようにパブリックアクセスが許可され、意図しないLambda関数へのアクセスが発生しうるのかの一例を確認してみました。

プリンシパルがS3でAWS:SourceArnで呼び出し元のS3バケットを指定している状態であれば、そのS3バケットの所有者がLambda関数へのアクセスをコントロールでき、基本的にはS3バケットも管理できる状態かと思うので意図しないアクセスは起こりにくそうです。しかしS3バケットとLambda関数でライフサイクルが異なること、またS3バケットは破棄したあとに第三者が同じバケット名称で取得可能なことに注意しなければなりません。もし関係のない第三者がAWS:SourceArnで指定してたS3バケットを取得していた場合、AWS:SourceAccountをリソースベースポリシーで指定していなければLambda関数のARNのみで関数の呼び出しが可能となります。

リソースベースポリシーでパブリックアクセスを許可してしまうケース

リソースベースポリシーはLambda関数でトリガを利用する際などに使用されます。マネジメントコンソールからLambdaのトリガを設定した場合は、このリソースベースポリシーは自動的に作成されます。

上記のようにAWSマネジメントコンソールからトリガを設定したあと、「アクセス権限」の「リソースベースのポリシー」を確認するとポリシーが設定されています。

ここではConditionsとして、AWS:SourceAccountAWS:SourceArn(呼び出し元のS3バケット)が設定されていますね。

さてこのリソースベースポリシーをAWS CLIなどから設定する場合、このAWS:SourceAccountを省略することができてしまいます。例えば以下の具合です。

$ aws lambda add-permission \
    --function-name public-access-lambda-function \
    --statement-id "s3-put-event-1" \
    --action "lambda:InvokeFunction" \
    --principal "s3.amazonaws.com" \
    --source-arn "arn:aws:s3:::my-test-bucket-1"

設定後、マネジメントコンソールで確認すると以下のように、Conditions部分にAWS:SourceAccountがありません。

この状態が「パブリックアクセスを禁止していない」となり、Security Hubの「AWS 基礎セキュリティのベストプラクティス v1.0.0」ではLambda.1がFAILEDとなってしまいます。

パブリックアクセスを許可したLambdaでどのようなことが起こり得るか

この「パブリックアクセスを許可した」Lambda関数にほかの(Lambda関数を所有していない)AWSアカウントからアクセスすることを考えてみます。Lambda関数を所有しているAWSアカウントを「AWSアカウント1」とすれば、別の「AWSアカウント2」のS3バケットへのアップロードをトリガにしてこのLambda関数を呼び出す、というぐあいですね。検証ですので、この「AWSアカウント2」とS3バケットは自身で所有してるものとします。

まずはやはり、S3バケット名を指定したリソースポリシーが必要となります。Lambda関数を所有している「AWSアカウント1」で以下のように、AWS:SourceAccountを指定せずパブリックアクセスを許可した状態でリソースポリシーを指定します。

$ aws lambda add-permission \
    --function-name public-access-lambda-function \
    --statement-id "s3-put-event-2" \
    --action "lambda:InvokeFunction" \
    --principal "s3.amazonaws.com" \
    --source-arn "arn:aws:s3:::my-test-bucket-2"

続いて呼び出し元のS3バケットを所有する「AWSアカウント2」での作業です。S3バケットに対してaws s3api put-bucket-notification-configurationコマンドでイベント通知を設定、オブジェクトの作成時にLambda関数を呼び出すようにします。

$ aws s3api put-bucket-notification-configuration \
    --bucket my-test-bucket-2 \
    --notification-configuration '{"LambdaFunctionConfigurations": [{"LambdaFunctionArn": "arn:aws:lambda:ap-northeast-1:111111111111:function:public-access-lambda-function", "Id": "test-from-another-account", "Events": ["s3:ObjectCreated:*"]}]}'

これで「AWSアカウント2」の所有しているS3バケット(my-test-bucket-2)からLambda関数を呼び出すことが可能です。(Lambda関数にアタッチする実行ロール(IAMロール)の権限についてはいったんおいておきます。)ここで必要な情報としては、設定対象のS3バケット名と、Lambda関数のARNでした。つまりS3バケットを所有している「AWSアカウント2」側としては、Lambda関数のARNさえわかっていればそのLambda関数を呼び出すことができるわけです。

さて、ここまで「AWSアカウント2」は自身で所有しており操作できるものとしてきました。ここで、削除したS3バケットについては関係のない第3者が同じ名称で取得可能であることを思い出してみましょう。AWS:SourceAccountを指定せず、my-test-bucket-2から呼び出し可能としていたLambda関数があったとします。このLambda関数はそのままにS3バケットmy-test-bucket-2だけを削除したとしましょう。その後、何者かがmy-test-bucket-2の名称でS3バケットを取得、さらに何らかの手段でLambda関数のARN(AWSアカウントIDとLambda関数名)がわかる(ないし予測できてしまう)とそのLambda関数を呼び出すことができてしまう、という状況が起こりうるわけです。これが「パブリックアクセスを許可した」Lambda関数の危うさの一例と考えます。「パブリックアクセスを許可しない」つまりリソースベースポリシーでAWS:SourceAccountを指定しておけば、呼び出し元のAWSアカウント自体を制限できこのような自体を避けることが可能となります。

S3をプリンシパルとするリソースポリシーではAWS:SourceAccountを指定しておこう

AWS:SourceAccountを指定しない場合、うっかりS3バケット名を間違えたり、S3バケットを削除してLambda関数だけ残ってしまった場合など、意図しない第3者からLambda関数が呼び出されてしまう可能性があることがわかりました。S3をプリンシパルとするリソースポリシーではAWS:SourceAccountを指定しておきましょう。

AWSマネジメントコンソールからだとポリシーステートメントを指定して[編集]ボタンから編集が行えます。

AWS CLIではaws lambda add-permissionコマンド実行時に--source-accountオプションで指定することを忘れないようにしましょう。(AWS:SourceAccountを追加したいという場合は同じStatement IDではエラーとなってしまうようなので、別のStatement IDを指定して権限を追加したあとに、AWS:SourceAccountのないポリシーを削除するかたちが良いのかなと考えています。)

$ aws lambda add-permission \
    --function-name public-access-lambda-function \
    --statement-id "s3-put-event-2" \
    --action "lambda:InvokeFunction" \
    --principal "s3.amazonaws.com" \
    --source-arn "arn:aws:s3:::my-test-bucket-2" \
    --source-account 222222222222

まとめ

Lambda関数のリソースベースポリシーでプリンシパルをS3とした際に、AWS:SourceAccount条件を含めずパブリックアクセスを禁止しない状況下でどのような意図しないアクセスが発生しうるのか、一例を確認してみました。プリンシパルがS3ではない場合はAWS:SourceArnのみで呼び出し元のリソースの所有AWSアカウントも必然的に特定できるかと思います。(ARNにAWSアカウントIDが含まれているため。)S3も設定のタイミングで所有AWSアカウントは特定できているはずですが、バケット名のタイポだったり、設定後のどこかのタイミングでS3バケットを削除してしまうケースも無きにしもあらずと考えます。S3プリンシパルの場合はAWS:SourceAccountもしっかりと指定するようにしましょう。