AssumeRoleを駆使して強大な権限を持つIAMユーザやアクセスキーを撲滅しよう

今回はIAMネタです。
突然ですが以下のJSONが何を表しているかわかるでしょうか？
「当たり前だろ！なめてるのか」っていう人と、「わかってないけどコピペで何とかしてます」っていう人の二極化するんじゃないかなと思ってます。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

上記のドキュメントは信頼ポリシーとかAssumeRolePolicyとか呼ばれるもので、雑に言ってしまうと`誰がこのロールの権限を使うことができるか`を示したものです。
例のJSONの場合、このロールはLambdaが使えるんだなと分かるわけです(逆に言うとLambda以外でこのロールは使えない)。
別に新しいネタでも何でもないですが、今回はこれについてまとめてみようと思います。


## AssumeRoleとは
ロールの引き受けと呼ばれているactionのことです。帽子を被るという表現されることも多いですね。
AssumeRoleすると、Principalは対象のロールに紐づけられた権限のセッション情報を利用してアクションを実行できるようになります。

詳しい説明はAssumeRoleでググると素晴らしい記事がたくさん出てきますのでそちらに譲りますが、今までAssumeRoleについてよく知らなかったよって方は`AssumeRoleすることで、必要な時だけ対象ロールの権限を持つことができる`とざっくり理解して差し支えないでしょう。

## 何が嬉しいの？
さて、ここまでの説明だと「結局コピペしてServiceのとこ書き換えてればそれで十分なんでしょ」と思うかもしれませんが、それだけじゃありません。
信頼ポリシーの`誰が`にあたるPrincipalには様々なエンティティを指定することができます。[公式ドキュメント](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html)に記載されている通り、AWSアカウントやIAMユーザなんかも指定できるんです。つまり、IAMユーザにアタッチする権限は最小限にしつつ、必要な時だけサクッと権限を切り替えながら操作することが可能となります。IAMユーザにアタッチされた権限が変わるわけではないので、仮にアクセスキーが流出してしまったとしても被害を最小限に抑えることができます。アカウントを跨いだAssumeRoleも可能なので、商用環境のAWSアカウントにはユーザを作成せず、ロールの切り替えのみでアクセスする、なんて運用も可能です。
今回は記事が大きくなりすぎるので書ききれませんが、外部のIDプロバイダーの認証情報を利用してAssumeRoleすることも可能です。(GCPとAWSを連携させたい時なんかに便利ですね！)

AWSアカウントやIAMユーザを指定する場合、信頼ポリシーは以下のような感じです。

#### AWSアカウントを指定する
指定したアカウントのユーザ全員がAssumeRoleすることが可能になります(複数指定可)。IAMロールが属するのと同じアカウントIDでも違うアカウントIDでも同じように指定することが可能です。

```json
"Principal": { "AWS": "arn:aws:iam::123456789012:root" }
```
or

```json
"Principal": { "AWS": "123456789012" }
```

#### IAMユーザを指定する
指定したIAMユーザだけがAssumeRoleすることができます(複数指定可)。ドキュメントにも記載されていますが、`全てのユーザ`を指定するために`ワイルドカード(*)`を使用することはできません。このため、複数のIAMユーザを指定することは限定的な用途以外ではあまりないでしょう。

```json
"Principal": {
  "AWS": [
    "arn:aws:iam::AWS-account-ID:user/user-name", 
  ]
}
```

## 使い方

具体的にAssumeRoleする方法について見ていきましょう。
まず自分の使うIAMユーザにAssumeRoleの権限がついていることを確認してください。対象リソースはひとまず`*`でOKです。
AssumeRoleする対象のIAMロールは、信頼関係の項目を上記のような信頼ポリシーにして作成したものを利用してください。

#### AWSマネジメントコンソールでAssumeRoleする
1. マネジメントコンソールの右上のドロップダウンメニューから`ロールの切り替え`をクリック。
1. 以下の画面になる。
![スクリーンショット 2022-12-19 19.04.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/360735/8258dd7f-ccd1-6478-7145-bce56760a390.png)
1. 切り替える元となるアカウントのアカウントID、切り替え先のロール名、表示名を入力してロールの切り替えをクリック。
1. 切り替わる。

簡単ですね。
入力がめんどくさい方はクエリストリングに値を埋め込んでブックマークしましょう。↓のようなURLはIAMロールの詳細ページから`コンソールでロールを切り替えるためのリンク`として記載されています。
`https://signin.aws.amazon.com/switchrole?roleName=your_role_name&account=your_account`

私はChromeの拡張機能を利用して切り替えの管理をしていますが、それもおすすめです(詳細は割愛)。

#### 2. aws cliでAssumeRoleする
2つ方法を紹介します。
どちらのやり方もaws cliのdefaultプロファイルにAssumeRole元の権限の設定は終わっているものとして進めます。
1つ目はaws cliの`assume-role`コマンドを使う方法です。
リクエストに成功すると、一時的なCredentialが返ってきます。

```shell
$ aws sts assume-role --role-arn arn:aws:iam::123456789012:role/example-role --role-session-name role_session_name
{
    "Credentials": {
        "AccessKeyId": "xxxxxxxxx",
        "SecretAccessKey": "xxxxxxxxxxxxxx",
        "SessionToken": "xxxxxxxxxxxxxxxx",
        "Expiration": "2022-12-19T11:19:20+00:00"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "xxxxxxxxxxxxx",
        "Arn": "arn:aws:iam::123456789012:role/example-role/role_session_name"
    }
}
```
返却値をそれぞれ環境変数に代入すると、ロールの権限でリクエストが実行できます。
```shell
$ export AWS_ACCESS_KEY_ID={ReturnedAccessKeyID}
$ export AWS_SECRET_ACCESS_KEY={ReturnedSecretKey}
$ export AWS_SESSION_TOKEN={ReturnedSessionToken}
$ aws sts get-caller-identity
{
    "UserId": "XXXXXXXXX",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/example-role/role_session_name"
}
```

少しめんどくさいやり方ですが、CI/CDのパイプラインに組み込んでも利用可能なため覚えといて損はないです。最小権限を持つアクセスキーだけをCI/CDサービスのシークレットに保存しておいて必要な時だけ強い権限に切り替えることができるようになります。このやり方が必要な場合はシェルスクリプトなどで自動化しておけば良いでしょう。

2つ目はaws cliのprofileとして設定する方法です。
以下のように、~/.aws/configにprofileを追加します。
通常、profileを作成する場合は`~/.aws/credentials`にアクセスキーを記載するやり方が一般的だと思いますが、AssumeRoleする場合は必要な情報をconfigに記載しておくだけでOKです。

```:~/.aws/config
[profile example-role-profile]
region = ap-northeast-1
output = json
role_arn = arn:aws:iam::123456789012:role/example-role
source_profile = default
role_session_name = role_session_name
```

あとは、通常のprofileと同様にexample-role-profileを指定するだけです。
コマンド実行時に`--profile`で指定するか、以下のように環境変数で指定する。
```
export AWS_PROFILE=example-role-profile
```

確認してみましょう。
```
$ aws sts get-caller-identity (--profile example-role-profile)
{
    "UserId": "XXXXXXXXX",
    "Account": "123456789012",
    "Arn": "arn:aws:sts::123456789012:assumed-role/example-role/role_session_name"
}
```
私はローカルのPCからcliを叩く必要がある場合は、こちらの方が便利なのでよく使っています。
profileの切り替えも多少めんどくさいので、私は環境変数を切り替える自作コマンドを使っています(こちらも詳細については割愛)。

## ここまでのまとめ
AssumeRoleの基本的な使い方は以上です。簡単にまとめます。
- AssumeRoleするとロールになる(意訳)ことができる
- AssumeRoleできる人はAWSサービス意外だけじゃない(AWSアカウント、IAMユーザ、外部IDPのユーザ、etc...)
- IAMユーザでAssumeRoleしたいなら、AssumeRoleアクションが許可されている必要があるよ
- マネジメントコンソールでもcliでも手軽にAssumeRoleできるよ

### 注意点
例えば、素の状態でIAMの閲覧権限がついているユーザの認証情報が漏れたとしたら、IAMロールの情報を辿ってAssumeRoleし、実質的に強い権限の認証情報が漏れたのと同じくらいの被害を被ることになります。
少なくとも、AssumeRole実行元の権限からAssumeRole先の情報を突き止めることができないようにしましょう。

## 業務で運用していくための諸々

割愛した部分は結構ありますが、上記までで基本はなんとか書ききれました。
いくつかユースケースごとに対処法を記載していきます。

#### AssumeRoleできるIPアドレスを制限したい
はい、できます。
信頼ポリシーのCondition句を使えば可能です。
アクセス元のIPアドレスがほぼ固定されている場合は、これだけでもかなりセキュリティが向上します。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "IpAddress": {
                    "aws:SourceIp": [
                        "x.x.x.x/32",
                        "y.y.y.y/32",
                    ]
                }
            }
        }
    ]
}
```

#### AssumeRoleしてても、ユーザを識別したい
監査などでかなり重要ですね。RoleSessionNameにIAMユーザ名と同一の文字列を指定しないとAssumeRoleできなくすることで、AssumeRole後でも誰が何をしたか判別可能です。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "StringLike": {
                    "sts:RoleSessionName": "${aws:username}"
                }
            }
        }
    ]
}
```

#### AssumeRoleする時もMFA必須にしたい
可能です。信頼ポリシーとcliのconfig両方を変更する必要があります。

```json:信頼ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "Bool": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        }
    ]
}
```

```diff:~/.aws/config
[profile example-role-profile]
region = ap-northeast-1
output = json
role_arn = arn:aws:iam::123456789012:role/example-role
source_profile = default
role_session_name = role_session_name
+ mfa_serial = arn:aws:iam::479677554411:mfa/role_session_name
```

mfa_serialはコンソールのMFAの設定から確認することができます。

#### Principalにグループを指定したい
これもよくありそうなケースかなと思います。
しかし、IAMのユーザグループをPrincipalに指定することはできません([公式ドキュメント](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html))。
以下のようにPrincipalにユーザを複数指定すれば複数ユーザの指定も可能ですが、愚直に全員分記載していく必要があります。

```json
"Principal": {
  "AWS": [
    "arn:aws:iam::AWS-account-ID:user/user-name1", 
    "arn:aws:iam::AWS-account-ID:user/user-name2", 
    "arn:aws:iam::AWS-account-ID:user/user-name3", 
  ]
}
```

このやり方だとユーザの追加・削除がある度にポリシーを書き換える必要があるので、あまりいいやり方とは思いませんが状況によってはこのやり方がシンプルで手軽な場合もあるでしょう。

別の手段として、`Condition配下のPrincipalArn`で正規表現を使う方法があります。
その中でも2パターンあるのですが、今回は2つ目のpathを用いた方法を紹介します。おすすめ。
1. IAMユーザ名にprefixをつける
1. IAMユーザのpathをつける

IAMはpathを使うことでリソースに階層構造を作ることができます。
pathと正規表現を利用して以下のようにPrincipalArnを指定することで、`/path`配下に属するIAMユーザのみAssumeRoleできるようになります。ちなみに、何も意識せずにIAMユーザを作成すると`/`配下に作成されます。

```json:信頼ポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:root"
            },
            "Action": "sts:AssumeRole",
            "Condition": {
                "StringLike": {
                    "aws:PrincipalArn": "arn:aws:iam::123456789012:user/path/*"
                }
            }
        }
    ]
}
```

## 個人的プラクティス

- IAMユーザの権限は最小に
  - 極端な話、対象のIAMロールへのAssumeRole許可のみで良い
  - 素の権限でAssumeRole先の情報は見れないようにしておく
- 必要な権限を持ったIAMロールを作る
  - 信頼ポリシーでAssumeRoleできる人を制御する(IPアドレス、MFA、ユーザのarn、ロールセッション名)
- AssumeRoleする手順は簡易化しておこう(コンソールなら切り替えURLをブックマークしておく、ブラウザの拡張機能を使う、CLIならprofileを作成しておく、スクリプトで関数化しておく、など)

IAMはまだまだ奥が深いので紹介しきれませんが、ひとまず以上のことを意識しながら管理すると強い権限を持ったアクセスキーを撲滅し、安全にAWSへのアクセスができるようになるんじゃないでしょうか。

以上です。
改善点とか、割愛した部分の詳細知りたいとかあればコメントお願いします。