予防コントロール [CT.LAMBDA.PV.2] でAPI GatewayからLambdaが呼べない問題とその解決策
AWS Control Towerを導入している環境で、Terraformなどを用いてAPI GatewayからLambdaへの呼び出し権限を付与しようとした際、lambda:AddPermissionに関するAccessDeniedExceptionで失敗したことはないでしょうか?
本記事では、予防コントロール [CT.LAMBDA.PV.2] の仕様と、その制約をどう乗り越えるべきかについての検討結果を共有します。
1. 発生した問題
Terraformで aws_lambda_permission をリソースとして定義し、API Gatewayをプリンシパルとして指定して権限を付与しようとしたところ、以下のエラーが発生しました。
AccessDeniedException: ... is not authorized to perform: lambda:AddPermission on resource: ... with an explicit deny in a service control policy
原因:予防コントロール [CT.LAMBDA.PV.2]
このエラーの正体は、Control Towerによって適用されるSCP(サービスコントロールポリシー)です。このポリシーは、Lambda関数へのクロスアカウントアクセスを制限することを目的としています。
具体的には、lambda:AddPermission を実行する際、許可するプリンシパル(lambda:Principal)が自アカウントのIAMエンティティ(arn:aws:iam::${aws:PrincipalAccount}:*)ではない場合、一律で Deny 判定となります。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CTLAMBDAPV2",
"Effect": "Deny",
"Action": "lambda:AddPermission",
"Resource": "arn:*:lambda:*:*:function:*",
"Condition": {
"StringNotLike": {
"lambda:Principal": [
"arn:*:iam::${aws:PrincipalAccount}:*",
"${aws:PrincipalAccount}"
]
},
"ArnNotLike": {
"aws:PrincipalArn": [
{{ExemptedPrincipalArns}}
"arn:*:iam::*:role/AWSControlTowerExecution"
]
}
}
}
]
}
API Gatewayのサービスプリンシパル(apigateway.amazonaws.com)はこの条件に合致しないため、同一アカウント内からの設定であっても拒否されてしまいます。
2. 暫定対応
ひとまずデプロイを優先するため、対象のアカウントが属するOUから [CT.LAMBDA.PV.2]の適用を解除することで、権限付与が可能になることを確認しました。
3. 恒久対応に向けた比較検討
「クロスアカウント公開を防ぐ」というセキュリティ要件を満たしつつ、API Gateway等のAWSサービス連携をスムーズに行うための2つの案を比較しました。
| 比較観点 | 案1:[CT.LAMBDA.PV.2] 有効 (AssumeRole方式) |
案2:カスタムSCP (+リソースベースポリシー) |
|---|---|---|
| セキュリティ | 最高レベル (IAM Principalベース) |
十分 (SourceArn制限付きなら安全) |
| 実装複雑性 |
高い (各APIに呼び出し用IAM Roleが必要) |
低い (標準的な実装パターン) |
| 運用負荷 | 高い (管理するRoleが増加) |
低い (自動化が容易) |
案1(AssumeRole方式)の具体的な実装例
[CT.LAMBDA.PV.2] が有効な環境では、lambda:AddPermissionを使って「サービスプリンシパル」に権限を与えることができません。そこで、API GatewayにIAMロールを渡して(Assume Role)、そのロールの権限でLambdaを呼び出す構成をとります。
実装のポイント
この方法の肝は、API Gatewayの統合リクエスト設定においてCredentialsプロパティにIAMロールのARNを指定することです。
CloudFormationでの定義例
API GatewayがLambdaを呼び出すための専用ロールを作成し、それをメソッドに関連付けます。
Resources:
# 1. API Gateway用のIAMロールを作成
ApiGatewayInvokeRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: apigateway.amazonaws.com # API Gatewayにこのロールの使用を許可
Action: sts:AssumeRole
Condition:
StringEquals:
"aws:SourceAccount": !Ref "AWS::AccountId"
# さらに絞るなら ArnLike で特定のAPIを指定
# ArnLike:
# "aws:SourceArn": !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${MyApi}/*"
Policies:
- PolicyName: InvokeLambda
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: lambda:InvokeFunction
Resource: !GetAtt MyLambdaFunction.Arn # 特定のLambdaのみに絞る
# 2. API Gatewayのメソッド設定
MyApiMethod:
Type: AWS::ApiGateway::Method
Properties:
# ... (中略)
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyLambdaFunction.Arn}/invocations
# 💡 ここが重要:作成したロールを指定 💡
Credentials: !GetAtt ApiGatewayInvokeRole.Arn
案2:カスタムSCPの設計例
以下のように、lambda:Principal の許可リストに *.amazonaws.com(AWSサービスをプリンシパルとする)を追加したカスタムSCPを作成することで、AWSサービスからの連携を許可できます。
// ~~~
"Condition": {
"StringNotLike": {
"lambda:Principal": [
"arn:*:iam::${aws:PrincipalAccount}:*",
"${aws:PrincipalAccount}",
"*.amazonaws.com" // AWSサービスを許可
]
}
}
// ~~~
注意:
lambda:AddPermission実行時のSCPによる制限(予防コントロール)では、SourceArnの有無までチェックすることはできません(lambda:AddPermissionに対してSCPが評価できるCondition keysは、lambda:Principal、lambda:FunctionUrlAuthTypeに限られるため)。(Actions, resources, and condition keys for AWS Lambda)
4. 注意点:「混乱した代理 (Confused Deputy)」問題
カスタムSCPでAWSサービスからの呼び出しを許可する場合(案2パターン)、「混乱した代理」問題への対策が必須となります。
単に apigateway.amazonaws.com を許可するだけでは、他人のAWSアカウントにあるAPI Gatewayから自分のLambdaを呼び出せてしまうリスクがあります。これを防ぐため、Lambda側のリソースポリシーには必ず SourceArn や SourceAccount の条件句を含め、呼び出し元を特定する必要があります。
{
"Version": "2012-10-17",
"Id": "default",
"Statement": [
{
"Sid": "AllowInvokeFromAPIGateway_VULNERABLE",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-west-2:111111111111:function:MySensitiveFunction"
// ⚠️ ここに Condition (SourceArn や SourceAccount) がないのが致命的 ⚠️
}
]
}
{
"Version": "2012-10-17",
"Id": "default",
"Statement": [
{
"Sid": "AllowInvokeFromAPIGateway_SECURE",
"Effect": "Allow",
"Principal": {
"Service": "apigateway.amazonaws.com"
},
"Action": "lambda:InvokeFunction",
"Resource": "arn:aws:lambda:us-west-2:111111111111:function:MySensitiveFunction",
"Condition": {
"ArnLike": {
// 🚩 これが重要:呼び出し元のAPI GatewayのARNを指定して絞り込む 🚩
"aws:SourceArn": "arn:aws:execute-api:us-west-2:111111111111:a1b2c3d4e5/*/*/*"
}
}
}
]
}
なお、案1パターンでもAssumeRoleに対する混乱した代理問題が生じますが、[CT.STS.PV.1] を併用することで、同じOrganizations以外からのクロスアカウントアクセスを抑制することができます。
まとめ
- [CT.LAMBDA.PV.2] は、API Gateway等のサービスプリンシパルによる権限付与もブロックする
- 開発の利便性を優先するなら、カスタムSCPで
*.amazonaws.comを許可するのがベター- ただし、Lambdaのリソースベースポリシーで
SourceArnによる制限を徹底する運用(またはAWS Config等による検出)が必要
- ただし、Lambdaのリソースベースポリシーで
- セキュリティ基準を担保したいなら、[CT.LAMBDA.PV.2] 有効(AssumeRole方式) がベター
- ただし、混乱した代理問題は生じうるので、[CT.STS.PV.1] を併用するなどの多層防御を検討
Discussion