[アップデート] サービスプリンシパルを含む IAM ポリシーの管理を簡素化する AWS グローバル条件キーが追加されました

「サービスプリンシパル」っていう呼び方を初めて知りました。プリンシパルとなる 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 ステートメントは以下のような書き方をするだけで済みます。

S3 バケットポリシーの一部

       {
            "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:SourceVpcaws:PrincipalServiceName両方が true になった場合に Deny されます。

特定の VPC もしくは cloudtrail.amazonaws.com 以外からのアクセスの場合、PutObjectが拒否されるという挙動です。

IfExistsStringNotEqualsに付与されているため、リクエストコンテキスト内に合致するキーが含まれていない場合には 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.comdelivery.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 アドレスからのアクセスです。

AllowIP

バケットの詳細画面に遷移すると、s3:ListBucketによりバケットの内訳を確認できます。

serviceprincipalcheck

許可されていない IP アドレスからの確認

許可されていない IP アドレスに切り替えます。

DenyIP

バケットの詳細画面に遷移すると、 Deny されたことが確認できます。

DenyIPListbucket

念のため直接オブジェクトの詳細画面に遷移しても同様です。

https://s3.console.aws.amazon.com/s3/object/chibayuki-from-vpce?region=ap-northeast-1&prefix=Hello.txt

DenyIPListObject

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 エンドポイントポリシーを以下のように修正します。

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を指定し、新規作成を行います。

trail-enable

ステップを最後まで進めると、自動的に以下のステートメントがバケットポリシーに追加されます。

追加されたステートメント

        {
            "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"
                }
            }
        }

そしてオブジェクトを確認すると、きちんとログが出力されています。

TrailS3putput

VPC フローログ

同じく出力先としてchibayuki-from-vpceを指定し、フローログを新規作成します。

flow-log-s3

設定が完了すると、以下のステートメントがバケットポリシーに追加されます。

追加されたステートメント

        {
            "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"
        }

そして正常にログが出力されています。

FlowLogs-principal

Deny ステートメントのアクションにs3:PutObjectが含まれていますが、サービスプリンシパルはその対象から除外されていることが確認できました。

終わりに

サービスプリンシパルに関する新たなグローバル条件キーを確認しました。 S3 バケットポリシーに使用することで、サービスプリンシパルに対するアクセス制御を柔軟に実現できます。

ともすれば S3 バケットポリシーは複雑になりがちですが、今回の条件キーを使用することでシンプル化することに役立ちそうです。

「 IP 制限はしたいが Trail からの出力は残したいからs3:PutObjectNotActionに加える」……としていた過去の自分が聞いたら喜びそうです。

もうちょっと揉める余地があるので色々試してみたいと思います。

以上、千葉(幸)がお送りしました。

参考