CloudFormationを使って一撃で作るAssumeRoleによる強い💪権限管理

インフラエンジニアのみつの（@kotatsu360）です。
この記事は[VASILY Advent Calendar 2017](https://qiita.com/advent-calendar/2017/vasily)10日目の記事です。

この記事では、AWSのIAMでAssumeRoleする権限周りを一撃でつくるCloudFormationテンプレートをまとめます。ご査収ください。

CloudFormationが作る環境のイメージ
![image.png](https://qiita-image-store.s3.amazonaws.com/0/62601/f940ba19-a168-93de-339d-7ab64312e840.png)

# このドキュメントの目的
IAMは調べれば調べるほど情報が断片的になる。あとJSON書くの面倒(一一 )
CloudFormationならYAMLで書けるし再現性がある。テンプレートにまとめようヽ(ﾟ∀ﾟ)ﾉ ﾊﾟｯ☆

# AssumeRoleとは
1. 役割ごとに必要な権限を持ったIAMロールを作っておく
   * 開発
   * 運用
   * 請求
   * 監査
   * etc...
2. IAMロールごとに「自分の権限を使うことを許可する」対象を設定しておく
   * 開発用のロールはエンジニアAとBとCと...
   * IAMロール「力がほしい時はいつでも我が名を呼ぶが良い」
3. つかう
   * 変身
   * 普段はブラックRX。たまにはバイオライダー。

## メリット
* 権限をロールという単位で整理できる
  * 個人ごとに細かく設定するとスパゲッティになりがち
* 場面場面で必要な権限だけが使える
  * 「開発時に誤って本番構成を変更してしまって泣く」ということが減る
* 権限の確認が楽
  * 個人ごとに設定すると、権限付与者（おそらく強い権限持ち）と非付与者の差異が生まれる
  * AssumeRoleであれば、そのロールにスイッチして自分で権限を確認できる

## デメリット
* めんどう
  * 「rootアカウントでいいじゃん」と言われたらそこまで・・・
  * アカウント管理をやりたい時はとてもオススメ

なお、後から入れるのはカロリーを消費するので導入は早い方が良いと思います。
また、CloudFormationを使うとコード管理できるので、かなり楽になります。
  
# CloudFormationテンプレートで実行されること
* IAMユーザの作成
  * ユーザは4人
    * 開発者
    * 開発者&本番運用者
    * 開発者&本番運用者
    * 請求管理者
  * 初ログイン時にパスワード変更画面を出す
* IAMグループの作成
  * 全員用のグループ
  * 本番運用者用のグループ
* IAMロールの作成
  * 開発者用の権限を管理するロール
  * 本番運用者用の権限を管理するロール
  * 請求管理者用の権限を管理するロール
* ユーザをグループに追加

グループとロールの住み分けについては後で触れます。

# CloudFormationテンプレート

初期パスワードが小文字と記号になっているので、アカウントのパスワードポリシーによっては作成に失敗します。

```yaml
AWSTemplateFormatVersion: 2010-09-09
Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label:
          default: 'IAM User Name'
        Parameters:
          - Dev1
          - Prod1
          - Prod2
          - Billing1
Parameters:
  Dev1:
    Type: String
    Default: 'kotatsu360a'
  Prod1:
    Type: String
    Default: 'kotatsu360b'
  Prod2:
    Type: String
    Default: 'kotatsu360c'
  Billing1:
    Type: String
    Default: 'kotatsu360d'
Resources:
  # ユーザ全員が所属するグループ
  IAMGroupAllMember:
    Type: 'AWS::IAM::Group'
    Properties:
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'iam:List*'
                Resource: '*'
              - Effect: 'Allow'
                Action:
                  - 'iam:GetLoginProfile'
                  - 'iam:ChangePassword'
                  - 'iam:CreateVirtualMFADevice'
                  - 'iam:DeleteVirtualMFADevice'
                  - 'iam:EnableMFADevice'
                Resource: 
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}'
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
             - Effect: 'Allow'
                Action:
                  - 'am:DeactivateMFADevice'
                Resource:
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}'
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username}'
                Condition:
                  Bool:
                    aws:MultiFactorAuthPresent: true
          PolicyName: 'mfa-attach'

  # 本番運用者が所属するグループ
  IAMGroupProductionOperator:
    Type: 'AWS::IAM::Group'
    Properties:
      Policies:
        - PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: 'Allow'
                Action:
                  - 'opsworks:UpdateMyUserProfile'
                  - 'opsworks:Describe*'
                Resource: '*'
          PolicyName: 'change-my-ssh-key'

  # [NOTE] 開発者と請求管理者については個人宛のルールが必要ないのでグループを作らない

  # 開発者の権限を管理するロール
  IAMRoleDeveloper:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'developer'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserDev1.Arn
                - !GetAtt IAMUserProd1.Arn
                - !GetAtt IAMUserProd2.Arn
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ReadOnlyAccess'

  # 本番運用者の権限を管理するロール
  IAMRoleProductionOperator:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: 'production-operator'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserProd1.Arn
                - !GetAtt IAMUserProd2.Arn
            Action: 'sts:AssumeRole'
            Condition:
              Bool:
                aws:MultiFactorAuthPresent: true
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/ReadOnlyAccess'
        - 'arn:aws:iam::aws:policy/AmazonS3FullAccess'

  # 請求管理者の権限を管理するロール
  IAMRoleBillingOwner:
    Type: "AWS::IAM::Role"
    Properties:
      RoleName: 'billing-owner'
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserBilling1.Arn
            Action: 'sts:AssumeRole'
            Condition:
              Bool:
                aws:MultiFactorAuthPresent: true
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/job-function/Billing'

  IAMUserDev1:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_whisky'
        PasswordResetRequired: true
      UserName: !Ref Dev1

  IAMUserProd1:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_cigar'
        PasswordResetRequired: true
      UserName: !Ref Prod1

  IAMUserProd2:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_sleeping'
        PasswordResetRequired: true
      UserName: !Ref Prod2

  IAMUserBilling1:
    Type: 'AWS::IAM::User'
    Properties:
      LoginProfile:
        Password: 'i_love_money'
        PasswordResetRequired: true
      UserName: !Ref Billing1

  IAMUserToGroupAdditionAllMember:
    Type: 'AWS::IAM::UserToGroupAddition'
    Properties:
      GroupName: !Ref IAMGroupAllMember
      Users:
        - !Ref IAMUserDev1
        - !Ref IAMUserProd1
        - !Ref IAMUserProd2
        - !Ref IAMUserBilling1

  IAMUserToGroupAdditionProductionOperator:
    Type: 'AWS::IAM::UserToGroupAddition'
    Properties:
      GroupName: !Ref IAMGroupAllMember
      Users:
        - !Ref IAMUserProd1
        - !Ref IAMUserProd2
```


# 権限の補足

## IAMグループの使いどころ

権限は次の方針で設定しています。

* ユーザ自体には何の権限も与えない
* IAMロールで権限管理

しかし、幾つかの権限はロール管理が難しいです。

* 自分のパスワードの変更
  * 特に、初ログイン時のパスワード変更はユーザ権限での操作以外ムリ
* 自分のMFA設定
* 自分のSSH鍵管理
  * OpsWorksを使う場合

「自分の」という部分が曲者です。単に権限を与えるだけでなく、「自分だけできる」というのが重要です。
そのため、これら「自分の情報だけに対して更新権限を与えたい」という場合はIAMグループを使って権限付与しています。

```yaml
              - Effect: 'Allow'
                Action:
                  - 'iam:GetLoginProfile'
                  - 'iam:ChangePassword'
                  - 'iam:CreateVirtualMFADevice'
                  - 'iam:DeleteVirtualMFADevice'
                  - 'iam:EnableMFADevice'
                Resource: 
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:mfa/${!aws:username}' # 自分のMFAだけを対象に
                  - !Sub 'arn:aws:iam::${AWS::AccountId}:user/${!aws:username} # 自分のユーザ情報だけを対象に
```

OpsWorksはActionレベルで自分だけに対してのものを用意してくれているのでそれを使います。

```yaml
              - Effect: 'Allow'
                Action:
                  - 'opsworks:UpdateMyUserProfile' # 自分の情報だけの更新権限, 参照権限版もある（DescribeMyUserProfile）
                  - 'opsworks:Describe*'
                Resource: '*'
```

## AssumeRoleの条件
AssumeRoleは、ユーザの指定以外にも特定の条件を満たした場合のみ、という制約をつけることができます。

本番運用者と請求管理者にはユーザの指定以外にMFAでログインしている場合のみAssumeRoleを許可するという制約をつけました。

```yaml
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !GetAtt IAMUserBilling1.Arn
            Action: 'sts:AssumeRole'
            Condition:
              Bool:
                aws:MultiFactorAuthPresent: true # MFAログインでないとConditionが満たせずAssumeRoleできない
```

# さいごに
AssumeRoleを理解してからは権限管理の世界が広がった感があります。
ぜひ権限を追加して、実態に即したロールに育ててください。

# さいごのさいごに
（IAMの具体的な使い方を紹介する記事がもっと増えますように）