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

CloudFormationでデプロイするユーザのアクセスキーが漏れてしまったら?と心配してしまいます。 そこで、AssumeRoleを利用して、「デプロイ用のIAMユーザ」を少しでも安全に扱う方法を試してみました。
2019.09.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

AWSアクセスキーセキュリティ意識向上委員会って何?

昨今、AWSのアクセスキーを漏洩させてしまうことが原因でアカウントへの侵入を受け、 多額の利用費発生・情報漏洩疑いなど重大なセキュリティ事案が発生するケースが実際に多々起きています。

そこで、アクセスキー運用に関する安全向上の取組みをブログでご紹介する企画をはじめました。

アクセスキーを利用する場合は利用する上でのリスクを正しく理解し、 セキュリティ対策を事前に適用した上で適切にご利用ください。

IAMユーザーのアクセスキー漏洩を少しでも防ぎたい

手元のパソコンや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ロールを除く)

iam.yml

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)を作成します。

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と組み合わせて使ってみる予定です。

参考