AWS IAM Identity Center の棚卸しで権限クリープを防ぎたい
AWS IAM Identity Center の棚卸しで権限クリープを防ぎたい
初寄稿の @wa6sn です。8/3-4 に開催される SRE NEXT 2024 が楽しみですね。筆者の所属する 株式会社ギフティ も、GOLD スポンサーとしてブースを出しています。ノベルティも配っているので、ぜひお立ち寄りください。
さて本題ですが、今回は AWS IAM Identity Center で付与したアクセス権限の棚卸しについて述べます。SRE をやっていると、こうした AWS アカウントに対するセキュリティ対策に関わる機会も多いのではないでしょうか、ということで書いてみました。なお、筆者の環境では Control Tower を利用して全アカウントで CloudTrail を有効化しつつログを一元保管しているという前提があります。
権限クリープ
マルチアカウント運用が広まっている昨今では、一人のユーザが複数個の AWS アカウントに対してアクセス権限を持っていることは珍しくありません。ある一つの AWS アカウントに対しても、オペレーションミスの防止といった目的で「強いアクセス権限」「弱いアクセス権限」をそれぞれ付与して使い分けたいこともよくあります。そういったユースケースで、アクセス権限を簡素に一元管理できる AWS IAM Identity Center は便利ですよね。
AWS IAM Identity Center に限らず権限管理一般に言えることですが、システムを健全に運用するには、アクセス権限を剥奪する方針も定めておく必要があります。初期のインフラ構築・メンバーの異動・アドホックな対応など、さまざまなケースでアクセス権限を付与することはあっても、剥奪することはおろそかになりがちです。こういった「不要な権限を保持し続けてしまう」事象は、セキュリティ文脈では 「権限クリープ」 と呼ばれています。セキュリティにおける最小権限の原則に従い、使っていない権限の棚卸しを行いたいです。
やりたいこと
AWS IAM Identity Center で、使っていないアクセス権限の洗い出しをしたいです。AWS IAM Identity Center においては「AWS アカウント, アクセス権限セット, ユーザ(グループ)」の組み合わせが一つの単位となります。単純にこの組み合わせを列挙する方法はいくつかありますが、さらに踏み込んで「列挙された組み合わせについて、それぞれいつ利用されたのか」を、CloudTrail ログと Athena で調べます。
実現方法
1.「AWS アカウント, アクセス権限セット, ユーザ(グループ)」の組み合わせを列挙する
まず、「AWS アカウント, アクセス権限セット, ユーザ(グループ)」の組み合わせを列挙します。この内容はマネジメントコンソール上でも確認できますが、コマンド一撃で列挙できる https://github.com/benkehoe/aws-sso-util という OSS がより便利です。この OSS の使い方を解説している AWS SSO を活用しているなら aws-sso-util を使おう という記事も参考になります。
筆者は aws-sso-util
を GitHub Actions で定期的に実行し、整形して markdown table として GitHub 上に出力しています。アクセス権限の付与・剥奪の仕組みも terraform で実現しているので、何かと都合がよいです。
# こんなスクリプトを GitHub Actions で定期実行している
echo "|Account ID|Account Name|User ID|Permission Sets|" > ./assignments.md
echo "|---|---|---|---|" >> assignments.md
aws-sso-util admin assignments --no-header --separator '|' | awk -F"|" '{print "|" $8 "|" $9 "|" $4 "|" $6 "|"}' | sort >> assignments.md
...<snip>
# 出力結果の例
$ cat assignments.md
|Account ID|Account Name|User ID|Permission Sets|
|---|---|---|---|
|012345678901|product.a|[email protected]|AWSAdministratorAccess|
|012345678901|product.a|[email protected]|AWSReadOnlyAccess|
|123456789012|sandbox.wa6sn|[email protected]|AWSAdministratorAccess|
...<snip>
2. 「その組み合わせ」が、最後に利用されたのがいつかを判断する
先ほど列挙したアクセス権限の組み合わせを補完する目的で、「その組み合わせが最後にいつ利用されたか」を把握したいです。しかし、おなじみの IAM メニューの「最後のアクティビティ」からは、各ロールに対して「“xx さんは” いつ使ったのか」までは知ることが出来ません。
これを知るために、CloudTrail ログを Athena で検索します。筆者の環境では Control Tower を導入しているので、各 AWS アカウントの CloudTrail ログが一元的に保管されています。保管先の S3 バケットへ Athena でクエリすることで、容易に横断検索をすることが出来ます。
CloudTrail ログを Athena で検索する際の具体的なテーブルは以下です。
主に 【Amazon Athena】パーティションを使ってマルチアカウントのCloudTrailログ検索を高速化する - サーバーワークスエンジニアブログ を参考にしました。<Control Tower の Log Archive アカウントで作成された S3 バケット>
と <Organization ID>
は適宜変更します。
CREATE EXTERNAL TABLE IF NOT EXISTS `cloudtrail_logs`.`partitioned_table` (
`eventversion` string,
`useridentity` STRUCT <
type: STRING,
principalid: STRING,
arn: STRING,
accountid: STRING,
invokedby: STRING,
accesskeyid: STRING,
userName: STRING,
sessioncontext: STRUCT <
attributes: STRUCT <
mfaauthenticated: STRING,
creationdate: STRING
>,
sessionissuer: STRUCT <
type: STRING,
principalId: STRING,
arn: STRING,
accountId: STRING,
userName: STRING
>,
ec2RoleDelivery: string,
webIdFederationData: map <
string,
string >
>
>,
`eventtime` string,
`eventsource` string,
`eventname` string,
`awsregion` string,
`sourceipaddress` string,
`useragent` string,
`errorcode` string,
`errormessage` string,
`requestparameters` string,
`responseelements` string,
`additionaleventdata` string,
`requestid` string,
`eventid` string,
`resources` array <
STRUCT <
arn: STRING,
accountid: STRING,
type: STRING
>
>,
`eventtype` string,
`apiversion` string,
`readonly` string,
`recipientaccountid` string,
`serviceeventdetails` string,
`sharedeventid` string,
`vpcendpointid` string,
`tlsDetails` struct <
tlsVersion: string,
cipherSuite: string,
clientProvidedHostHeader: string
>
)
PARTITIONED BY (region string, date string, accountid string)
ROW FORMAT SERDE 'org.apache.hive.hcatalog.data.JsonSerDe'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://<Control Tower の Log Archive アカウントで作成された S3 バケット>/AWSLogs/<Organization ID>/'
TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.date.type' = 'date',
'projection.date.range' = '2023/01/01,NOW',
'projection.date.format' = 'yyyy/MM/dd',
'projection.date.interval' = '1',
'projection.date.interval.unit' = 'DAYS',
'projection.region.type' = 'enum',
'projection.region.values'='us-east-1,us-east-2,us-west-1,us-west-2,af-south-1,ap-east-1,ap-south-1,ap-northeast-2,ap-southeast-1,ap-southeast-2,ap-northeast-1,ca-central-1,eu-central-1,eu-west-1,eu-west-2,eu-south-1,eu-west-3,eu-north-1,me-south-1,sa-east-1',
'projection.accountid.type' = 'injected',
'storage.location.template' = 's3://<Control Tower の Log Archive アカウントで作成された S3 バケット>/<Organization ID>/AWSLogs/<Organization ID>/${accountid}/CloudTrail/${region}/${date}',
'classification'='cloudtrail',
'compressionType'='gzip',
'typeOfData'='file',
'classification'='cloudtrail'
);
作成したテーブルに対して、以下のようなクエリを実行します。実際にはスクリプトから実行しているので、{account_ids_str}
には対象の AWS アカウント ID の array が、{start_date_str}
, {end_date_str}
には 2024/03/25
のような文字列が入ります。
イベントの絞り込み条件は “userIdentity.type
が AssumedRole
である” かつ “ロール名に AWSReservedSSO_
を含む” としています。当初は ConsoleLogin
や Federated
イベントでの検出を検討しましたが、一時的なクレデンシャルを払い出しての操作など、マネジメントコンソール経由以外の操作が拾えないケースを確認しました。結局、AWSReservedSSO_
ロールで何らかのアクティビティが存在したら、そのアクセス権限セットを使っているとみなしていいだろう というシンプルな考え方に倒しました。このあたりの仕様はユーザガイドの CloudTrail userIdentity 要素 - AWS CloudTrail も参考にしています。複数のイベントが検出されるので、そのうち最新のものに絞り込んでいます。
WITH FilteredEvents AS (
SELECT
eventTime,
useridentity.accountid as AWSAccountId,
regexp_extract(userIdentity.arn, '([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{{2,4}})', 1) as UserName,
regexp_extract(userIdentity.arn, '(AWSReservedSSO_[a-zA-Z0-9_]+)', 1) as RoleName,
userIdentity.arn AS ARN
FROM
partitioned_table
WHERE
accountId IN ({account_ids_str})
AND date between '{start_date_str}' and '{end_date_str}'
AND userIdentity.arn LIKE '%AWSReservedSSO_%'
AND userIdentity.type = 'AssumedRole'
),
RankedEvents AS (
SELECT
eventTime,
AWSAccountId,
UserName,
RoleName,
ROW_NUMBER() OVER (PARTITION BY ARN ORDER BY from_iso8601_timestamp(eventTime) DESC) AS row_num
FROM
FilteredEvents
)
SELECT
eventTime,
AWSAccountId,
UserName,
RoleName
FROM
RankedEvents
WHERE
row_num = 1
ORDER BY AWSAccountId, UserName;
# Athena によるクエリの結果の例。
$ head -3 athena_query_results.csv
eventTime,AWSAccountId,UserName,RoleName
2024-03-21T09:13:52Z,012345678901,[email protected],AWSReservedSSO_AWSAdministratorAccess_0123456789abcdef
2024-04-15T13:03:53Z,123456789012,[email protected],AWSReservedSSO_AWSAdministratorAccess_23456789abcdef01
...<snip>
3. それぞれの結果をマージする
これまでの結果をマージしてロール名等を整形し、spreadsheet へ csv を import するなど、いい感じにします。
例えばこの結果からは、[email protected]
は 012345678901
アカウントの AWSReadOnlyAccess
を使っていないことが分かるので、剥奪するか、「毎回 AdministratorAccess で操作するのは誤操作が怖いので、ReadOnlyAccess と使い分ける手もありますよ」といったコミュニケーションを行います。単に利用者に「このアクセス権限、使っていますか?」と聞くのではなく、最後にいつ使ったかを提示してあげることで、実態に即した回答がしやすくなりました。
target_id,target_name,principal_name,permission_set_name,last_logined
012345678901,product.a,[email protected],AWSAdministratorAccess,2024-03-21T09:13:52Z
012345678901,product.a,[email protected],AWSReadOnlyAccess,
123456789012,sandbox.wa6sn,[email protected],AWSAdministratorAccess,2024-04-15T13:03:53Z
...<snip>
まとめ
ある程度の期間、運用したシステムで発生しがちな「権限クリープ」を防ぐための、実態に即したアクセス権限の棚卸し方法について紹介しました。アクセス権限が使われているかどうかを人に聞いて回るのも大変ですし、コミュニケーションの摩擦の少ない状態で手軽にやりたいですよね。
技術的には、やはり Control Tower のような Organization-wide な仕組みは、組織全体へレバレッジを効かせることが出来て便利だなあと思いました。どんどん使っていきたいですね。それでは、参考になれば幸いです。