認証用アカウントから開発用アカウントにアクセスキーを利用してアクセスする際にMFA 必須にする方式 ② 設定編

前回記事にて前置きをしましたが、ここでは具体的な設定手順を記載していきます。

以下のようなアカウント構成で、認証アカウントにサインインしたIAM ユーザが開発アカウントへAssumeRole してアクセスします。その認証アカウントへのサインイン、AssumeRole の両方でMFA を必須とします。
また、開発アカウントには、MFA 認証されていない場合は一切操作できないような”開発用SCP” をOrganizations にて設定としておきます。これによりアクセスキー流出が起きてもMFA 認証ない限り一切操作ができません。

手順概要

前提として、AWS Organizations と必要なAWS アカウントの作成は設定済みとします。

管理者の手順

  1. MFA 認証されていない場合は操作できない開発用SCP をOrganizations に設定して開発OU などに設定する。
  2. 認証アカウントに、開発者が利用するIAM ユーザを作成
  3. IAM ユーザにMFA 認証されていないと操作できないポリシーを設定する。ただし個人のパスワードや仮想MFA デバイスの設定を各自が実施できるように制限を緩和する
  4. 開発アカウントに、開発者が利用するIAM ロールを作成

開発者の手順

  1. 認証アカウントにログイン
  2. 初回ログイン時にパスワードの設定および、MFA デバイスの設定を実施(今回は仮想MFA デバイスを想定)
  3. アクセスキーを作成
  4. 開発環境にアクセスキーを設定
  5. sts get-session-token を利用してMFA 認証済みの一時的認証情報を入手
  6. 取得した一時的認証情報を利用して、開発アカウントのIAM ロールをassume-role して開発アカウントの一時的認証情報を入手
  7. 開発アカウントの一時的認証情報を利用して、aws cli もしくはプログラムからアクセス
    ※ 5-6 は、スクリプト化して自動化しておく

管理者の手順

まずOrganizations のSCP の設定をしますので、マスターアカウントにサインインして作業します。なお手順の詳細は記載しませんのでご了承ください。

開発OU に割り当てる開発SCP を設定します。以下のようなポリシーを設定すればよいです。MFA 認証されていなかったらすべての操作をDeny するという内容です。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllDenyWithoutMFA",
            "Effect": "Deny",
            "Action": [
                "*"
            ],
            "Resource": "*",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "false"
                }
            }
        }
    ]
}

次は、認証アカウントの操作です。認証アカウントにサインインして、開発者用のIAM ユーザを作成します。なお、開発者用IAM ユーザは、必ず開発者一人一人に作成してください。決してひとつのIAM ユーザを複数人で使い回す(=認証情報を共有するということ)はしないでください。

IAM ユーザを作成したら、以下のようなポリシーを割り当てます。MFA 認証されていない場合は、自分のパスワードを変更する、仮想MFA デバイスを設定するといった操作以外を拒否します。なお、MFA デバイスの削除はできないようにしています。MFA デバイスが削除できていますと、アクセスキーが仮に流出するとMFA デバイスを削除して新しいMFA デバイスを設定できてしまうからです。そうなるとほぼ意味がなくなります。よって、一度設定したMFA デバイスを変更したい場合は、管理者経由で行うということになります。
MFA 認証されたら、開発者アカウントのIAM ロールをassumeRole できます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowSelfManagementOfMFA",
            "Effect": "Allow",
            "Action": [
                "iam:CreateVirtualMFADevice",
                "iam:EnableMFADevice",
                "iam:ListMFADevices",
                "iam:ListVirtualMFADevices",
                "iam:ResyncMFADevice"
            ],
            "Resource": [
                "arn:aws:iam::*:mfa/${aws:username}",
                "arn:aws:iam::*:user/${aws:username}"
            ]
        },
        {
            "Sid": "AllowManageOwnPasswords",
            "Effect": "Allow",
            "Action": [
                "iam:ChangePassword",
                "iam:GetUser"
            ],
            "Resource": "arn:aws:iam::*:user/${aws:username}"
        },
        {
            "Sid": "AllowManageOwnAccessKeys",
            "Effect": "Allow",
            "Action": [
                "iam:CreateAccessKey",
                "iam:DeleteAccessKey",
                "iam:ListAccessKeys",
                "iam:UpdateAccessKey"
            ],
            "Resource": "arn:aws:iam::*:user/${aws:username}"
        },
        {
            "Sid": "DenyNoMFAWithoutSelfManagementOfMFA",
            "Effect": "Deny",
            "NotAction": [
                "iam:CreateVirtualMFADevice",
                "iam:EnableMFADevice",
                "iam:GetUser",
                "iam:ListMFADevices",
                "iam:ListVirtualMFADevices",
                "iam:ResyncMFADevice",
                "iam:DeactivateMFADevice",
                "iam:ChangePassword",
                "iam:CreateAccessKey",
                "iam:DeleteAccessKey",
                "iam:ListAccessKeys",
                "iam:UpdateAccessKey"
            ],
            "Resource": "*",
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "false"
                }
            }
        },
        {
            "Sid": "AllowAssumeRoleWithMFA",
            "Effect": "Allow",
            "Action": [
                "sts:assumeRole"
            ],
            "Resource": [
                "arn:aws:iam::*:role/<change_to_your_role_name>"
            ],
            "Condition": {
                "BoolIfExists": {
                    "aws:MultiFactorAuthPresent": "true"
                }
            }
        }
    ]
}

次は開発アカウントにIAM ロールを作成します。IAM ロールの信頼ポリシーには以下のような設定をします。Condition 区に MFA 認証受けていない場合はassumeRole できない設定とします。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<auth_account_id>:user/<iam_username>"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "Bool": {
          "aws:MultiFactorAuthPresent": "true"
        }
      }
    }
  ]
}

基本的に開発者は自分専用のAWS アカウントは自由に操作できて良いため、IAM ロールのアクセス権限は”AdministratorAccess” ポリシーなどを設定しておいて良いかと思います。(操作させたくないものはSCP にて制御するという考え方です。)

以上が、管理者による設定です。

開発者の手順

認証アカウントのマネージメントコンソールにログインして、パスワードの変更と仮想MFA デバイスの設定をし、アクセキーを作成します。

  1. マネージメントコンソールで、Auth アカウトに開発者IAM ユーザでサインイン (初回サインイン時にパスワード変更するようにIAM ユーザを作成していた場合は自分でパスワード変更)
  2. マネジメントコンソールの右上IAM ユーザ名のメニューにて、”マイセキュリティ資格情報” をクリック
  3. “MFA デバイスの割り当て” ボタンをクリック
  4. 仮想 MFA デバイスを設定する
  5. MFA デバイスのarn をメモしておく
    (arn:aws:iam::<auth_account_id>:mfa/<iam_username> など)
  6. アクセスキーを作成しメモしておきます

次は、開発端末での作業です。

まずは、aws configure で先ほど作成したアクセスキーを設定します(プロファイルはDefault でも何でも良い)

次に、以下のようなスクリプトを作成します。

#!/bin/bash -eu
# Auth アカウントのIAM ユーザ アクセスキーにより、get-session-token を実行して、MFA 認証済みの一時的認証情報を入手する
credentials=$(aws sts get-session-token \
	--serial-number arn:aws:iam::<auth_account_id>:mfa/<iam_username> \
	--token-code $1  \
	--query "Credentials.[AccessKeyId, SecretAccessKey,SessionToken]" \
	--output text)

# 一旦環境変数にMFA 認証済み一時的認証情報をセット
export AWS_ACCESS_KEY_ID=$(echo $credentials | cut -d ' ' -f 1)
export AWS_SECRET_ACCESS_KEY=$(echo $credentials | cut -d ' ' -f 2)
export AWS_SESSION_TOKEN=$(echo $credentials | cut -d ' ' -f 3)

# 開発アカウントのIAM ロールをassume-role する
credentials=$(aws sts assume-role --role-arn arn:aws:iam::<dev-accountid>:role/<role_name> \
    --role-session-name <session_name(任意)> \
	--query "Credentials.[AccessKeyId, SecretAccessKey,SessionToken]" \
	--output text)

# 取得した開発アカウントの一時的認証情報をaws cli のdev プロファイルにセットする
access_key_id=$(echo $credentials | cut -d ' ' -f 1)
secret_access_key=$(echo $credentials | cut -d ' ' -f 2)
session_token=$(echo $credentials | cut -d ' ' -f 3)

aws configure set profile.dev.aws_access_key_id "$access_key_id"
aws configure set profile.dev.aws_secret_access_key "$secret_access_key"
aws configure set profile.dev.aws_session_token "$session_token"

# 環境変数削除
unset AWS_ACCESS_KEY_ID
unset AWS_SECRET_ACCESS_KEY
unset AWS_SESSION_TOKEN

上記のスクリプトを “dev-assume-role.sh” のような名前で作成した場合は、以下のように仮想MFA デバイスのトークンを引数として実行します。

$ dev-assume-role.sh <your_mfa_code>

上記がうまくいくと、aws cli の”dev” プロファイルにMFA 認証済みの一時的認証情報がセットされます。例えば、以下のようにaws cli を実行して開発アカウントのAPI を実行できます。

$ aws ec2 describe-instances --profile dev

エラーがなく、情報が表示されていればうまく行っています。

assume-role を呼ぶ際にMFA トークンを渡す方法ではだめなのか?

今回の方式では、一旦 get-session-token により、MFA デバイス及びMFAをトークンを指定して、MFA 認証済みの一時的認証情報を取得した上で、開発アカウントのIAM ロールをassume-role しています。

しかし、assume-role にもMFA デバイスをMFA トークンを指定する実行の仕方があります。例えば以下のような実行方法です。

aws sts assume-role --role-arn arn:aws:iam::<dev_account_id>:role/<role_name> \
	--serial-number arn:aws:iam::<auth_account_id>:mfa/test01 \
    --role-session-name <session_name> \
	--token-code <token_code>

この方法でも信頼ポリシー "aws:MultiFactorAuthPresent": "true" を条件として指定したIAM ロールをassume できます。

しかし、この場合に取得した一時的認証情報は、MFA 認証情報が含まれていません。要はMFA 任用されていない一時的認証情報という扱いになります。

よって、開発アカウントに設定しているSCP にて、MFA 認証されていないリクエストはすべてDeny するという条件をクリアしないということになります。なので、GetSessionToken -> AssumeRole という順番で一時的認証情報を取得しているということになります。

以下参考情報です。

GetSessionToken によって返される一時的な認証情報には MFA 情報が含まれているので、認証情報によって実行される各 API オペレーションで MFA を確認できます。

AssumeRole によって返される一時的な認証情報のコンテキストには MFA 情報が含まれていないので、MFA に対する個別の API オペレーションを確認できません。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_mfa_configure-api-require.html

追記: この内容に関して、より詳細に説明する記事を作成しました。

以上で、具体的なMFA 必須の設定方法例を説明しました。MFA 認証必須とすることでアクセスキーが仮に流出してしまったとしても、不正アクセスをブロックすることができます。

安全な環境でどんどんと実験し、アジャイルな開発を進めていきましょう!

以上です。