CEL(Common Expression Language)を使ってIAMポリシーを検索する iam-policy-finder

SREチームの藤原です。

今回は CEL(Common Expression Language) を使って、AWSのIAMポリシーを検索するツールを作ったので紹介します。

github.com

3行でまとめ

  • CEL (Common Expression Language)の式を指定してAWS IAMポリシーを検索するツールをOSSとして作りました。GetAccountAuthorizationDetails APIで取得したIAMポリシーをCELで評価して、マッチするものを出力します
  • 例えば「lambda:GetFunctionがあるがlambda:ListTagsがないポリシーを探す」などができます
  • AWSからたびたびやってくる、IAMポリシーに関するお知らせに対応するのに便利です

突然の「Action Required」

ある日、AWSからこんなメールが届きました。

Lambda GetFunction API の認証に変更に際し、お客様のアクションが必要になる可能性があるため、ご連絡しております。

これまで、ListTags API を明示的に使用する場合にのみ、ListTags への権限が必要でした。しかし、GetFunction API 権限を持つプリンシパルにおいても、GetFunction 呼び出しにより出力されたタグ情報にはアクセスできました。2024 年 7 月 27 日以降、Lambda は GetFunction API を呼び出すプリンシパルに ListTags API に対する明示的な許可権限が設定されたポリシーがある場合にのみタグデータを返すようになります。GetFunction API を呼び出すロールに対して、拒否ポリシーが設定されているか、ListTags API へのアクセスを明示的に許可するポリシーがない場合、Lambda は GetFunction API 呼び出しへの応答でタグデータを返さなくなります。

お客様のアカウントには、GetFunction API へのアクセスを許可するロールがあることを確認しましたが、そのポリシーでは ListTags API へのアクセスが許可されていません。引き続き GetFunction API を使用してタグデータを受信する場合は、GetFunction API を呼び出すために使用される AWS Identity and Access Management (IAM) ロールに、明示的な「ListTags API のアクセス許可」をするポリシーを追加する必要があります

要約すると…

  • (これまで) Lambda GetFunction APIでlambda:ListTagsがなくてもタグが取得できていた
  • (これから)lambda:ListTags権限がないとタグが取得できなくなる
  • このアカウントにはlambda:GetFunctionがあるけどlambda:ListTagsがないポリシーがあるよ
  • 期日までにlambda:ListTagsを許可しないとタグが取れなくなるよ!

というお知らせです。なるほど、事情は分かります。おそらくタグの扱いを厳格にしたいのですね。

しかし、そのようなポリシーがあるのが分かっているならどれが該当するのかまで教えてくれればよさそうなものですが、この種のお知らせにおいてはなぜか教えてくれないことが多いのです。

ある条件を持つIAMポリシーを探す方法

どうやってIAMポリシーを洗い出すか、まず考えるのがマネージメントコンソールでの目視です。しかしこれは大変です。なにしろ数が多すぎる (1358) ←

IAMマネージメントコンソール

AWSの人に聞いたところ、aws iam get-account-authorization-detailsを使えばアカウント内の許可一覧を取得できると教えてもらえました。

しかし、これも結局目視でポリシーを探す必要があります。出力はJSONなのでjq芸でなんとかしたくなりますが、これも意外と難しいのです。IAMのポリシーJSONは配列でも文字列でもよい要素(ActionResource)があり、人間が書くには便利なのですが、機械に読ませるにはちょっと面倒な構造をしているためです。

仕方ないので、ちゃっちゃとAWS SDK Go(v2)でコードを書きました。1から書いても30分ぐらいなので悩んでるより早いです。

lambda:GetFunctionがあるのにlambda:ListTagsがないポリシーを見つけるくん · GitHub

やっていることは簡単で、GetAccountAuthorizationDetails APIでアカウント内の許可一覧を取得して、ポリシーの"JSON文字列"に対してGoのコードで評価するだけです。

"lambda:GetFunction"があるが"lambda:ListTags"がないポリシーを探すための評価式はこんな感じです。

func detect(s string) bool {
  d, _ := url.QueryUnescape(s) // APIの結果はURL escapeされている
  l := strings.ToLower(d)      // Actionは大文字小文字を区別しないので小文字に揃える
  return strings.Contains(l, `"lambda:getfunction"`) && !strings.Contains(l, `"lambda:listtags"`)
}

これを、お知らせが来たアカウントで実行して回れば解決です。

果たして実行してみたところ、複数のアカウントでぽろぽろと見つかったのは AWSSupportServiceRolePolicy でした。これはAWSのマネージドポリシーです。こちらでは編集できないので、どうしろという感じですが……

(このポリシーを何かにattachして使っている場合は問題が起きるので通知自体は仕方ないのですが、なんとかならないものでしょうか)

また別の「Action Required」が

しかし、続けてまた別のお知らせが来ました。

[要対応] IAM ポリシーを更新して明示的な ecs:TagResource の「許可」を追加してください

ECSでもecs:CreateCluster時にecs:TagResourceが必要になると言われています。例によって、どのポリシーが該当しているかは教えてもらえません。特定の条件でポリシーを探したいことはこれからも多そうです。

💡 評価式部分を汎用化したら使い回せるのでは?

ここで、評価式をGoのコードに埋め込むのではなくなんらかの形で外部から与えることができれば、IAMポリシーを検索する汎用的なツールになりそうだと思いつきました。

問題は「式評価」を何で実装するか。自作するのは大変です。汎用言語(Lua, JavaScriptなど)を組み込むこともできますが、今回はGoogleが先日発表したCEL(Common Expression Language)を使ってみることにしました。

cel.dev

CELは非チューリング完全で式評価に特化した、高速、安全に実行可能な式評価言語です。Go, Java, C++に組み込み可能で、GoogleCloudのCertificate Authority Service, Kubernetes, Istio などで既に実用されています。

CEL式の例は以下のような感じです。言語の定義はこちら

// 数値の範囲チェック
age >= 18 && age < 65

// 文字列の前方一致や正規表現マッチ
name.startsWith('Foo')
name.matches('^Foo.*')

// リスト内の要素に条件を満たすものがあるか
roles.exists(role, role == 'admin')

// 日付、時間の比較
request.date < timestamp('2023-12-31T23:59:59Z')
timeout >= duration('10s')

値に型があり(例えばbool, int, uint, double, string, timestamp, duration...)、演算子も一通り揃っています。文字列関数もあります。これらを組み合わせて評価した結果を、任意の値(boolだけではなく)で返せるため、今回の用途にはちょうどよさそうです。

iam-policy-finder

ということで作成した、CELを使ってIAMポリシーを検索するツールがこちらです。

github.com

Usage: iam-policy-finder <expr> [flags]

Arguments:
  <expr>    CEL expression string or file name

Flags:
  -h, --help                     Show context-sensitive help.
      --dump                     dump found policy document
  -f, --filter=FILTER,...        filter policy document(User, Group, Role, LocalManagedPolicy,
                                 AWSManagedPolicy)
      --debug                    debug logging
      --skip-evaluation-error    skip evaluation error
      --[no-]progress            show progress dots
      --lc                       convert action to lower case
  1. GetAccountAuthorizationDetails でポリシーを取得
  2. ポリシーをCELで式評価をして真になるものを出力

やっていることはこれだけです。以下は具体的な使い方の例です。

ポリシーの名前で検索する

単純に、ポリシーの名前の一致で検索する例です。

$ iam-policy-finder 'Name == "AmazonEC2FullAccess"'

time=2024-07-18T17:37:13.110+09:00 level=INFO msg="starting scan" expr="Name == \"AmazonEC2FullAccess\"" filter=[]
time=2024-07-18T17:37:26.272+09:00 level=INFO msg=found policy=AmazonEC2FullAccess versions="[v5 v4 v3 v2 v1]" attached=2
time=2024-07-18T17:37:33.377+09:00 level=INFO msg=finished found=5 scanned=975

ポリシーのJSONを文字列比較する

最初に書いた雑ツールの「ポリシー文字列に"lambda:GetFunction"があり"lambda:ListTags"がない」という条件で検索する例をCELにすると以下のようになります。

Document.matches('"lambda:[Gg]et[Ff]unction"')
&&
!Document.matches('"lambda:[Ll]ist[Tt]ags"')

変数DocumentにはポリシーのJSON文字列が入っています。matchesは、文字列に対して正規表現マッチを行うCELの関数です。

IAM policy actionは大文字小文字を区別しないという仕様があります。CELには大文字小文字を変換したり、同一視して比較する方法がデフォルトでは用意されていないため、正規表現の文字クラスを使っています。

正規化したポリシーを評価する

雑な文字列マッチだけでは少々誤検知が多そうなので、少し頑張ってポリシーを正規化してみました。 JSON文字列をパースして、StatementActionResourceが文字列だった場合は全てリストに正規化します。

例えば以下のポリシーは

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "s3:*",
      "Effect": "Allow",
      "Resource": "*",
      "Sid": "1"
    }
  ]
}

このように正規化されてから処理されます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": ["s3:*"],
      "Effect": "Allow",
      "Resource": ["*"],
      "Sid": "1"
    }
  ]
}

この正規化したStatementを使って、「s3:*を許可している」ポリシーを検索する式はこのようになります。

Statement.exists(s, (s.Action.exists(a, a == "s3:*") && s.Effect == "Allow"))

exists(要素, 式)はCELで使える、リストの要素を式で評価してどれかが真になれば真になる関数です。

Actionを小文字に正規化して比較

CELには大文字小文字を変換したり無視して比較する方法が(デフォルトでは)ないので、Actionを小文字に正規化して比較するオプションを追加しました。

--lc オプションを付けると、正規化時にAction要素を全部小文字に変換します。

「Actionにlambda:GetFunctionがあるがlambda:ListTagsがない」式を--lcと一緒に使うと以下のように書けます。

Statement.exists(s,
  (
    s.Action.exists(a, a == "lambda:getfunction")
    &&
    !s.Action.exists(a, a == "lambda:listtags")
  )
)

元のポリシーJSON文字列Documentは小文字になりません。

ECSの件を早速調べてみた

さて、よいツールができたので早速ECSの件を調べてみましょう!

ecs:CreateClusterがあるがecs:TagResourceがないもの」を--lcオプション付きで探す式は以下のようになります。

// ecs.cel
Statement.exists(s, (
    s.Action.exists(a, a == "ecs:createcluster")
    &&
    !s.Action.exists(a, a == "ecs:tagresouce")
))
$ iam-policy-finder --dump --lc  ecs.cel

CEL式はコマンドライン引数で直接渡すこともできますが、式をファイルに書いてそのファイル名を引数に渡すこともできます。

--dump は見つかったポリシーJSON文字列も出力するオプションです。

$ iam-policy-finder --dump --lc  ecs.cel
time=2024-07-18T18:02:23.881+09:00 level=INFO msg=found
policy=AmazonEC2ContainerServiceforEC2Role versions="[v7 v6 v5 v4 v3 v2 v1]" attached=1
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:CreateCluster",
        "ecs:DeregisterContainerInstance",
        "ecs:DiscoverPollEndpoint",
        "ecs:Poll",
        "ecs:RegisterContainerInstance",
        "ecs:Submit*"
      ],
      "Resource": "*"
    }
  ]
}

見つかったのは AmazonEC2ContainerServiceforEC2Role ……AWSのマネージドポリシーでした!!!

まとめ

  • CEL (Common Expression Language)の式を指定してAWS IAMポリシーを検索するツールをOSSで作りました。GetAccountAuthorizationDetails APIで取得したIAMポリシーをCELで評価して、マッチするものを出力します
  • 例えば「lambda:GetFunctionがあるがlambda:ListTagsがないポリシーを探す」などができます
  • AWSからたびたびやってくる、IAMポリシーに関するお知らせに対応するのに便利です

どうぞご利用ください。

カヤックではOSSが好きなエンジニアを募集しています!