認証用アカウントから開発用アカウントにアクセスキーを利用してアクセスする際に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. 開発アカウントのIAM ロールをMFA 情報を指定してassume-role し開発アカウントの一時的認証情報を入手
  6. 開発アカウントの一時的認証情報を利用して、aws cli もしくはプログラムからアクセス

管理者の手順

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

開発OU に割り当てる開発SCP を設定します。以下のようなポリシーを設定すればよいです。AssumeRole されたリクエスト以外はMFA 認証されていなかったらすべての操作をDeny するという内容です。この設定をしておくことで、開発OU配下のAWS アカウントのアクセスキーが想定外の事故で漏れてしまったとしてもMFA デバイスが無い限り不正アクセスはされません。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyWithoutMFAExcludeAssumedRole",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalType": "AssumedRole"
        },
        "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
# MFA トークンコードを指定して開発アカウントのIAM ロールをassume-role する
credentials=$(aws sts assume-role --role-arn arn:aws:iam::<dev-accountid>:role/<role_name> \
	--serial-number arn:aws:iam::<auth_account_id>:mfa/<iam_username> \
	--token-code $1  \    
        --role-session-name <session_name(※)> \
        --duration-seconds <900 ~ 43200> \
	--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"

上記のスクリプトを “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

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

※) –role-session-name に指定するセッション名は例えばassume-role を呼び出したIAM ユーザ名とすると後でCloudTrail のログを確認するときにどのIAMユーザによる呼び出しかをセッション名で判断できるため便利です。

assume-role を呼ぶ際にMFA トークンコードを渡す場合の注意点

assume-role を呼ぶ際にMFA トークンコードを指定しても、生成される一時的認証情報にはMFA 認証情報が含まれていません。つまり、このAPIリクエストは「”mfaAuthenticated”: “false”」として実行されます。
もし、MFA 認証されていないと処理できないようなポリシーがIAMロールや捜査対象のリソースのポリシーに設定されている場合はAPI呼び出しは拒否されます。

AssumeRole した際の一時的認証情報にMFA認証情報を含めて処理したい場合は、sts:GetSessionToken -> sts;AssumeRole というチェーン呼び出しで一時的認証情報を取得します。この場合、GetSessionToken で返される一時的認証情報に含まれるMFA情報が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

ただし、このような一時的認証情報のチェーン取得では、最終的なassumeRole のセッション有効期限は、最大1時間に制限されますのでご注意ください。

参考) IAM ロールの連鎖セッションで有効期間の制限を引き上げることはできますか?
https://aws.amazon.com/jp/premiumsupport/knowledge-center/iam-role-chaining-limit/

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

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

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

以上です。