MacからAWSにアクセスする時はAssumeRoleすることにした

  • 45
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

発端

ローカルPCからAWSのAPIを呼び出す場合、アクセスキーをローカルに保存しておく必要がある。
ディスクは暗号化してるしパスワードでロックもされてるけど、大きい権限を持ったユーザのアクセスキーをベタ書きにしておくのは気がひける。
かといって何も出来ないユーザを作っても意味が無い。

ということで、権限の少ないユーザから大きな権限を持つロールを一時的に拝借する形にした。

aws-cliの自動AssumeRoleとの使い分け

aws-cli には 自動的にAssumeRoleを行ってくれる機能 がある。cliだけ使うならこちらを使ったほうがいいと思う。
mfa_serial を指定すればワンタイムトークンも使えるし、クレデンシャルの有効期限が切れたら再入力を促してくれる。

ただ、AWS SDK for Ruby では自動的にAssumeRoleしてくれないようだった。
SDKでもクレデンシャルを共有したい場合は、ここに書いてある方法もありだと思う。

設定例
# In ~/.aws/credentials:
[development]
aws_access_key_id = foo
aws_access_key_id = bar

# In ~/.aws/config
[profile crossaccount]
role_arn = arn:aws:iam:...
source_profile = development

手順

  1. AssumeRole しか出来ないユーザを作成
    このユーザのアクセスキーをローカルPCに保存する。

  2. AssumeRole 対象のロールを作成
    実際に必要な権限をこのロールに含める。

  3. ローカルPCから AssumeRole を実行
    1.で作成したユーザのアクセスキーを使って 2.のロールを AssumeRole する。

  4. AWS の API を呼び出す
    3.の AssumeRole で取得した一時的なクレデンシャルを使って AWS の API を呼び出す。

IAMユーザの作成

このユーザのアクセスキーをローカルPCに保存し、実作業に必要な権限を持ったロールを AssumeRole する。
AssumeRole 時は External ID と MFA のトークンを入力させることでセキュリティ的にイイ感じにする。
(MFA の設定は各ロールで行う。「read onlyなロールだから MFA は不要」「power userロールは MFA 必須」など適宜。)
Management Consle にはログインしないのでパスワードは設定しない。

作成

ここらへんを参考にユーザを作成する。
AWSで最初にすべきこと ~運用ユーザの作成~

アクセスポリシー

  • 指定しない
  • ロール側のトラストポリシーで Principal にこのユーザを指定する(後述)
  • 今回は同一アカウント内で AssumeRole を行う(= root ユーザが同一人物となる)ので、使う側/使われる側のどちらかのポリシーで許可がおりていればよい模様。逆に、クロスアカウントで AssumeRole を行うには、使う側/使われる側それぞれで自アカウントの root ユーザから許可を得る必要がある。

MFA の設定

初めての人はここらへんを参考に MFA の設定をする。

AWSアカウントの2段階認証を設定する
AWSアカウント作ったらこれだけはやっとけ!IAMユーザーとAuthyを使ったMFAで2段階認証

設定後 MFA デバイスの ARN をメモっとく。
arn:aws:iam::XXXXXXXXXXXX:mfa/my-user

IAMロールの作成

AssumeRole 対象のロール power-user を作成する。
ここでは IAM と STS 以外の Action を許可する。

作成

Step 1 : Set Role Name

Step 1 はロール名に power-user に入れるだけ。

Step 2 : Select Role Type

クロスアカウントではないけど他に選択肢がなさそうなのでこれ↓を選択する。

select-role-type.png

Step 3 : Establish Trust

  • Account ID
    3rd partyでなく自分のAWSアカウントIDを入力する。

  • External ID
    適当な文字列を入力する。
    実際に AssumeRole 時にパラメータの1つとして使う。
    これは口外してはいけない類のものだと思う。

  • Require MFA
    今回は AssumeRole 時に MFA を使うのでチェックを入れる。
    MFA 不要であればチェックは外す。

establish-trust.png

Step 4 : Set Permissions

IAM 以外の処理は全部許可するので Power User Access を選択。
Select Pollicy Template > Power User Access

トラストポリシー

ロール作成後のトラストポリシーは下記のようになっているはず。

trust-policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::XXXXXXXXXXXX:root"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "hogehoge"
        },
        "Bool": {
          "aws:MultiFactorAuthPresent": true
        }
      }
    }
  ]
}

デフォルトではアカウント単位でこのロールの利用許可を出している。
なので、最初に作成したユーザ my-user だけが AssumeRole できるよう設定する。
ユーザを追加できますよ感を出すために配列で指定するようにしてみた。

"Principal": {
  "AWS": [
    "arn:aws:iam::XXXXXXXXXXXX:user/my-user"
  ]
}

グループ単位や user/* での指定は出来なかった。
間接的に指定できるようにすると「混乱した使節問題」みたいなことが起きるからだろうか。よく分かってない。
と思ったらただのバグだった。
Can a Group ARN be specified as a Principal for a Bucket Policy?

ちなみに設定変更はロール詳細画面の Edit Trust Relationship から行う。

edit-trust-policy.png

AssumeRoleの実行

AWS CLI

作成したIAMユーザのアクセスキーを保存し AssumeRole で一時的なクレデンシャルを取得してみる。

$ aws sts assume-role \
> --role-arn arn:aws:iam::XXXXXXXXXXXX:role/power-user \
> --role-session-name in-office \
> --external-id hogehoge \
> --serial-number arn:aws:iam::XXXXXXXXXXXX:mfa/my-user \
> --token-code XXXXXX
{
    "AssumedRoleUser": {
        "AssumedRoleId": "XXXXXXXXXXXXXXXXXXXXX:in-office",
        "Arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/power-user/in-office"
    },
    "Credentials": {
        "SecretAccessKey": "XXXXXXXXXX...",
        "SessionToken": "XXXXXXXXXX...",
        "Expiration": "2015-01-22T03:16:56Z",
        "AccessKeyId": "XXXXXXXXXXXXXXXXXXXX"
    }
}

普段は AssumeRole して設定ファイルを更新するまでの処理をまとめたシェルを使っている。

aws-assume-role
#!/bin/sh

set -e

role_name=$1
external_id=$2
token_code=$3
profile=$role_name

cmd="aws sts assume-role"
cmd="$cmd --role-arn arn:aws:iam::XXXXXXXXXXXX:role/$role_name"
cmd="$cmd --role-session-name in-office"
cmd="$cmd --external-id $external_id"
if [ -n "$token_code" ]; then
  cmd="$cmd --serial-number arn:aws:iam::XXXXXXXXXXXX:mfa/my-user"
  cmd="$cmd --token-code $token_code"
fi

creds=$($cmd)

access_key_id=$(echo $creds | jq --raw-output .Credentials.AccessKeyId)
secret_access_key=$(echo $creds | jq --raw-output .Credentials.SecretAccessKey)
session_token=$(echo $creds | jq --raw-output .Credentials.SessionToken)

aws configure set region ap-northeast-1 --profile $profile
aws configure set aws_access_key_id $access_key_id --profile $profile
aws configure set aws_secret_access_key $secret_access_key --profile $profile
aws configure set aws_session_token $session_token --profile $profile

aws configure list --profile $profile

API を呼び出すまでの一連の流れはこんな感じ。

$  aws-assume-role power-user hogehoge XXXXXX
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile               power-user           manual    --profile
access_key     ****************XXXX shared-credentials-file
secret_key     ****************XXXX shared-credentials-file
    region           ap-northeast-1      config-file    ~/.aws/config
$ aws ec2 describe-regions --profile power-user

$  aws-assume-role read-only hogehoge
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                read-only           manual    --profile
access_key     ****************XXXX shared-credentials-file
secret_key     ****************XXXX shared-credentials-file
    region           ap-northeast-1      config-file    ~/.aws/config
$ aws ec2 describe-regions --profile read-only

AWS SDK for Ruby

同様にSDKの場合。
APIリファレンスには serial_numbertoken_code の記述がないけど渡せば動く。
普段は pry の history に external_id を残さないように気をつけてる。

$ aws.rb --region ap-northeast-1
Aws> assumed_role = Aws::AssumeRoleCredentials.new(
Aws|   role_arn: 'arn:aws:iam::XXXXXXXXXXXX:role/power-user',
Aws|   role_session_name: 'in-office',
Aws|   external_id: 'hogehoge',
Aws|   serial_number: 'arn:aws:iam::XXXXXXXXXXXX:mfa/my-user',
Aws|   token_code: 'XXXXXX'
Aws| )
Aws> assumed_role.credentials.access_key_id
"XXXXXXXXXXXXXXXXXXXX"
Aws> assumed_role.credentials.secret_access_key
"XXXXXXXXXX..."
Aws> assumed_role.credentials.session_token
"XXXXXXXXXX..."

普段は AssumeRole して設定ファイルを更新するまでの処理をまとめたコマンドを使っている。

.pryrc
begin
  require 'aws-sdk-core'
rescue LoadError
else
  # aws.rb may already have been initialized
  Aws.config[:region] ||= 'ap-northeast-1'

  Pry::Commands.import(Pry::CommandSet.new {
    block_command 'aws-assume-role' do |role_name, external_id, token_code|
      system "~/bin/aws-assume-role #{role_name} #{external_id} #{token_code}"
    end
  })
end

API を呼び出すまでの一連の流れはこんな感じ。

$ aws.rb --region ap-northeast-1
Aws> aws-assume-role power-user hogehoge XXXXXX
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile               power-user           manual    --profile
access_key     ****************XXXX shared-credentials-file
secret_key     ****************XXXX shared-credentials-file
    region           ap-northeast-1      config-file    ~/.aws/config
Aws> ec2 = Aws::EC2::Client.new profile: 'power-user'
#<Aws::EC2::Client>
Aws> ec2.describe_regions.regions[0]
[Aws::EC2::Client 200 0.119853 0 retries] describe_regions()
{
  :region_name => "eu-central-1",
  :endpoint    => "ec2.eu-central-1.amazonaws.com"
}
Aws> aws-assume-role read-only hogehoge
      Name                    Value             Type    Location
      ----                    -----             ----    --------
   profile                read-only           manual    --profile
access_key     ****************XXXX shared-credentials-file
secret_key     ****************XXXX shared-credentials-file
    region           ap-northeast-1      config-file    ~/.aws/config
Aws> ec2 = Aws::EC2::Client.new profile: 'read-only'
#<Aws::EC2::Client>

まとめ

MFA はめんどう。