IP制限を適用したIAMロールを使いつつ、Serverless FrameworkでLambda関数をローカルからデプロイしようとしてハマった話を書きます。
[AWSリソースを守る]
AWSのアクセスキーを誤ってGitHubなどに公開してしまって、アクセスキーを不正利用されて高額請求の被害にあう、という事例をたまに耳にします。
そもそもアクセスキーなどをgitにコミットしてしまうから不正利用につながるので、gitへのコミットを抑止するツールがawslabsのGitHubリポジトリで公開されています。
ただ、以下のようなケースも考えられます。
- git-secretsの設定を忘れてしまう
- git以外の経路(メールやチャットツールなど)で公開してしまう
なので、追加の防御策としてグローバルIP制限で不正利用対策をしてみます。
[IP制限IAMポリシー]
というわけで、まずは結論です。
グローバルIP制限をしつつ、Serverless Frameworkを利用可能なIAM Policyは以下のようになります。ポイントはNotIpAddress条件と、Null条件を指定している部分です。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GlobalIpFinalPolicy",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"XXX.XXX.XXX.XXX/XX",
"YYY.YYY.YYY.YYY/YY",
]
},
"BoolIfExists": {
"aws:ViaAWSService": "false"
}
}
}
]
}
上記ポリシーとAdministratorAccessポリシーを組み合わせることで、IP制限をしつつServerless Frameworkを利用してLambda関数をデプロイできます。
[Serverless FrameworkでLambda関数デプロイ]
NotIpAddress条件は、グローバルIPでAWSへのアクセスを制限する際に利用されます。
以下では、もう一つの条件であるNull条件が必要になった経緯を見ていきます。
まずは、以下のようにシンプルにIP制限のみの条件がついたIAMポリシーを利用して、Serverless FrameworkでLambda関数をデプロイしてみます。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GlobalIpOnlyPolicy",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"XXX.XXX.XXX.XXX/XX",
"YYY.YYY.YYY.YYY/YY",
]
}
}
}
]
}
上記IAMポリシーとAdministratorAccessポリシーがついたIAMロール(ここではGlobalIpRoleという名前とします)を引き受けて一時的な認証情報を取得します。
$ aws sts assume-role --role-arn arn:aws:iam::123456789012:role/GlobalIpRole --role-session-name cli-test
取得した一時的な認証情報を今回は環境変数に設定して利用します。
$ export AWS_ACCESS_KEY_ID=(assume-roleコマンドの結果のアクセスキー)
$ export AWS_SECRET_ACCESS_KEY=(assume-roleコマンドの結果のシークレットキー)
$ export AWS_SESSION_TOKEN=(assume-roleコマンドの結果のセッショントークン)
Serverless Frameworkでデプロイするリソースは、以下のようなLambda関数のみとします。
# Serverless Frameworkをインストールしておく(npmのインストールは省略)
$ npm install -g serverless
$ cat serverless.yml
service: myService
provider:
name: aws
runtime: nodejs22.x
functions:
functionOne:
handler: handler.functionOne
$ cat handler.js
module.exports.functionOne = function (event, context) {
console.log(event);
console.log(context);
};
ここまで準備ができたらsls deployコマンドを実行します。
すると、CloudFormationスタックを作成して、関数コードを含むzipファイルがS3に送られて、Lambdaがそれを利用して実行する準備ができる......はずなのですが、下の画像のようにS3へのアクセスが拒否された旨のメッセージが出て、デプロイが失敗します。

S3のオブジェクトへのアクセスで何が起きているかを確認するために、CloudTrailのイベントログを有効化します。
もう一度sls deployコマンドを実行してみると、以下のようなCloudTrailログが出力されていました。
{
"eventVersion":"1.11",
"userIdentity":{
"type":"AssumedRole",
(略)
"arn":"arn:aws:sts::(略):assumed-role/GlobalIpRole/cli-test",
(略)
},
(略)
"eventSource":"s3.amazonaws.com",
"eventName":"GetObject",
"sourceIPAddress":"cloudformation.amazonaws.com",
"userAgent":"cloudformation.amazonaws.com",
"requestParameters":{
(略)
"key":"serverless/myService/dev/1767435969801-2026-01-03T10:26:09.801Z/compiled-cloudformation-template.json"
},
(略)
"vpcEndpointId":"AWS Internal",
"vpcEndpointAccountId":"AWS Internal",
"eventCategory":"Data"
}
sourceIPAddressがIPアドレスの形式になっておらず、CloudFormationのサービスからのアクセスであることがわかります。おそらくこれが原因で、IAMポリシーの条件に違反してアクセスが拒否されているものと思われます。
CloudTrailログをみるとAWS内部のVPCエンドポイント経由のアクセスになっているので、この部分を判定に利用して「VPCエンドポイント経由の場合には許可する」ようにすれば良さそうです。というわけでaws:SourceVpce条件キーを利用して次のようにIAMポリシーを修正します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GlobalIpPolicy1",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"XXX.XXX.XXX.XXX/XX",
"YYY.YYY.YYY.YYY/YY",
]
},
"Null": {
"aws:SourceVpce": "true"
}
}
}
]
}
これでsls deployコマンドを実行すると、無事成功することが確認できました。
ですが、上記のポリシーではVPCエンドポイント経由の場合にアクセスが許可されてしまう問題があります。
もう少しいい条件はないか...と探してみたところ、aws:ViaAWSService条件キーも利用できそうです。
aws:ViaAWSService条件キーを利用すると、IAMポリシーは以下のようになります。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "GlobalIpPolicy2",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"NotIpAddress": {
"aws:SourceIp": [
"XXX.XXX.XXX.XXX/XX",
"YYY.YYY.YYY.YYY/YY",
]
},
"BoolIfExists": {
"aws:ViaAWSService": "false"
}
}
}
]
}
デプロイしていたリソースを一度削除してから、改めてsls deployを実行すると、こちらもデプロイが成功しました。
しかし、結局AWSサービス経由の場合にアクセスが許可されてしまう問題は残ります。
AWSサービス経由もしくはVPCエンドポイント経由の場合にアクセスが許可されてしまう問題については、IAMロールの一時的な認証情報なので短期間で使えなくなるはず、ということで許容することにしました。
もし何か良い解決策をお持ちの方がいたら教えて欲しいです。