[AWS][IAM][Terraform] ロール切替用にユーザを作成し、MFA 認証を有効にする


以下の記事の続き。

マルチアカウントにおけるユーザ管理を考える
https://qiita.com/batatch/items/425661dbffece3b981bf

ロール切替方式の権限設定一式を作成する
https://qiita.com/batatch/items/08aa3f705336a0fea89e


## 概要

実装編の続きで、マスターアカウントへのユーザ登録部分を設定します。
やりたいことは以下になります。

- ユーザ自身に自分でパスワードを変更できるようにする
- ユーザ自身に MFA 仮想デバイスを自分で登録できるようにする

以下の公式ドキュメントを参考にそれぞれのポリシーを定義し、共通グループを作成してユーザをぶら下げる形にしていきます。

AWS: IAM ユーザーがセキュリティ認証情報ページで自分のコンソールのパスワードを管理できるようにする
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage-password-only.html

AWS: MFA で認証された IAM ユーザーがセキュリティ認証情報ページで自分の MFA デバイスを管理できるようにする
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_examples_aws_my-sec-creds-self-manage-mfa-only.html

![fig-user-basegroup.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558772/eeeb8bfb-1fb4-faa9-a699-008d42ccbefb.png)


## ポリシー定義

公式ドキュメントのままですが、以下のようにポリシー定義を用意します。


### 自身のパスワードの変更を許可 (userOwnPasswordPolicy)

```userOwnPasswordPolicy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "ViewAccountPasswordRequirements",
            "Effect": "Allow",
            "Action": "iam:GetAccountPasswordPolicy",
            "Resource": "*"
        },
        {
            "Sid": "ChangeOwnPassword",
            "Effect": "Allow",
            "Action": [
                "iam:GetUser",
                "iam:ChangePassword"
            ],
            "Resource": "arn:aws:iam::*:user/${aws:username}"
        }
    ]
}
```

### 自身の MFA 設定を許可 (userForceMFAPolicy)

```userForceMFAPolicy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowViewAccountInfo",
            "Effect": "Allow",
            "Action": "iam:ListVirtualMFADevices",
            "Resource": "*"
        },
        {
            "Sid": "AllowManageOwnVirtualMFADevice",
            "Effect": "Allow",
            "Action": [
                "iam:CreateVirtualMFADevice",
                "iam:DeleteVirtualMFADevice"
            ],
            "Resource": "arn:aws:iam::*:mfa/${aws:username}"
        },
        {
            "Sid": "AllowManageOwnUserMFA",
            "Effect": "Allow",
            "Action": [
                "iam:DeactivateMFADevice",
                "iam:EnableMFADevice",
                "iam:GetUser",
                "iam:ListMFADevices",
                "iam:ResyncMFADevice"
            ],
            "Resource": "arn:aws:iam::*:user/${aws:username}"
        },
        {
            "Sid": "DenyAllExceptListedIfNoMFA",
            "Effect": "Deny",
            "NotAction": [
                "iam:CreateVirtualMFADevice",
                "iam:EnableMFADevice",
                "iam:GetUser",
                "iam:ListMFADevices",
                "iam:ListVirtualMFADevices",
                "iam:ResyncMFADevice",
                "iam:ChangePassword", ★
                "sts:GetSessionToken"
            ],
            "Resource": "*",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "false"
                }
            }
        }
    ]
}
```

一点、上記の公式ドキュメントの設定だと、ユーザ作成時に初回パスワード変更を有効にすると、パスワード変更ができないという問題でハマりました。
こちらで詳しく説明されています。

MFA 強制ポリシーを適用した IAM ユーザーで初回ログイン時のパスワード変更（リセット）ができない時の対処法
https://dev.classmethod.jp/articles/force-mfa-with-iam-changepassword/

★印の `iam:ChangePassword` が変更点です。
MFA 仮想デバイスの登録がない状態での許可アクションの条件に追加します。


## ログインユーザ用の共通グループ設定

### IAM ポリシーの定義

前回の記事のスクリプトで IAM ポリシー定義を作成します。
今回は、上記のポリシー定義の JSON ファイルを用意して、スクリプトで読み込みます。

**スクリプト**

```hcl
resource "aws_iam_policy" "this" {
  name        = var.name                                         # ポリシー名
  path        = var.path                                         # パス (デフォルトで "/")
  description = var.desc                                         # ポリシー説明 (英数字のみ)
  policy      = file(format("policy_defs/%s.json", var.name))    # ポリシー定義の本文、例は外部から "ポリシー名.json" ファイルを読み込み
}
```


### IAM グループの定義

共通グループを作成し、上記で定義したポリシーを割り当てます。

**入力情報**

```yaml
- name: Base
  policies:
    - userOwnPasswordPolicy
    - userForceMFAPolicy
```

**スクリプト**

```hcl
resource "aws_iam_group" "this" {
  name = format("%sGroup", var.name)                        # グループ名
  path = var.path
}

resource "aws_iam_group_policy_attachment" "this" {
  for_each = toset(var.policies)

  group      = aws_iam_group.this.name
  policy_arn = each.key                                     # グループに割り当てるポリシーの ARN
}
```


## 共通グループ定義の Terraform の構成

共通グループ定義部分の構成です。
前回の master 部分に追加するイメージです。(今回の範囲に抜粋)

```
versions.tf
components/
  account-master/
    main.tf
    outputs.tf
    versions.tf -> ../../versions.tf
  policy_defs/
    userOwnPasswordPolicy.json
    userForceMFAPolicy.json
modules/
  group/
    main.tf
    outputs.tf
    variables.tf
  policy/
    main.tf
    outputs.tf
    variables.tf
```

マスターアカウントで以下のようにコマンドを実行します。

```
$ cd components/account-master/
$ terraform init
$ terraform plan
$ terraform apply
```

**共通グループの設定スクリプトのイメージ**

```components/account-master/main.tf
locals {
  policy_defs = {                                            # 個別に定義するポリシー設定
    "userForceMFAPolicy"    = "Policy for force MFA",
    "userOwnPasswordPolicy" = "Policy for own password"
  }

  group_defs = {                                             # グループ定義とポリシー割当の設定
    "Base" = {
      policies = [
        "userForceMFAPolicy",
        "userOwnPasswordPolicy"
      ],
      system_policies = []
    }
  }
}

data "aws_caller_identity" "current" {}                      # 実行している自身の AWS アカウントの情報を取得

module "master_policy" {                                     # ポリシーを定義
  source = "../../modules/policy"

  for_each = local.policy_defs

  name = each.key
  path = "/"
  desc = each.value
  body = file(format("../policy_defs/%s.json", each.key))    # ポリシー名.json でポリシー定義を用意しておいて読み込みます
}

module "master_group" {                                      # グループを定義し、ポリシーを割当
  source = "../../modules/group"

  for_each = local.group_defs

  name = each.key
  path = "/"
  policies = concat(
    [for k in each.value.policies : format("arn:aws:iam::%s:policy%so-%s", data.aws_caller_identity.current.account_id, local.path, k)],
                                                             # 自身が定義したポリシーを、ARN を作成しながら追加 
    [for k in each.value.system_policies : format("arn:aws:iam::aws:policy/%s", k)]
                                                             # システムで用意されたポリシーを追加する場合
  )
}
```

あとは、先の resource 定義を module へ当てはめていけば完成です。


## ユーザ作成の設定

続いて、ユーザの作成です。

IAM ユーザを作成し、上で作成した共通グループを割り当てるだけですが、Terraform で IAM ユーザを作成する場合は、初期パスワード作成用に PGP 鍵を求められます。

IAM 側で作成したパスワードを受け取るために、以下のような処理をしてるようです。

![fig-user-pgp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/558772/8dcad08f-1743-3f08-46a2-1ea2a56f497f.png)


なので、Terraform スクリプトによる処理の前後に、いくつかの PGP 鍵操作が必要になります。


### PGP 鍵のペアを作成

PGP 鍵操作には GnuPG を使います。
このコマンドが gpg で PGP とややこしいです。。

- **PGP (Pretty Good Privacy)** : 公開/秘密鍵による暗号化/復号の方式、公開仕様ではない (Symantec 保有)
- **GnuPG (GNU Privacy Guard)** : GNU による PGP 互換のオープンソース、OpenPGP を経て作られた

PGP そのものの仕様が非公開だったり、中で使われる暗号化方式が米国から輸出禁止だったりという背景があったようですね。

まずは、GnuPG (以下、gpg) を使って公開/秘密鍵のペアを作成します。

```
$ gpg --gen-key --batch gpg.conf
```

あらかじめ鍵のレシピを設定ファイルで用意しておくと便利だと思います。
デフォルトでは対話処理で指定します。

```gpg.conf
Key-Type: RSA
Key-Length: 4096
Name-Real: aws
Expire-Date: 1y
%commit
%echo done
```

`Name-Real` というのがペアの名前になるようです。(例では aws とします)
以下のように鍵の一覧表示したときに名前で指定することができます。

```
$ gpg --list-keys
/home/ec2-user/.gnupg/pubring.gpg
----------------------------------------
pub   4096R/2C8C6882 2021-02-14 [expires: 2022-02-14]
uid                  aws

$ gpg --list-keys aws
pub   4096R/2C8C6882 2021-02-14 [expires: 2022-02-14]
uid                  aws
```


### PGP 公開鍵を Terraform 受け渡し用に設定

Terraform で IAM ユーザ作成時に PGP 公開鍵を送るには、公開鍵を Base64 エンコードしてから送るようです。

ここでは以下のように Base64 エンコードした公開鍵を `terraform.tfvars` に書き込んで連携します。

```
$ gpg -o /tmp/gpg.pub --export aws                                        # 公開鍵をファイルに取り出し (aws は Real Name)
$ cat /tmp/gpg.pub | base64 | tr -d '\n' > /tmp/gpg.pub.base64            # Base64 エンコード
$ echo "pgp_key = \"$( cat /tmp/gpg.pub.base64 )\"" > terraform.tfvars    # PGP 公開鍵を terraform.tfvars へ出力
```

Terraform スクリプトからは pgp_key 変数として参照します。


### IAM ユーザを定義

IAM ユーザを作成し、ログインプロファイルに PGP 公開鍵を渡します。

そのあと、IAM ユーザを IAM グループへ割り当てます。

**入力情報**

```yaml
- name: user001
  groups:
    - Base
```

**スクリプト**

```hcl
resource "aws_iam_user" "this" {
  name          = var.name                                   # ユーザ名
  path          = var.path
  force_destroy = true
}

resource "aws_iam_user_login_profile" "this" {
  user                    = aws_iam_user.this.name
  password_length         = 8                                # パスワードポリシー(長さ)
  password_reset_required = true                             # 初回ログイン時にパスワード変更を強制
  pgp_key                 = var.pgp_key                      # terraform.tfvars から公開鍵を連携
}

resource "aws_iam_user_group_membership" "this" {
  user   = aws_iam_user.this.name
  groups = var.groups                                        # グループへ割当
}
```

`aws_iam_user_login_profile` の出力結果を見ると、公開鍵で暗号化されたパスワードが載ってきます。

```
  encrypted_password      = "wcF..."
  id                      = "user001"
  key_fingerprint         = "38cb..."
  pgp_key                 = "mQIN..."
```

この `encrypted_password` を outputs.tf で出力対象にしておき、terraform の実行結果を復号することでパスワードを取得します。

```
$ terraform show -json | jq -r '.values.outputs.encrypted_password' > password.enc    # terraform 出力から encrypted_password を取得
$ cat password.enc | base64 -d | gpg -r aws > password                                # Base64 デコードしてから gpg コマンドで復号 (aws は Real Name)
```


## ユーザ登録の Terraform の構成

ユーザ登録部分の構成です。

```
versions.tf
gpg.conf                               # PGP 鍵作成用の設定
components/
  account-user/
    main.tf
    outputs.tf
    versions.tf -> ../../versions.tf
    terraform.tfvars                   # 自動生成
modules/
  user/
    main.tf
    outputs.tf
    variables.tf
```

マスターアカウントで以下のようにコマンドを実行します。
terraform 操作の前後で PGP 公開鍵設定と、暗号化パスワードの復号が必要になります。(gpg コマンド類はイメージです)

```
$ gpg --gen-key --batch gpg.conf
$ gpg -o /tmp/gpg.pub --export aws
$ cat /tmp/gpg.pub | base64 | tr -d '\n' > /tmp/gpg.pub.base64
$ echo "pgp_key = \"$( cat /tmp/gpg.pub.base64 )\"" > components/account-user/terraform.tfvars

$ cd components/account-user/
$ terraform init
$ terraform plan
$ terraform apply

$ terraform show -json | jq -r '.values.outputs.encrypted_password' | base64 -d | gpg -r aws
```

**ユーザ登録のスクリプトのイメージ**

```components/account-user/main.tf
locals {
  policy_defs = {                                            # 個別に定義するポリシー設定
    "user001" = {
      "groups"  = ["o-BaseGroup"]
    },
    "user002" = {
      "groups"  = ["o-BaseGroup"]
    },
  }
}

module "user" {                                              # ユーザ登録とグループへの割当
  source = "../../modules/user"

  for_each = local.users

  name    = each.key
  path    = "/"
  groups  = each.value.groups                                # グループ一覧(ユーザ毎)
  pgp_key = var.pgp_key                                      # terraform.tfvars から連携する PGP 公開鍵
}
```

先の resource 定義を modules/user へ当てはめ、`aws_iam_user_login_profile` の結果の `encrypted_password` を `outputs.tf` で terraform の出力として連携させます。

複数ユーザを扱っているので、ユーザ名とパスワードのペアで一覧を参照できるようにすると便利です。

運用としては、ここでユーザ名と復号したパスワードをセットにして、利用者へ通知する形になります。
利用者は通知を受け取ったら、以下の手続きを経て利用開始となります。

- 通知されたユーザ名とパスワードで、マスターアカウントへログイン
- 初回パスワード変更を求められるので、パスワードを変更
- 次に `マイセキュリティ資格情報` の画面から MFA 仮想デバイスを登録
- 利用開始 (ロール切替を適宜設定)



// EOF