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


# 発端

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

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

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

aws-cli には [自動的にAssumeRoleを行ってくれる機能](http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#using-aws-iam-roles) がある。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に保存する。

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

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

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

# IAMユーザの作成

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

## 作成

ここらへんを参考にユーザを作成する。
[AWSで最初にすべきこと　～運用ユーザの作成～](http://qiita.com/moiwasaki/items/ac65481c0b7433aac468)

## アクセスポリシー

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

## MFA の設定

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

[AWSアカウントの2段階認証を設定する](http://qiita.com/macoshita/items/06e2a2474bc8eca1a67d)
[AWSアカウント作ったらこれだけはやっとけ！IAMユーザーとAuthyを使ったMFAで2段階認証](http://qiita.com/yoshidashingo/items/cd206daca0596659b440)

設定後 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](https://qiita-image-store.s3.amazonaws.com/0/379/3ea7119a-fe36-567c-20dd-016e916eec1f.png "select-role-type.png")

### Step 3 : Establish Trust

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

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

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

![establish-trust.png](https://qiita-image-store.s3.amazonaws.com/0/379/547aa4e8-ff70-5727-a2de-eb3d5267337b.png "establish-trust.png")

### Step 4 : Set Permissions

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

## トラストポリシー

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

```json: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?](https://forums.aws.amazon.com/message.jspa?messageID=356160)

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

![edit-trust-policy.png](https://qiita-image-store.s3.amazonaws.com/0/379/38a077b7-265d-bedc-0e86-75d11585e3a0.png "edit-trust-policy.png")

# AssumeRoleの実行

## AWS CLI

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

```bash
$ 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 して設定ファイルを更新するまでの処理をまとめたシェルを使っている。

```bash: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 を呼び出すまでの一連の流れはこんな感じ。

```bash
$  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_number` と `token_code` の記述がないけど渡せば動く。
普段は pry の history に external_id を残さないように気をつけてる。

```ruby
$ 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 して設定ファイルを更新するまでの処理をまとめたコマンドを使っている。

```ruby:.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 を呼び出すまでの一連の流れはこんな感じ。

```ruby
$ 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 はめんどう。