aws:PrincipalIsAWSService を使用せずにサービスプリンシパルからのアクセス制御を試してみた(ができなかった)

サービスプリンシパルが AssumedRole であると僕が急に言い出したら、君はどんな顔をするだろう

コンバンハ、千葉(幸)です。

先日のアップデートにより、AWS グローバル条件コンテキストキーaws:PrincipalIsAWSServiceが追加されました。

これは「サービスプリンシパルから AWS リソースへのアクセス」に必要なパーミッションの管理をシンプルにしてくれるものであり、例えば以下を同時に満たす S3 バケットポリシーを簡単に定義できます。

  • ユーザーからのアクセスは VPC エンドポイント経由に限定したい
  • CloudTrail からのアクセスを許可したい

Manage-date-access-permissions-IAM-easier-1

(画像引用元:IAM makes it easier for you to manage permissions for AWS services accessing your resources | AWS Security Blog

ここで気になるのは、aws:PrincipalIsAWSServiceにより実現できるようになったのが以下のいずれかということです。

  • 今までできなかった管理の仕方ができるようになった
  • 今までは複雑なポリシーを書く必要があったがシンプルに書けるようになった

アップデートを取り上げた以下エントリでもそこに触れており、その時点では「aws:PrincipalIsAWSServiceを使わずに同様の制御を実現する」ことはできませんでした。

過去がどうであろうと今後はaws:PrincipalIsAWSServiceを使えばいいわけですから、そこにこだわる必要はありません。が、こまかいことが気になると夜しかねむれなくなる自分としては、ネチネチとそこを考え続けていました。

そんな中aws:PrincipalTypeという 条件キーを見つけました。リクエストを行うプリンシパルのタイプを表すものです。何かそれっぽいことできそう……!と思い、これを使ってのアクセス制御を試してみました。

先にまとめ

  • 条件キーaws:PrincipalTypeが取りうる値は以下のいずれか
    • Account
    • User
    • FederatedUser
    • AssumedRole
    • Anonymous
  • 状況証拠的に、サービスプリンシパルからのリクエストに含まれるaws:PrincipalTypeキーの値は AssumedRole
  • 「サービスプリンシパルからのアクセスだけ Allow / Deny する」をaws:PrincipalTypeを使用して実現することはできない

平たく言うと、今回やりたいことは実現できませんでした。でもちょっと面白いなと思う箇所があったので、暇な方は続きを読んでください。

やりたいこと

aws:PrincipalIsAWSServiceのドキュメントに記載されている以下のポリシーを、aws:PrincipalTypeを使用したものに置き換えたいです。

{
  "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"
        },
        "BoolIfExists": {
          "aws:PrincipalIsAWSService": "false"
        }
      }
    }
  ]
}

このポリシーは S3 バケットポリシー(の一部)を表しており、冒頭にのせた例と同じく以下を満たすためのものです。

  • ユーザーからのアクセスは特定の VPC の VPC エンドポイント経由に限定したい
  • CloudTrail からのアクセスを許可したい

前提知識

今回のエントリを読んでいただく上で必要となる知識をおさらいしておきます。

プリンシパル

プリンシパルは AWS リソースへのアクションを行う主体を表します。ドキュメントでは以下のように記載されています。

プリンシパルは、AWS リソースのアクションまたはオペレーションに対してリクエストできるユーザーまたはアプリケーションを指します。

IAM ポリシーにおけるPrincipal要素の中では、以下のいずれかを指定できます。

  • AWS アカウントと ルートユーザー
  • IAM ユーザー
  • フェデレーティッドユーザー(ウェブ ID または SAML フェデレーションを使用)
  • IAM ロール
  • ロールを引き受けるセッション
  • AWS サービス
  • 匿名ユーザー(非推奨)

プリンシパルとなる AWS サービスはサービスプリンシパルと呼ばれます。

リクエストコンテキスト

プリンシパルからのリクエストには、以下のような様々な情報が含まれます。(ドキュメントより引用)

  • アクションまたはオペレーション – プリンシパルが実行するアクションまたはオペレーション。AWS マネジメントコンソール ではアクション、AWS CLI や AWS API ではオペレーションです。
  • リソース – アクションまたはオペレーションを実行する対象の AWS リソースオブジェクト。
  • プリンシパル – エンティティ (ユーザーまたはロール) を使用してリクエストを送信するユーザーまたはアプリケーション。プリンシパルに関する情報には、プリンシパルがサインインに使用したエンティティに関連付けられたポリシーが含まれます。
  • 環境データ – IP アドレス、ユーザーエージェント、SSL 有効化ステータス、または時刻に関する情報。
  • リソースデータ – リクエストされているリソースに関連するデータ。これには、DynamoDB テーブル名、Amazon EC2 インスタンスのタグなどの情報が含まれる場合があります。

これらのリクエストの内容はリクエストコンテキストに収集され、リクエストの評価と認可に使用されます。

Condition 要素の条件キー

IAM ポリシーにConditon要素を含めることができます。「リクエストコンテキストに収集されたキーと値の組み合わせ」と「Condition要素内の条件キーと値の組み合わせ」を突き合わせ、リクエストに対する評価/認可が行われます。

条件キーには以下の2種類があります。

  • グローバル条件キー
    • aws:CurrentTime
    • aws:referer
    • aws:PrincipalIsAWSService など
  • サービス固有の条件キー
    • s3:TlsVersion
    • ec2:InstanceType
    • rds:DatabaseEngine など

前者のグローバル条件キーは特定のサービスに依存しないもので、プレフィックスがaws:です。以下に一覧があります。

後者のサービス固有の条件キーは該当するサービスへのリクエスト時にのみ含まれるもので、プレフィックスは各サービス名となっています。以下ページから各サービスの詳細ページに遷移することで確認できます。

グローバル条件コンテキストキーの可用性

サービスに依存しないグローバル条件コンテキストキーであっても、すべてのリクエストに含まれているわけではありません。

どういった場合に含まれているかは、以下ページの可用性(Availability)部から確認できます。

globalconditionkey

今回登場するグローバル条件キーの可用性については以下の通りです。

条件キー 可用性(概要)
aws:SourceVpc VPCエンドポイント経由のリクエストにのみ含まれる
aws:PrincipalIsAWSService すべての署名付きAPIリクエストに含まれる
aws:PrincipalType すべてのリクエストコンテキストに含まれる

評価ロジック

「今回やりたいこと」で引用したポリシーをもとに、評価ロジックを確認します。

トピックをかいつまんで書くと以下の通りです。

  • Conditon要素全体が ture の場合にEffect要素の Allow / Deny の効果がトリガーされる
  • Conditionブロック内の各要素は AND 条件で評価される
    • つまりすべての要素が true の場合のみ全体として true
  • IfExists 条件演算子 が付与された場合、リクエストコンテキストにキーが含まれていない場合は true の評価となる

conditonlogic

簡単にパターンを想定すると以下の結果となります。

リクエストのパターン ①部 ②部 全体の結果
指定されたVPCエンドポイント経由の場合 false true false
サービスプリンシパルの場合 true false false
インターネット経由のIAMユーザーの場合 true true true

Conditon要素全体として false であれば Deny 効果は発揮されない、ということになります。

Notを含む文字列条件演算子や「値が false の Bool条件演算子」が含まれているとこんがらがりますね。

aws:PrincipalType を使用したバケットポリシーの作成

前提をおさらいしたところで、早速本題に入っていきます。

バケットchibayuki-from-vpceを使用し、以下のバケットポリシーを設定しました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Expected-network+service-principal",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::chibayuki-from-vpce/*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalType": [
                        "Account",
                        "User",
                        "FederatedUser",
                        "AssumedRole",
                        "Anonymous"
                    ]
                },
                "StringNotEqualsIfExists": {
                    "aws:SourceVpc": "vpc-111bbb22"
                }
            }
        }
    ]
}

ここでは Deny のステートメントのみが含まれていますが、サービスプリンシパルからの Allow を定義するステートメントは別途必要です。(マネジメントコンソールからの操作の場合、自動的に追加されます。)

aws:PrincipalType キーが取りうる値

aws:PrincipalType はすべてのリクエストに含まれています。どういった値を取りうるかは以下ページに記載があります。

抜粋して記載すると以下の通りです。

プリンシパル aws:PrincipalTypeの値
AWS account root user Account
IAM user User
Federated user FederatedUser
Web federated user AssumedRole
SAML federated user AssumedRole
Assumed role AssumedRole
Role assigned to an Amazon EC2 instance AssumedRole
Anonymous caller Anonymous

ここにはサービスプリンシパルについての記載がないため、「上記の値以外だったらxxする」という条件づけで制御できるのでは、と考えました。(結果的には違っていたのですが、、)

期待する評価ロジック

以下のイメージです。

conditonlogic2

aws:PrincipalTypeキーの値として、複数の値を指定しています。これらの値は OR 条件で評価されます。(一つでも合致すれば true 。)

先ほど確認した「取りうる値」をすべて羅列しているため、①は基本的に true 評価になることを想定しています。唯一これらの値のいずれも取らないであろう、サービスプリンシパルからのリクエストのみ false になることを期待しています。

やってみた

今回は以下の構成で試してみます。

Principaltype_try

  • VPC 外からの IAM ユーザーによるアクセス
  • 指定した VPC 内の VPC エンドポイント経由でのアクセス
  • VPC エンドポイントを経由しない EC2 が引き受ける IAM ロールによるアクセス
  • サービスプリンシパルによるアクセス
    • CloudTrail(cloudtrail.amazonaws.com
    • VPC フローログ(delivery.logs.amazonaws.com

S3 バケットには先述の Deny ステートメントを含むバケットポリシーを設定し、サービスプリンシパルからのアクセスを有効化する際に Allow ステートメントを自動で追加してもらいます。

上記で太字になっている箇所のアクセスのみ許可されるという挙動を期待しています。

VPC 外からの IAM ユーザーによるアクセス

AdministratorAccess を持つユーザーで手元の端末から aws s3 cp による PutObject を実行しました。

% aws s3 cp test.txt s3://chibayuki-from-vpce
upload failed: ./test.txt to s3://chibayuki-from-vpce/test.txt An error occurred (AccessDenied) when calling the PutObject operation: Access Denied

期待通り拒否されました。

指定した VPC 内の VPC エンドポイント経由でのアクセス

バケットポリシーで指定した VPC 上のインスタンスから実行します。インスタンスを配置したサブネットのルートテーブルでは、S3 向けのゲートウェイ型 VPC エンドポイントターゲットとするエントリを設定済みです。 aws s3 cp による PutObject を試みました。

[root@ip-192-168-0-165 ~]# touch /tmp/dummy.txt
[root@ip-192-168-0-165 ~]# aws s3 cp /tmp/dummy.txt s3://chibayuki-from-vpce
upload: ../tmp/dummy.txt to s3://chibayuki-from-vpce/dummy.txt

期待通り正常に実行できました。

VPC エンドポイントを経由しない EC2 が引き受ける IAM ロールによるアクセス

先ほどと同じインスタンスおよび IAM ロールを用いながら、ルートテーブルから VPC エンドポイント向けのエントリを削除した状態で試みました。ここではインターネットゲートウェイを経由してアクセスすることになります。

[root@ip-192-168-0-165 ~]# aws s3 cp /tmp/dummy.txt s3://chibayuki-from-vpce
upload failed: ../tmp/dummy.txt to s3://chibayuki-from-vpce/dummy.txt An error occurred (AccessDenied) when calling the PutObject operation: Access Denied

期待通りアクセスが拒否されました。

サービスプリンシパルによるアクセス

CloudTrail 証跡を新規作成し、出力先として S3 バケット chibayuki-from-vpce を指定します。作成時にエラーが表示され、完了しません。

CloudTrail_Management_Console_NG-0649500

証跡の作成や設定変更をマネジメントコンソールから行う際、必要なアクセス許可が与えられているかをあわせてチェックしてくれているようです。

試しに一度バケットポリシーを空にしてから証跡の設定を行い、その後 Deny ステートメントを含むバケットポリシー(必要な Allow ステートメントは含まれている)を設定し直すと、数分後に以下のような状態となります。

CloudTrail_Management_Console

フローログは作成時のアクセス許可チェックは行われないようで作成自体は正常に行えますが、数分後にステータスを確認するとアクセスエラーが表示されていました。

flowlog-access-error

せっかくここまで期待通りだったのに……つまづいてしまいました。

サービスプリンシパルのプリンシパルタイプは AssumedRole

試行錯誤した結果、Deny ステートメントを以下のように修正することでサービスプリンシパルからのアクセスが正常に行われるようになりました。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Expected-network+service-principal",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:PutObject",
            "Resource": "arn:aws:s3:::chibayuki-from-vpce/*",
            "Condition": {
                "StringEquals": {
                    "aws:PrincipalType": [
                        "Account",
                        "User",
                        "FederatedUser",
                        "Anonymous"
                    ]
                },
                "StringNotEqualsIfExists": {
                    "aws:SourceVpc": "vpc-111bbb22"
                }
            }
        }
    ]
}

差分としては、aws:PrincipalTypeキーの値から AssumedRole を除いたのみです。

もちろんこの状態だと、サービスプリンシパル以外の AssumedRole タイプのプリンシパル(今回だとインターネットゲートウェイ経由での EC2 からのアクセス)も Deny の対象外となるため、当初のやりたいことは実現できていません。

aws:PrincipalTypeキーを使用してのサービスプリンシパルのアクセス制御は実現不可能である、ということが分かってしまいました。

いいんだ、ちょっと面白かったから

当初のやりたいことは実現できませんでしたが、サービスプリンシパルもロールを引き受けた上でリクエストを実行している(であろう)、ということが分かりました。

ではどのロールを使用しているのか?というのが気になるところです。

例えば以下のページでは各サービスにおける「サービスにリンクされたロール」の有無を確認でき、CloudTrail では「あり」となっています。

TrailServiceLinkedRole

詳細ページ を確認するとAWSServiceRoleForCloudTrailという名称でサービスにリンクされたロールが作成されるとあります。しかしこれは Organizations の証跡機能を使用する際に用いられるものであり、今回試行した時点では私の環境に存在しないものです。

他にもそれらしき IAM ロールは見当たらなかったため、少なくとも自身の AWS アカウントに存在するわけではなさそうです。

となるとここから先は妄想の世界となるのですが、どこか AWS マネージドな特殊な AWS アカウントがあり、そこに専用の IAM ロールがあるのではないか、などと考えました。

servideprincipal_delusion

実態としては IAM ロールを引き受けたセッションであるけれども、カスタマー側からは引っくるめてサービスプリンシパルとして見える、というイメージです。

このあたりは情報が見つけられなかったので完全に妄想の世界の話ですが、「もしかしたらこの世のどこかにスペシャルな IAM ロールがあるかも知れない」と考えるだけで私は幸せです。

終わりに

aws:PrincipalIsAWSServiceの代わりにaws:PrincipalTypeを使用してサービスプリンシパルからのアクセスの制御を試みる、という話でした。

結果的に実現できなかったのですが、サービスプリンシパルのプリンシパルタイプが AssumedRole であるという、ちょっと面白い発見ができました。

改めてトータルで考えると「そもそもどちらでもいいことを調べて、そのうえ失敗している」という生産性が無いことこの上ない行為をしていますが、有限で貴重な時間を溶かすことに快感を感じるタイプの人間なので、私は元気です。

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