IAMで二段階認証を強制する際の罠

## 追記（2024/09/25）
- 現在はマネジメントコンソールの挙動が改善し、下記チェックボックスを使用する際の本記事に記載の問題は発生しません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/1f33fdd5-2008-97ba-066f-99008f242b00.png)
こちらのチェックボックスをオンにしてポリシーを生成すると、Conditionは以下のようになり、長期的認証情報でのアクセスは許可されません。
```json
            "Condition": {
                "Bool": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
```
- なお、ポリシージェネレータを用いずに直接JSONを記述する場合でも同様、上記のConditionを利用することによりDenyポリシーを用いずに二段階認証を強制することが出来ます。


→ したがって本記事は今後、「昔のマネコンはこういうことが起きたんだな～」と懐かしむ場となります。

## はじめに
本記事では、特定のactionを行う際に二段階認証を強制するIAMポリシーの書き方について私見を述べたものです。

「IAM 二段階認証」でググると、aws:MultiFactorAuthPresentを用いた方法を書いた記事が多く見つかります~~が、
これは「長期的な認証情報を用いてアクセスされる」、つまりAWS CLI等でアクセスキーを用いることで二段階認証を使わずにアクションを実行出来てしまうという罠があります~~。

ハマりがちな部分だと感じたため、お読みいただいた方のお役に立てればと思い
マネコン経由、CLI経由両方で二段階認証を強制する方法について提案する記事を書こうと考えました。

## やりたいこと

IAMユーザーtestuserから、Lambda関数testFunctionを呼び出す。
ただし、二段階認証が行われていない場合はアクセスを拒否する。

## 課題

### 意図せずアクセスできてしまう例

まず、testuserにアタッチするIAMポリシーを作成する。

~~マネコンのポリシーエディタでInvokeFunctionを許可するよう設定する。
また、下図赤枠部「MFAが必須」チェックボックスにチェックを入れる。~~

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/1387f283-4e33-5a2d-5f4f-a475a7e0f826.png)

~~その後、「ポリシーを確認」ボタンをクリックする。
この操作により、~~以下ポリシーがtestuserにアタッチされる。

```json:BasicPolicy4testFunction
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:ap-northeast-1:************:function:testFunction",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        }
    ]
}
```

この状態でAWS CLIからtestFunctionを呼び出すことが出来る。
IAMユーザtestuserのアクセスキーを用いると・・・

```shell
> aws configure --profile testuser
AWS Access Key ID [None]: ********************
AWS Secret Access Key [None]: ****************************************
Default region name [None]: ap-northeast-1
Default output format [None]: json

> aws lambda invoke --function-name testFunction --profile testuser --payload '{}' output.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```

！！？

```output.txt
{"statusCode": 200, "body": "\"Hello from Lambda!\""}
```

`"aws:MultiFactorAuthPresent": "true"`がConditionに記述されているので二段階認証が必須のハズ。
なぜ・・・・！？！？！？！？！？！？！？？！？

### aws:MultiFactorAuthPresentの謎

じゃあ`aws:MultiFactorAuthPresent`っていったい何なんだ？
ドキュメントを見てみる・・・

> このキーを使用して、リクエストを行った一時的なセキュリティ認証情報を検証するために多要素認証 (MFA) を使用したか確認します。

うんうん、二段階認証が使われたか確認するんだよね。

> aws:MultiFactorAuthPresent キーは、API または CLI コマンドがアクセスキーペアなど長期的な認証情報で呼び出された場合には表示されません。

え、長期的な認証情報を使った時は関係ないってこと！！？

なるほど、アクセスキーは「長期的な認証情報」だから`aws:MultiFactorAuthPresent`では二段階認証を行ったかどうかチェック出来ないんだね・・。

> AWS Management Console の IAM ユーザーが、知らないうちに一時的認証情報を使用します。ユーザーは、長期的な認証情報であるユーザー名とパスワードを使用してコンソールにサインインします。ただし、バックグラウンドでは、コンソールがユーザーに代わって一時的な認証情報を生成します。

はえ～～～～
マネコンでアクセスするときって、裏では一時的な認証情報を使っていたんだ！
だからマネコンでアクセスした時は`aws:MultiFactorAuthPresent`で二段階認証を行ったことを確認することが出来た、と。

## 解決策１

### 長期的な認証情報を用いた場合もチェック

~~以下ポリシーをアタッチするか、追記すればよい~~。
以下ポリシーのいずれかで実現可能。

```json:BasicPolicy4testFunction(改)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:ap-northeast-1:************:function:testFunction",
            "Condition": {
                "Bool": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        }
    ]
}
```

- 二段階認証を通過した場合のみ当該Lambda関数の呼び出しが許可される。

```json:ForceMFAPolicy
        {
            "Sid": "DenyUnlessAuthenticatedWithMFA",
            "Effect": "Deny",
            "Action": "*",
            "Resource": "*",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "false"
                }
            }
        }
```

-  Denyステートメントによって、すべての操作は二段階認証必須になる。

後者を意訳すると以下の通りである。

 - アクセスキーは長期的認証情報のため`aws:MultiFactorAuthPresent`キーがない。上記Conditionは`BoolIfExists`なのでキーがない場合は真とする。条件に合致するのでDenyとなる。従ってAWS CLIでcredentialにアクセスキーを書いてアクセスしても拒否される。
 - マネコンは一時的認証情報のため`aws:MultiFactorAuthPresent`が存在する。キーが存在するので`BoolIfExists`の中を評価する。ここで、二段階認証をクリアできていれば`aws:MultiFactorAuthPresent`の値は`true`なので偽となり、条件に合致しない。この場合はアクセス可能である。一方で二段階認証をクリアできていない場合（例えばマネコンにログイン後にポリシーをアタッチした場合）は`aws:MultiFactorAuthPresent`の値は`false`なので真となり、条件に合致するのでDenyとなる。
 - AWS CLIから`aws sts get-session-token`コマンド等で二段階認証を行い、得られたアクセスキーとセッショントークンでアクセスした場合。これは一時的認証情報のため`aws:MultiFactorAuthPresent`が存在する。キーが存在するので`BoolIfExists`の中を評価する。二段階認証を経て得られたキーなので`aws:MultiFactorAuthPresent`の値は`true`となり、偽なので条件に合致しない。したがって本ポリシーが適用されていたとしても二段階認証さえ行っていればAWS CLI経由でアクセス可能。

ドキュメントには以下のように記載されている。

> BoolIfExists 演算子を使用して、リクエストが MFA を使用して認証されたかどうかを確認することをお勧めします。

> Deny、BoolIfExists、false のこの組み合わせは、MFA を使用して認証されないリクエストを拒否します。具体的には、MFA を使用しないで一時的認証情報を使用して行われたリクエストを拒否します。また、AWS CLI などの長期的な認証情報またはアクセスキーを使用して行われる AWS API オペレーションを使用して行われるリクエストも拒否されます。

### AWS CLIからアクセスする方法

##### アクセスキー自体は、二段階認証の概念がないため直接使うことが出来ない

上記ポリシーを適用することでアクセスキーでinvokeFunctionできなくなったことを示すため、以下コマンドを実行する。

```shell
> aws lambda invoke --function-name testFunction --profile testuser --payload '{}' output.txt

An error occurred (AccessDeniedException) when calling the Invoke operation: User: arn:aws:iam::********:user/testuser is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-northeast-1:********:function:testFunction with an explicit deny in an identity-based policy
```

アクセスキーを用いているのでアクセスが拒否される。
では、どうしたらAWS CLI経由でアクセスすることが出来るのだろうか？

##### AWS CLIにおける二段階認証

以下コマンドで二段階認証を行い、「アクセスキー」と「セッショントークン」を取得することが出来る。
（ちなみに、上記ポリシーのDenyの部分で`Action:*`、`Resource:*`であったとしても実行することが出来る。）

以下コマンドを実行する際は「testuser」の部分をAWS CLIのprofileやIAMユーザ名に、[アカウントID]の部分をAWSアカウントのアカウントIDに、[二段階認証コード]の部分をMFAデバイスから得られた認証コードに、それぞれ置き換えること。

```shell
> aws sts get-session-token --profile testuser --serial-number arn:aws:iam::[アカウントID]:mfa/testuser --token-code [二段階認証コード]
{
    "Credentials": {
        "AccessKeyId": "********************",
        "SecretAccessKey": "****************************************",
        "SessionToken": "****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************",
        "Expiration": "202*-**-**T**:**:**+00:00"
    }
}
```

あとはこれをcredentialに追記するだけ。

```text:.aws/config
[profile testuser_mfa]
region = ap-northeast-1
output = json
```

```text:.aws/credentials
[testuser_mfa]
aws_access_key_id=********************
aws_secret_access_key=****************************************
aws_session_token=****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
```

二段階認証で得られたアクセスキー、セッショントークンをcredentialに入れたprofileを用いることで、
上記対応後も無事AWS CLI経由でのアクセスを行うことができた。

```shell
> aws lambda invoke --function-name testFunction --profile testuser_mfa --payload '{}' output.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
```

`BasicPolicy4testFunction(改)`を使用した場合、`Bool`では`aws:MultiFactorAuthPresent`がない場合`false`となるため、「二段階認証を使用しない認証情報」ではConditionが偽になる。今回は二段階認証を通過した認証情報を使用したためアクションが許可されたと考えられる。

`ForceMFAPolicy`を使用した場合は、「二段階認証を認証した一時的認証情報」を使用したため`aws:MultiFactorAuthPresent`が`true`となったためアクションが拒否されなかったと考えられる。

### 問題点

~~IAMのクォータにより、ポリシーの文字数やアタッチできるポリシーの数に上限がある。
すべての場合において、上記Denyポリシーを追加する余裕があるわけではない。
また、一時的認証情報と長期的認証情報の2種類があると混乱するかもしれない。~~
→ `Bool`と`aws:MultiFactorAuthPresent`の組み合わせを用いることで、Allowステートメントでできます。

なお、いずれにせよIAMユーザーはアクセスキーやID/PWが漏洩するリスクが大きいので、IAMロールを用いることがベストプラクティスです。

これを解決するためにも、下記「解決策２（推奨）」または「解決策３（推奨）」で権限をロールに集約し、スイッチロールかAWS SSOで対応するべきである。

## 解決策２（推奨）

### ロールを使おう

そもそも「ロールを使う」とは「assumeRole」で得られたアクセスキーを用いてアクセスするということ。
assumeRoleは裏でstsを呼んで一時的な認証情報を取得することである。
これなら一番最初に書いた`aws:MultiFactorAuthPresent`:`true`を条件とするAllowのステートメントだけで二段階認証の対応ができる。

普段からロールを前提でIAMを設計しておいて、必要な権限はすべてロールに集約するべきである。

## 解決策３（推奨）

### AWS SSOを使う

ログインをAWS SSOに集約すれば解決できる。
わざわざ`aws sts`コマンドを叩いてjsonの一部をコピペ～としなくても、
下図のような画面が用意されており、exportから始まるコマンドをコンソールにポコっとコピペするだけで一時的認証情報を気軽に使うことが出来る。

マネコンを使用する際も見た目上はAWS SSO経由でログインしたように見えるが、実際には裏ではロールに入って操作している。

二段階認証を経たことを確実にすることができ、常に一時的認証情報が使えてIAMユーザもいらなくなるので、よりセキュアである。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653543/4fb8c4f4-d776-1898-622d-516cd29d19bb.png)

## まとめ

「MFAが必須」とはいったい・・・・・

もし、全部ロールを使ってアクセスするという前提だったなら、今回のことで悩むこともなかっただろう。
やっぱベスプラは正義なんだ。
Well-Architected万歳！！

## 引用文献

aws:MultiFactorAuthPresent
 - https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_condition-keys.html?icmpid=docs_iam_console#condition-keys-multifactorauthpresent

## 参考文献

IAM と IAM AWS STSクォータ
 - https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_iam-quotas.html