[アップデート] サービスプリンシパルを含む IAM ポリシーの管理を簡素化する AWS グローバル条件キーが追加されました
コンバンハ、千葉(幸)です。
2021/5/4 のアップデートにより、以下の AWS グローバル条件キーが追加されました。
aws:PrincipalIsAWSService
aws:PrincipalServiceName
aws:PrincipalServiceNamesList
これらの条件キーの使用が想定されているのは、リソースベースポリシー、特に S3 バケットポリシーです。「S3 バケットのパーミッションでアクセスを限定したいが、 サービスプリンシパルからのアクセスは柔軟に許可しておきたい」……というケースで役立ちます。
先にまとめ
- サービスプリンシパルとは
Princial
要素で指定された AWS サービスを表す - 今回追加された条件キーを使用することが想定されるのはアイデンティティベースポリシーでなくリソースベースポリシー
- S3 バケットで「送信元の VPC エンドポイントや IP アドレスを制限しているがサービスプリンシパルからのアクセスは許可したい」といったシナリオで有効
aws:PrincipalIsAWSService
- Bool 条件演算子でリクエストが「サービスプリンシパルかどうか」を判定
aws:PrincipalServiceName
- 文字列条件演算子でリクエストに含まれているサービスプリンシパル名が条件に一致するかを判定
- 複数のサービスプリンシパルを指定できる
aws:PrincipalServiceNamesList
- 使い方を私が分かっていない
サービスプリンシパルとは
先にサービスプリンシパルという言葉をおさらいします。
IAM の JSON ポリシーにおけるPrincipal
要素で AWS サービスを指定する場合、それはサービスプリンシパルと呼ばれます。
例えば CloudTrail ログを S3 バケットに出力したいとなった場合、出力先の S3 バケットのポリシーで以下の許可を与える必要があります。
ここではサービスプリンシパルとしてcloudtrail.amazonaws.com
が指定されています。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "AWSCloudTrailAclCheck20150319", "Effect": "Allow", "Principal": {"Service": "cloudtrail.amazonaws.com"}, "Action": "s3:GetBucketAcl", "Resource": "arn:aws:s3:::myBucketName" }, { "Sid": "AWSCloudTrailWrite20150319", "Effect": "Allow", "Principal": {"Service": "cloudtrail.amazonaws.com"}, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::myBucketName/[optional prefix]/AWSLogs/myAccountID/*", "Condition": {"StringEquals": {"s3:x-amz-acl": "bucket-owner-full-control"}} } ] }
サービスプリンシパル以外のプリンシパルとしては、以下が存在します。
- AWS アカウントと ルートユーザー
- IAM ユーザー
- フェデレーティッドユーザー(ウェブ ID または SAML フェデレーションを使用)
- IAM ロール
- ロールを引き受けるセッション
- 匿名ユーザー(非推奨)
AWS サービスだけどサービスプリンシパル以外を使用するケース
「 AWS サービスから S3 への出力」というケースにおいて、必ずしもすべてでサービスプリンシパルが使用されるわけではありません。
例えば AWS Config では、セットアップ時に指定した IAM ロールを引き受けての書き込みを優先して行い、そこで必要な権限がなければサービスプリンシパルconfig.amazonaws.com
からの書き込みを行う、という挙動となります。
AWS サービスが IAM ロールを引き受けて S3 へのアクセスを行う場合、そのプリンシパルはサービスプリンシパルでなく IAM ロール(もしくはロールを引き受けたセッション)となることに注意が必要です。
また、少し特殊なパターンとして ALB のアクセスログがあります。 ALB のアクセスログは専用の AWS アカウントから出力されるため、プリンシパルとして当該アカウントを許可する形となります。("AWS": "arn:aws:iam::elb-account-id
:root")
ここまで見たような「サービスプリンシパルを使用しない」パターンの場合、各条件キーにおいて以下の考え方となります。
キー | リクエストに含まれているか | 値 |
---|---|---|
aws:PrincipalIsAWSService | はい | FALSE |
aws:PrincipalServiceName | いいえ | なし |
aws:PrincipalServiceNamesList | いいえ | なし |
aws:PrincipalIsAWSService のユースケース
例えば以下のようなケースを想定してください。
- 各種サービスプリンシパルからのアクセスは許可したい
- ユーザーやプログラムからのアクセスは以下に限定したい
- 特定の VPC エンドポイント経由
- 特定の送信元 IP アドレス
上記以外からのアクセスについては拒否したい、という要件です。
今回のアップデートで追加された条件キーを使用する場合、S3 バケットポリシーの Deny ステートメントは以下のような書き方をするだけで済みます。
{ "Effect": "Deny", "Principal": "*", "Action": [ "s3:PutObject", "s3:GetObject", "s3:GetBucketAcl", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::myBucketName", "arn:aws:s3:::myBucketName/*" ], "Condition": { "NotIpAddress": { "aws:SourceIp": "xx.xx.xx.xx/32" }, "Bool": { "aws:PrincipalIsAWSService": "false" }, "StringNotEquals": { "aws:SourceVpce": "vpce-xxxxxxxx" } } },
- (アクションはひとまず最低限のものをピックアップしたので、必要に応じて要修正です。)
- (各サービスプリンシパルからの書き込みを許可する Allow ステートメントは別途定義する必要があります。)
評価ロジック
いつもこんがらがるのでおさらいをしておくと、Condition
内の各要素は AND 条件で評価されます。
各要素が全て true になった時、 Deny となります。つまり、以下のうちいずれかでも false になれば Deny の対象外です。
- IP アドレスが指定したものでない(指定した IP であれば false)
- プリンシパルが AWS サービスでない( AWS サービスの場合 false )
- 送信元 VPC エンドポイントが指定したものでない(指定した VPCE であれば false)
Not
や Bool の false が含まれていると混乱しがちですね。
aws:PrincipalIsAWSService を使用しないで実現できる?
「aws:PrincipalIsAWSService
が登場する前は、今回の要件を満たすためにこんな複雑な書き方をする必要がありました」、
と書くつもりだったのですが、そもそも実現できなかったのではという思いに駆られてきました。
当初は以下のようにNotPrincipal
でサービスプリンシパルを羅列するパターンを考えていました。「ワイルドカードが使用できないので、一つずつ定義するのが大変……。」などとのたまうつもりだったのです。
{ "Effect": "Deny", "NotPrincipal": { "Service": [ "cloudtrail.amazonaws.com", "config.amazonaws.com" ] }, "Action": [ "s3:PutObject", "s3:GetObject", "s3:GetBucketAcl", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::myBucketName", "arn:aws:s3:::myBucketName/*" ], "Condition": { "NotIpAddress": { "aws:SourceIp": "xx.xx.xx.xx/32" }, "StringNotEquals": { "aws:SourceVpce": "vpce-xxxxxxxx" } } }
ところがこのステートメントを含むポリシーを実際に試してみたところ、正常に機能しませんでした。ポリシーとしての設定自体はできましたが、許可されていない IP アドレスからでもアクション可能な状態となっていました。
NotPrincipal
でサービスプリンシパルが対応していないのかな?と考えましたが、ドキュメントには以下の記載があります。
Use the NotPrincipal element to specify the IAM user, federated user, IAM role, AWS account, AWS service, or other principal that is not allowed or denied access to a resource.
Deny や Condition と組み合わせた時にうまく行かないのかと考えましたが、原因の特定に至らず……。そもそも NotPrincipal
が必要となるシナリオはとても少ない、とドキュメントに記述があったので、諦めることにしました。
次に、以下ページの内容のようにStringNotLike
でサービスプリンシパルを指定するアプローチを考えます。
{ "Sid": "", "Effect": "Deny", "Principal": "*", "Action": [ "s3:ListBucket", "s3:DeleteObject", "s3:GetObject", "s3:PutObject" ], "Resource": [ "arn:aws:s3:::awsexamplebucket1/*", "arn:aws:s3:::awsexamplebucket1" ], "Condition": { "StringNotLike": { "aws:userid": [ "AROAID2GEXAMPLEROLEID:*", "444455556666" ] } } }
aws:userid
に代わりサービスプリンシパルを表すグローバル条件キーがあれば実現できそうです。ということで以下を確認しますが、それらしきものは見当たりません。
aws:ViaAWSService
というキーを見つけましたが、aws:PrincipalIsAWSService
の代わりに使用しても意図通りの挙動を示しませんでした。( CloudTrail からのログ書き込みも拒否された。)
{ "Effect": "Deny", "Principal": "*", "Action": [ "s3:PutObject", "s3:GetObject", "s3:GetBucketAcl", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::chibayuki-from-vpce", "arn:aws:s3:::chibayuki-from-vpce/*" ], "Condition": { "NotIpAddress": { "aws:SourceIp": "202.xx.xx.xx/32" }, "Bool": { "aws:ViaAWSService": "false" }, "StringNotEquals": { "aws:SourceVpce": "vpce-xxxxxxxx" } } },
というわけで、
「aws:PrincipalIsAWSService
を使用することでシンプルに実現できるようになった」
ではなく、
「aws:PrincipalIsAWSService
を使用することで今までできなかった制御の仕方ができるようになった」
が正しいかも知れません。
「いやいや使わなくてもできたはできたよ」という方がいらっしゃったら教えてください。
aws:PrincipalServiceName と aws:PrincipalServiceNamesList
ここまで見てきたaws:PrincipalIsAWSService
は「サービスプリンシパルかどうか」のみを判定する Bool 条件演算子でしたが、以下のキーは具体的なサービスプリンシパル名までを判定に含めることができる文字列条件演算子です。
aws:PrincipalServiceName
aws:PrincipalServiceNamesList
プリンシパルが AWS サービスでない場合、リクエストコンテキストの中にこれらのキーが含まれていないことに注意してください。
aws:PrincipalServiceName
以下にサンプルのバケットポリシーが記載されています。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "expected-network+service-principal", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-logs-bucket/AWSLogs/AccountNumber/*", "Condition": { "StringNotEqualsIfExists": { "aws:SourceVpc": "vpc-111bbb22", "aws:PrincipalServiceName": "cloudtrail.amazonaws.com" } } } ] }
ここでのStringNotEqualsIfExists
内の要素は AND 条件のため、aws:SourceVpc
とaws:PrincipalServiceName
の両方が true になった場合に Deny されます。
特定の VPC もしくは cloudtrail.amazonaws.com 以外からのアクセスの場合、PutObjectが拒否されるという挙動です。
IfExists
がStringNotEquals
に付与されているため、リクエストコンテキスト内に合致するキーが含まれていない場合には true 扱いになります。
今回のポリシーにおいてはIfExists
が付与されていてもいなくても挙動としては変わりませんが、aws:PrincipalServiceName
がすべてのリクエストに含まれているわけではない、というのはこういった部分に影響してきますので、頭の片隅に置いておいてください。
aws:PrincipalServiceNamesList
単一のサービスプリンシパル名を指定するのでなく複数指定したい場合はこちらのキーを使用する……と言いたいところですが、そうではありません。
例えば先ほどのaws:PrincipalServiceName
を使用して、以下のように複数のサービスプリンシパル名を書けるからです。
{ "Version": "2012-10-17", "Statement": [ { "Sid": "expected-network+service-principal", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-logs-bucket/AWSLogs/AccountNumber/*", "Condition": { "StringNotEqualsIfExists": { "aws:SourceVpc": "vpc-111bbb22", "aws:PrincipalServiceName": [ "cloudtrail.amazonaws.com", "delivery.logs.amazonaws.com" ] } } } ] }
(ここでのcloudtrail.amazonaws.com
とdelivery.logs.amazonaws.com
は OR 条件で評価されます。)
ではaws:PrincipalServiceNamesList
はどういう時に使うのかと言うと、ドキュメントでは以下の記載があります。(以下は機械翻訳結果。)
このキーは、サービスに属するすべてのサービスプリンシパル名のリストを提供します。これは高度な条件キーです。このキーを使って、サービスが特定の地域からしかリソースにアクセスできないように制限することができます。サービスによっては、特定のリージョン内でのサービスの特定のインスタンスを示すために、リージョナルサービスプリンシパルを作成することがあります。リソースへのアクセスを、サービスの特定のインスタンスに制限することができます。サービス・プリンシパルがリソースに直接リクエストすると、aws:PrincipalServiceNamesListには、サービスの地域インスタンスに関連するすべてのサービス・プリンシパル名の順不同のリストが含まれます。
例えばcloudtrail.amazonaws.com
ではなくcloudtrail.ap-northeast-1.amazonaws.com
のようなリージョナルなサービスプリンシパルをリスト化しておいて、特定のリージョンからのアクセスだけ許可する……、というような意味合いなのかと想像しましたが、正直そこから先に理解が及びませんでした。
ForAllValues
または ForAnyValue
set 演算子が鍵になりそうですが、深掘りは持ち越したいと思います。使い方を知っている方がいたらこっそり教えてください。
やってみた
aws:PrincipalIsAWSService
を使用するパターンを試してみます。
- S3 バケット:chibayuki-from-vpce
- 許可 IP アドレス:202.xx.xx.xx
バケットポリシーの設定
まずは以下のバケットポリシーを設定します。
Deny ステートメントのみを持つ状態です。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": "*", "Action": [ "s3:PutObject", "s3:GetObject", "s3:GetBucketAcl", "s3:ListBucket" ], "Resource": [ "arn:aws:s3:::chibayuki-from-vpce", "arn:aws:s3:::chibayuki-from-vpce/*" ], "Condition": { "NotIpAddress": { "aws:SourceIp": "202.xx.xx.xx/32" }, "Bool": { "aws:PrincipalIsAWSService": "false" }, "StringNotEquals": { "aws:SourceVpce": "vpce-xxxxxxxx" } } } ] }
アクセス元制限の動作確認
許可 IP アドレスからの確認
まずは許可されている IP アドレスからのアクセスです。
バケットの詳細画面に遷移すると、s3:ListBucket
によりバケットの内訳を確認できます。
許可されていない IP アドレスからの確認
許可されていない IP アドレスに切り替えます。
バケットの詳細画面に遷移すると、 Deny されたことが確認できます。
念のため直接オブジェクトの詳細画面に遷移しても同様です。
https://s3.console.aws.amazon.com/s3/object/chibayuki-from-vpce?region=ap-northeast-1&prefix=Hello.txt
VPC エンドポイントからのアクセス
EC2 インスタンスに接続し、 S3 からオブジェクトを Get します。
sh-4.2$ uname -a Linux ip-192-168-0-165.ap-northeast-1.compute.internal 4.14.231-173.360.amzn2.x86_64 #1 SMP Mon Apr 19 23:20:22 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux sh-4.2$ aws s3 cp s3://chibayuki-from-vpce/Hello.txt /tmp download: s3://chibayuki-from-vpce/Hello.txt to ../../tmp/Hello.txt
問題なく Get できました。
きちんと VPC エンドポイントを経由しているか?の確認のため VPC エンドポイントポリシーを以下のように修正します。
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Deny", "Principal": "*", "Action": "s3:GetObject", "Resource": "*" } ] }
同じ操作を試みるとエラーが発生しました。
sh-4.2$ aws s3 cp s3://chibayuki-from-vpce/Hello.txt /tmp fatal error: An error occurred (403) when calling the HeadObject operation: Forbidden
サービスプリンシパルからの書き込みの有効化
送信元 IP アドレス制限、 VPC エンドポイント制限がきちんと機能していることが確認できました。
以下のサービスプリンシパルからの書き込みが正常に行えるか確認します。
- CloudTrail(
cloudtrail.amazonaws.com
) - VPC フローログ(
delivery.logs.amazonaws.com
)
CloudTrail
CloudTrail 証跡の出力先としてchibayuki-from-vpce
を指定し、新規作成を行います。
ステップを最後まで進めると、自動的に以下のステートメントがバケットポリシーに追加されます。
{ "Sid": "AWSCloudTrailAclCheck20150319", "Effect": "Allow", "Principal": { "Service": "cloudtrail.amazonaws.com" }, "Action": "s3:GetBucketAcl", "Resource": "arn:aws:s3:::chibayuki-from-vpce" }, { "Sid": "AWSCloudTrailWrite20150319", "Effect": "Allow", "Principal": { "Service": "cloudtrail.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::chibayuki-from-vpce/test-trail/AWSLogs/058066485239/*", "Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } } }
そしてオブジェクトを確認すると、きちんとログが出力されています。
VPC フローログ
同じく出力先としてchibayuki-from-vpce
を指定し、フローログを新規作成します。
設定が完了すると、以下のステートメントがバケットポリシーに追加されます。
{ "Sid": "AWSLogDeliveryWrite", "Effect": "Allow", "Principal": { "Service": "delivery.logs.amazonaws.com" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::chibayuki-from-vpce/test-flow-log/AWSLogs/012345678910/*", "Condition": { "StringEquals": { "s3:x-amz-acl": "bucket-owner-full-control" } } }, { "Sid": "AWSLogDeliveryAclCheck", "Effect": "Allow", "Principal": { "Service": "delivery.logs.amazonaws.com" }, "Action": "s3:GetBucketAcl", "Resource": "arn:aws:s3:::chibayuki-from-vpce" }
そして正常にログが出力されています。
Deny ステートメントのアクションにs3:PutObject
が含まれていますが、サービスプリンシパルはその対象から除外されていることが確認できました。
終わりに
サービスプリンシパルに関する新たなグローバル条件キーを確認しました。 S3 バケットポリシーに使用することで、サービスプリンシパルに対するアクセス制御を柔軟に実現できます。
ともすれば S3 バケットポリシーは複雑になりがちですが、今回の条件キーを使用することでシンプル化することに役立ちそうです。
「 IP 制限はしたいが Trail からの出力は残したいからs3:PutObject
をNotAction
に加える」……としていた過去の自分が聞いたら喜びそうです。
もうちょっと揉める余地があるので色々試してみたいと思います。
以上、千葉(幸)がお送りしました。