[AssumeRole] アクセスキーが流出しても被害が最小限になるIAMユーザでCloudFormationにデプロイする方法

手元のパソコンやCircleCIなどを使ってAWS(CloudFormation等)にデプロイするとき、強い権限(Create系など)が必要です。

その権限を保持するIAMユーザのアクセスキー (アクセスキーID & シークレットアクセスキー) が漏れてしまったら?と心配する方は多いと思います。 実際に下記のような例も存在します。

そこで、「デプロイ用のIAMユーザ」を少しでも安全に扱う方法を試してみました。 「強い権限を持つIAMユーザのアクセスキーをそのまま使ってデプロイしている状態」をやめる第一歩になればと思います。

目次

全体像

下記の2つを組み合わせます。

  • AssumeRole
  • CloudFormation用のIAMロール

AssumeRoleを活用してデプロイする概要図

ポイントは下記です。

  • IAMユーザ自身には、AssumeRoleができる権限しか持たせない
  • デプロイ用のIAMロールにAssumeRoleして、一時的なアクセスキーを入手する
  • 一時的なアクセスキーを用いて、CloudFormationにデプロイする
  • デプロイ時にCloudFormationが用いるIAMロールを指定する

AssumeRoleとは?

すごく簡単に表現すれば、「特定のRole(に紐づく権限)を一時的に引き受ける(Assume)」です。

AssumeRoleを活用すると、本来持っていない権限を一時的に得られるため、「デプロイ時のみ特定の権限を得る」ことができます。

今回はデプロイに特化していますが、「複数のAWSアカウントのログイン管理を集約」することもできます。 (ログイン用のIAMユーザを作成し、実際の権限はIAMロールで管理し、ログイン後にIAMロールを切り替えて作業する、など)

CloudFormation用のIAM Roleとは?

CloudFormation操作時に権限が無くても、特定のRole(に紐づく権限)をCloudFormation自体に与えることができます。

これにより、たとえば「Lambdaに対する権限がゼロ」でも、「CloudFormationに与えるロールにLambdaFullAccessの権限がある」なら、Lambdaのデプロイが可能になります。

実際にやってみる

今回は「同じAWSアカウント」にデプロイする想定ですが、「異なるAWSアカウント」にデプロイする場合も、「信頼するAWSアカウントID」を任意に変更し、各種調整をすればOKです。

IAM関連で作成するもの

下記を作成します。

AWS 名前 役割
IAM User deploy-iam-sample-user デプロイ用のIAMユーザ
IAM Poricy deploy-iam-sample-user-policy IAMユーザにAssumeRole権限を持つ
IAM Role deploy-iam-sample-deploy-role-for-user CloudFormationとS3に対する権限を持つ
IAM Role deploy-iam-sample-deploy-role-for-cloudformation AWSの各サービスに対する権限を持つ

IAMユーザ & IAMロールを作成

デプロイ用のIAMユーザと付与するIAMポリシーについて

このユーザ自身に与える権限は、AssumeRoleできる権限のみです。 そのため、万が一このIAMユーザのアクセスキーが流出しても、流出したアクセスキーでは実質何もできません。

デプロイ用のIAMユーザがAssumeRoleするIAMロールについて

「デプロイ用のIAMユーザ」がAssumeRoleする(引き受ける)「IAMロール」です。 今回作成するIAMロールには下記の権限を付与しますが、必要に応じて変更してください。

  • S3バケットの作成権限
  • S3オブジェクトの作成権限
    • お試しデプロイとしてAWS SAMを使うため、S3の権限も付与(Lambdaコードのアップロードをするため)
  • CloudFormationのデプロイ準備に必要な権限
    • 「CloudFormation用のIAMロール」はcloudformation:ExecuteChangeSet実行時に使われるため、スタックやチェンジセットの作成権限はこのIAMロールに必要
  • CloudFormationにIAMロールを渡す権限

また、信頼する相手として「対象となるIAMユーザのAWSアカウント」を指定します。

CloudFormation用のIAMロールについて

CloudFormationが実際のデプロイ(LambdaやDynamoDBの作成等)で使用する権限を付与します。 この権限自体はIAMユーザに紐付いておらず、信頼する相手として「CloudFormation」を指定します。

実際に作成する

iam.ymlを作成します。SamAppStackNameBucketNameは実際に使用する値をデフォルト値として設定していますが、コマンド実行時に外部から渡すのもアリですね。

※2019/9/10 20:30更新:FullAccess系の権限を使わないようにしました(CloudFormation用のIAMロールを除く)

AWSTemplateFormatVersion: 2010-09-09
Description: Create IAM User and Role for deploy

Parameters:
  SamAppStackName:
    Type: String
    Default: "Deploy-Iam-Sample-SAM-App"

  BucketName:
    Type: String
    Default: "cm-fujii.genki-sam-app-sample-bucket"

Resources:
  # デプロイ用のIAMユーザ
  DeployUser:
    Type: AWS::IAM::User
    Properties:
      UserName: "deploy-iam-sample-user"

  # デプロイ用のIAMユーザに付与するIAMポリシー(AssumeRoleできる)
  DeployUserPoricy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: "deploy-iam-sample-user-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Resource: !GetAtt DeployRoleForUser.Arn
      Users:
        - !Ref DeployUser

  # デプロイ用のIAMユーザがAssumeRoleするIAMロール(CloudFormationとS3に対する権限)
  DeployRoleForUser:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "deploy-iam-sample-deploy-role-for-user"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              AWS:
                - !GetAtt DeployUser.Arn
            Condition:
              StringEquals:
                sts:ExternalId: "any-id-hoge-fuga"
      Policies:
        - PolicyName: "deploy-iam-sample-deploy-policy-for-user"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "cloudformation:CreateStack"
                  - "cloudformation:CreateChangeSet"
                  - "cloudformation:DeleteChangeSet"
                  - "cloudformation:DescribeChangeSet"
                  - "cloudformation:DescribeStacks"
                  - "cloudformation:ExecuteChangeSet"
                Resource:
                  - !Sub "arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/${SamAppStackName}/*"
              - Effect: "Allow"
                Action:
                  - "s3:CreateBucket"
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}"
              - Effect: "Allow"
                Action:
                  - "s3:PutObject"
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}/*"
              - Effect: "Allow"
                Action:
                  - "iam:PassRole"
                Resource:
                  - !GetAtt DeployRoleForCloudFormation.Arn
      MaxSessionDuration: 3600

  # CloudFormation用のIAMロール(AWS各サービスに対する権限)
  DeployRoleForCloudFormation:
    Type: AWS::IAM::Role
    Properties:
      RoleName: "deploy-iam-sample-deploy-role-for-cloudformation"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service:
                - "cloudformation.amazonaws.com"
      # 実際にデプロイする際に必要な権限
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCloudFormationFullAccess
        - arn:aws:iam::aws:policy/AWSLambdaFullAccess
        - arn:aws:iam::aws:policy/IAMFullAccess
        - arn:aws:iam::aws:policy/AmazonAPIGatewayAdministrator
      MaxSessionDuration: 3600

Outputs:
  OutputDeployUser:
    Description: "IAM User for Deploy"
    Value: !GetAtt DeployUser.Arn

  OutputDeployRoleForUser:
    Description: "IAM Role (AssumeRole) for Deploy User"
    Value: !GetAtt DeployRoleForUser.Arn

  OutputDeployRoleForCloudFormation:
    Description: "IAM Role (AssumeRole) for  Deploy CloudFormation"
    Value: !GetAtt DeployRoleForCloudFormation.Arn

上記のYAMLを作成してデプロイします。この操作自体は、作成権限のあるアカウントで最初に実行します。

$ aws cloudformation deploy \
    --template-file iam.yml \
    --stack-name Deploy-Iam-Sample-Iam \
    --capabilities CAPABILITY_NAMED_IAM

作成したIAMロールのARNが必要になるため、取得しておきます。(下記のOutputValueです)

$ aws cloudformation describe-stacks \
    --stack-name Deploy-Iam-Sample-Iam \
    --query 'Stacks[].Outputs'
[
    [
        {
            "OutputKey": "OutputDeployUser",
            "OutputValue": "arn:aws:iam::1234567890:user/deploy-iam-sample-user",
            "Description": "IAM User for Deploy"
        },
        {
            "OutputKey": "OutputDeployRoleForCloudFormation",
            "OutputValue": "arn:aws:iam::1234567890:role/deploy-iam-sample-deploy-role-for-cloudformation",
            "Description": "IAM Role (AssumeRole) for  Deploy CloudFormation"
        },
        {
            "OutputKey": "OutputDeployRoleForUser",
            "OutputValue": "arn:aws:iam::1234567890:role/deploy-iam-sample-deploy-role-for-user",
            "Description": "IAM Role (AssumeRole) for Deploy User"
        }
    ]
]

デプロイ用のIAMユーザのアクセスキーを取得

下記コマンドで取得します。「デプロイ用のIAMユーザ」に切り替える際に使用します。

$ aws iam create-access-key \
	--user-name deploy-iam-sample-user
{
    "AccessKey": {
        "UserName": "deploy-iam-sample-user",
        "AccessKeyId": "aaaaaa",
        "SecretAccessKey": "bbbbbb"
    }
}

デプロイ対象(サーバーレスなアプリ)の準備

AWS SAMでデフォルト作成されるLambdaアプリケーションをそのままデプロイします。

デプロイ時に--role-arnオプションでIAMロールを指定できます。 CloudFormation(AWS CLI)やAWS CDKでも--role-arnオプションが同じように使えます。

なお、--role-arnで指定したIAMロールは、cloudformation:ExecuteChangeSet実行時に使用されます。

--role-arn (string) The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that AWS CloudFormation assumes when executing the change set. deploy — AWS CLI 1.16.234 Command Reference

AWS SAMプロジェクトを作成

下記コマンドで作成します。

sam init --runtime python3.7 --name AppSample

デプロイ用のIAMユーザに切り替える

あらかじめ取得しておいたアクセスキーを使用し、「デプロイ用のIAMユーザ」に切り替えます。 CircleCIなどでデプロイする場合は、「デプロイ用のIAMユーザのアクセスキー」を使用して以降の流れを再現すればOKですね。

export AWS_ACCESS_KEY_ID=aaaaaa
export AWS_SECRET_ACCESS_KEY=bbbbbb
export AWS_DEFAULT_REGION=ap-northeast-1

試しにこの状態でCloudFormationのスタック一覧やLambda関数一覧を取得しようとしても、権限が無いため失敗します。

$ aws cloudformation list-stacks
An error occurred (AccessDenied) when calling the ListStacks operation: User: arn:aws:iam::1234567890:user/deploy-iam-sample-user is not authorized to perform: cloudformation:ListStacks

$ aws lambda list-functions
An error occurred (AccessDeniedException) when calling the ListFunctions operation: User: arn:aws:iam::1234567890:user/deploy-iam-sample-user is not authorized to perform: lambda:ListFunctions on resource: *

AssumeRoleする

まずは普通にAssumeRoleする

下記コマンドでAssumeRoleを行い、「デプロイ用のIAMユーザがAssumeRoleするIAMロール」の一時アクセスキーを取得します。1234567890部分は、各自のAWSアカウントIDを使用します。

$ aws sts assume-role \
	--role-arn arn:aws:iam::1234567890:role/deploy-iam-sample-deploy-role-for-user \
	--role-session-name deploy-test \
    --duration-seconds 900 \
    --external-id any-id-hoge-fuga
{
    "Credentials": {
        "AccessKeyId": "xxxxxx",
        "SecretAccessKey": "yyyyyy",
        "SessionToken": "zzzzzz",
        "Expiration": "2019-09-06T12:31:37Z"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "hoge:deploy-test",
        "Arn": "arn:aws:sts::1234567890:assumed-role/deploy-iam-sample-deploy-role-for-user/deploy-test"
    }
}

下記コマンドでアクセスキーを切り替える(上書きする)ことで、「デプロイ用のIAMユーザがAssumeRoleするIAMロール」として、AWSにアクセスできるようになります。

export AWS_ACCESS_KEY_ID=xxxxxx
export AWS_SECRET_ACCESS_KEY=yyyyyy
export AWS_SESSION_TOKEN=zzzzzz

めんどくさいのでスクリプトを作る

AssumeRoleする作業がめんどくさいですし、CircleCI等で実行する場合は上記のように手動で作業はできません。

そこで下記のスクリプト(assume_role.sh)を作成します。

#!/usr/bin/env bash

set -xeuo pipefail

aws_sts_credentials="$(aws sts assume-role \
  --role-arn "$AWS_DEPLOY_IAM_ROLE_ARN" \
  --role-session-name "$ROLE_SESSION_NAME" \
  --external-id "$AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID" \
  --duration-seconds 900 \
  --query "Credentials" \
  --output "json")"

cat <<EOT > "aws-env.sh"
export AWS_ACCESS_KEY_ID="$(echo $aws_sts_credentials | jq -r '.AccessKeyId')"
export AWS_SECRET_ACCESS_KEY="$(echo $aws_sts_credentials | jq -r '.SecretAccessKey')"
export AWS_SESSION_TOKEN="$(echo $aws_sts_credentials | jq -r '.SessionToken')"
EOT

jqが無い場合は、インストールします。(下記はMacでインストールする例)

$ brew install jq

あらかじめAssumeRoleするIAMロール名などを環境変数にセットしておきます。

export AWS_DEPLOY_IAM_ROLE_ARN=arn:aws:iam::1234567890:role/deploy-iam-sample-deploy-role-for-user
export ROLE_SESSION_NAME=deploy-test
export AWS_DEPLOY_IAM_ROLE_EXTERNAL_ID=any-id-hoge-fuga

続いてassume_role.shを実行し、生成されたaws_env.shを読み込めばOKです。

./assume_role.sh
source aws-env.sh

デプロイする

S3バケットの作成

コード等を格納するためのS3バケットを作成します。作成済みの場合は飛ばします。

aws s3 mb s3://cm-fujii.genki-sam-app-sample-bucket

build

下記でビルドします。

sam build

package

コード一式をS3バケットにアップロードします。

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-sam-app-sample-bucket

deploy

デプロイします。このとき、--role-arnオプションで「CloudFormation用のIAMロール」を指定します。

sam deploy \
    --template-file packaged.yaml \
    --stack-name Deploy-Iam-Sample-SAM-App \
    --capabilities CAPABILITY_IAM \
    --role-arn arn:aws:iam::1234567890:role/deploy-iam-sample-deploy-role-for-cloudformation

ちなみに、--role-arnオプションを付けない場合は、「IAMロールの作成ができない(権限が無い)」ので失敗します。

API: iam:CreateRole User: arn:aws:sts::1234567890:assumed-role/deploy-iam-sample-deploy-role-for-user/deploy-test is not authorized to perform: iam:CreateRole on resource: arn:aws:iam::1234567890:role/Deploy-Iam-Sample-SAM-App-HelloWorldFunctionRole-USD7R9OSWGWH

動作確認

API Gatewayのアドレスを取得する

$ aws cloudformation describe-stacks \
    --stack-name Deploy-Iam-Sample-SAM-App \
    --query 'Stacks[].Outputs'
[
    [
        {
            "OutputKey": "HelloWorldApi",
            "OutputValue": "https://ttttt.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/",
            "Description": "API Gateway endpoint URL for Prod stage for Hello World function"
        }
    ]
]

WebAPIを叩く

HelloWorldApiを元に叩きます。

$ curl https://ttttt.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "hello world"}

動いてますね!!

さいごに

権限をさらに小さくしたり、細かい調整は必要になると思いますが、「強い権限を持つIAMユーザのアクセスキーをそのまま使ってデプロイしている状態」をやめる第一歩になればと思います。

個人的には、CircleCIと組み合わせて使ってみる予定です。

参考