AWS と GitHub Actions の OIDC 接続で Pull Request の Merge のみをトリガーとする際の権限リソースを AWS CDK で作成する

2023.06.10

こんにちは、CX 事業本部 Delivery 部の若槻です。

GitHub Actions 上で AWS に接続したい場合は、OIDC(Open ID Connect)による AssumeRole が推奨されています。

OIDC ではアクセスキーを使う必要がないため、安全に AWS の一時クレデンシャルを取得することができます。

今回は、AWS と GitHub Actions の OIDC 連携で Pull Request の Merge をアクショントリガーとして AWS CDK によるデプロイを行いたい際の権限リソースを、 AWS CDK で作成しつつ、必要最低限のポリシーについて確認をしてみました。

OIDC 連携用のリソースを作成する CDK スタック定義

OIDC 連携に必要な次のリソースの作成および設定を行う CDK スタックを定義します。

  • GitHub とのフェデレーション認証を行う OIDC プロバイダーを作成
  • 引受先のフェデレーションユーザーを制限する信頼ポリシーを定めたロールを作成
  • CDK Deploy に必要な権限を定めたポリシーを作成
  • ロールにポリシーをアタッチ

lib/cdk-deploy-gh-oidc-stack.ts

import { Stack, StackProps, aws_iam } from 'aws-cdk-lib';
import { Construct } from 'constructs';

const GITHUB_OWNER = process.env.GITHUB_OWNER || '';
const GITHUB_REPO = process.env.GITHUB_REPO || '';
const CDK_QUALIFIER = 'hnb659fds'; // 既定値

export class CdkDeployGhOidcStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    const accountId = Stack.of(this).account;
    const region = Stack.of(this).region; // 複数リージョンで使いたい場合は、'*' を指定する。

    // GitHub とのフェデレーション認証を行う OIDC プロバイダーを作成
    const gitHubOidcProvider = new aws_iam.OpenIdConnectProvider(
      this,
      'GitHubOidcProvider',
      {
        url: 'https://token.actions.githubusercontent.com',
        clientIds: ['sts.amazonaws.com'],
      }
    );

    // AssumeRole の引受先を制限する信頼ポリシーを定めたロールを作成
    const gitHubOidcRole = new aws_iam.Role(this, 'GitHubOidcRole', {
      roleName: 'GitHubOidcRole',
      assumedBy: new aws_iam.FederatedPrincipal(
        gitHubOidcProvider.openIdConnectProviderArn,
        {
          StringEquals: {
            // 引受先の Audience(Client ID)を 'sts.amazonaws.com' に制限。
            'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
            'token.actions.githubusercontent.com:sub':
              // トリガーを Pull Request に制限。
              `repo:${GITHUB_OWNER}/${GITHUB_REPO}:pull_request`,
            ,
          },
        },
        'sts:AssumeRoleWithWebIdentity' // 未指定だと既定で 'sts:AssumeRole' が指定されるため、指定必須。
      ),
    });

    // CDK Deploy に必要な権限を定めたポリシーを作成
    const cdkDeployPolicy = new aws_iam.Policy(this, 'CdkDeployPolicy', {
      policyName: 'CdkDeployPolicy',
      statements: [
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['s3:getBucketLocation', 's3:List*'],
          resources: ['arn:aws:s3:::*'],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: [
            'cloudformation:CreateStack',
            'cloudformation:CreateChangeSet',
            'cloudformation:DeleteChangeSet',
            'cloudformation:DescribeChangeSet',
            'cloudformation:DescribeStacks',
            'cloudformation:DescribeStackEvents',
            'cloudformation:ExecuteChangeSet',
            'cloudformation:GetTemplate',
          ],
          resources: [
            `arn:aws:cloudformation:${region}:${accountId}:stack/*/*`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['s3:PutObject', 's3:GetObject'],
          resources: [
            `arn:aws:s3:::cdk-${CDK_QUALIFIER}-assets-${accountId}-${region}/*`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['ssm:GetParameter'],
          resources: [
            `arn:aws:ssm:${region}:${accountId}:parameter/cdk-bootstrap/${CDK_QUALIFIER}/version`,
          ],
        }),
        new aws_iam.PolicyStatement({
          effect: aws_iam.Effect.ALLOW,
          actions: ['iam:PassRole'],
          resources: [
            `arn:aws:iam::${accountId}:role/cdk-${CDK_QUALIFIER}-cfn-exec-role-${accountId}-${region}`,
          ],
        }),
      ],
    });

    // ロールにポリシーをアタッチ
    gitHubOidcRole.attachInlinePolicy(cdkDeployPolicy);
  }
}

ポイントは IAM ロールです。信頼ポリシーを適切に設定することにより、AssumeRole の引受先を必要最低限に制限しています。特に sub で repo:${GITHUB_OWNER}/${GITHUB_REPO}:pull_request と指定することにより、特定の GitHub アカウントのリポジトリの Pull Request に限定しています。

lib/cdk-deploy-gh-oidc-stack.ts

// AssumeRole の引受先を制限する信頼ポリシーを定めたロールを作成
const gitHubOidcRole = new aws_iam.Role(this, 'GitHubOidcRole', {
  roleName: 'GitHubOidcRole',
  assumedBy: new aws_iam.FederatedPrincipal(
    gitHubOidcProvider.openIdConnectProviderArn,
    {
      StringEquals: {
        // 引受先の Audience(Client ID)を 'sts.amazonaws.com' に制限。
        'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
        // トリガーを Pull Request に制限。
        'token.actions.githubusercontent.com:sub': `repo:${GITHUB_OWNER}/${GITHUB_REPO}:pull_request`,
      },
    },
    'sts:AssumeRoleWithWebIdentity' // 未指定だと既定で 'sts:AssumeRole' が指定されるため、指定必須。
  ),
});

上記スタックを CDK デプロイします。

作成された OIDC プロバイダーです。サムプリント(基本的に全アカウントで共通)は自動追加されます。

作成された IAM ロールです。

信頼ポリシーは次のようになります。

Trusted Entity

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Federated": "arn:aws:iam::XXXXXXXXXXXX:oidc-provider/token.actions.githubusercontent.com"
            },
            "Action": "sts:AssumeRoleWithWebIdentity",
            "Condition": {
                "StringEquals": {
                    "token.actions.githubusercontent.com:sub": "repo:cm-rwakatsuki/cdk_sample_app:pull_request",
                    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                }
            }
        }
    ]
}

ロール ARN を Actions Variables に登録

先程作成した OIDC 用ロールの ARN を Actions Variables に登録します。Actions Variables は秘匿する必要がない情報をワークフローから参照させたい場合に便利です。

リポジトリの Settings タブで、Security > Secrets and variables > Actions から登録します。

登録できました。変数(Variable)なので値はマスクされず、登録後も確認することができます。

GitHub Actions Workflow の作成

package.json に deploy コマンドを追加して、アクションから使用できるようにします。

package.json

{
  "scripts": {
    "deploy": "cdk deploy 'cdkSampleStack' --require-approval never"
  },
}

GitHub Actions Workflow です。

.github/workflows/deploy.yaml

on:
  pull_request: # プルリクエストをトリガーにする。
    branches: # トリガーさせるマージ先ブランチを制限。
      - main
    types:
      - closed

env:
  AWS_REGION: ap-northeast-1

jobs:
  deploy:
    permissions:
      # GitHub の OIDC トークンエンドポイントへの接続で必須な権限。
      id-token: write
      contents: read
    runs-on: ubuntu-latest
    if: github.event.pull_request.merged == true
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get temporary credentials with OIDC
        uses: aws-actions/configure-aws-credentials@v2
        with:
          role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
          aws-region: ${{env.AWS_REGION}}

      - name: Cache Dependency
        uses: actions/cache@v3
        id: cache_dependency_id
        env:
          cache-name: cache-cdk-dependency
        with:
          path: node_modules
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}
          restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-

      - name: Install CDK Dependency
        if: steps.cache_dependency_id.outputs.cache-hit != 'true'
        run: npm install

      - name: Deploy
        run: npm run deploy

AWS の公式提供アクションである aws-actions/configure-aws-credentials を使うことにより、OIDC による AssumeRole と、一時クレデンシャルの取得および環境変数への格納を簡単に行うことができます。

.github/workflows/deploy.yaml

- name: Get temporary credentials with OIDC
  uses: aws-actions/configure-aws-credentials@v2
  with:
    role-to-assume: ${{ vars.AWS_OIDC_ROLE_ARN }}
    aws-region: ${{env.AWS_REGION}}

動作確認

main ブランチへのマージによる Pull Request トリガーで AssumeRole が成功する(期待通り)

次のような CDK スタックを GitHub アクションからデプロイしてみます。

lib/cdk-sample-stack.ts

import { aws_lambda_nodejs, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    new aws_lambda_nodejs.NodejsFunction(this, 'sampleFunc');
  }
}

Pull Request を作成し、 main ブランチにマージします。

するとアクションがトリガーされ、OIDC による AssumeRole で取得された一時クレデンシャルで CDK デプロイを実施できました。

Pull Request 以外のトリガーでは AssumeRole が失敗する(期待通り)

ワークフローのトリガーの種類に、プッシュを追加します。

.github/workflows/deploy.yaml

on:
  push: # プッシュをトリガーにする。

ブランチに変更を Push することにより、 GitHub Actions ワークフローをトリガーします。

すると AssumeRole がエラーとなりアクション実行が失敗しました。期待通りの動作です。

エラーは次のようになります。OIDC による AssumeRole の実行が認可できていないことがわかります。

Error: Not authorized to perform sts:AssumeRoleWithWebIdentity

不正な ロール ARN を指定した場合は AssumeRole が失敗する(期待通り)

ワークフローの aws-actions/configure-aws-credentials アクションの実行で role-to-assume に不正なロール ARN を指定してみます。

.github/workflows/deploy.yaml

- name: Get temporary credentials with OIDC
  uses: aws-actions/configure-aws-credentials@v2
  with:
    role-to-assume: 'INVALID_ROLE_ARN'
    aws-region: ${{env.AWS_REGION}}

Pull Request のマージによりアクションを実行させると、Role Arn が不正であるため AssumeRole(のおそらく手前のチェック処理)が失敗しました。期待通りの動作です。

Error: Source Account ID is needed if the Role Name is provided and not the Role Arn.

GitHub の OIDC トークンエンドポイントへの接続権限がない場合は AssumeRole が失敗する(期待通り)

ワークフロージョブの id-token: write および contents: read 権限付与を削除してみます。

.github/workflows/deploy.yaml

jobs:
  deploy:
    permissions:
      # GitHub の OIDC トークンエンドポイントへの接続で必須な権限。
      # id-token: write
      # contents: read
    runs-on: ubuntu-latest

Pull Request のマージによりアクションを実行させると、GitHub の OIDC トークンエンドポイントへ接続できず、AssumeRole が失敗しました。期待通りの動作です。

Error: Credentials could not be loaded, please check your action inputs: Could not load credentials from any providers

その他

ロールを複数のリポジトリに対応させたい場合

OIDC 用ロールを複数のリポジトリに対応させたい場合は、信頼ポリシーの token.actions.githubusercontent.com:sub に配列で複数のリポジトリの pull_request トリガーの許可を指定することにより、OR 条件での許可が可能です。

下記は GITHUB_REPO および GITHUB_REPO_2 の Pull Request に対応させたい場合の例です。

lib/cdk-deploy-gh-oidc-stack.ts

// AssumeRole の引受先を制限する信頼ポリシーを定めたロールを作成
const gitHubOidcRole = new aws_iam.Role(this, 'GitHubOidcRole', {
  roleName: 'GitHubOidcRole',
  assumedBy: new aws_iam.FederatedPrincipal(
    gitHubOidcProvider.openIdConnectProviderArn,
    {
      StringEquals: {
        'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
        'token.actions.githubusercontent.com:sub': [
          // トリガーを GITHUB_REPO および GITHUB_REPO_2 の Pull Request に制限。
          `repo:${GITHUB_OWNER}/${GITHUB_REPO}:pull_request`,
          `repo:${GITHUB_OWNER}/${GITHUB_REPO_2}:pull_request`,
        ],
      },
    },
    'sts:AssumeRoleWithWebIdentity'
  ),
});

もしくは次のように、token.actions.githubusercontent.com:subrepo:${GITHUB_OWNER}/*:pull_request と指定することにより、GITHUB_OWNER の全リポジトリの Pull Request に対応させることも可能です。注意点として、ワイルドカード指定をする場合は StringLike 句を使用する必要があります。

lib/cdk-deploy-gh-oidc-stack.ts

// AssumeRole の引受先を制限する信頼ポリシーを定めたロールを作成
const gitHubOidcRole = new aws_iam.Role(this, 'GitHubOidcRole', {
  roleName: 'GitHubOidcRole',
  assumedBy: new aws_iam.FederatedPrincipal(
    gitHubOidcProvider.openIdConnectProviderArn,
    {
      StringEquals: {
        'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com',
      },
      StringLike: {
        //トリガーを任意のリポジトリの Pull Request に制限。
        'token.actions.githubusercontent.com:sub': `repo:${GITHUB_OWNER}/*:pull_request`,
      },
    },
    'sts:AssumeRoleWithWebIdentity'
  ),
});

この条件文の書き方については、次のドキュメントが参考になります。

AWS SDK for JavaScript (v2) が使われている旨の警告が出るのは仕様

現在、AssumeRole の成否に関わらず、次のような警告が出ます。これは aws-actions/configure-aws-credentials アクションで内部的に AWS SDK for JavaScript の非推奨バージョンである v2 が使用されているためです。

(node:1721) NOTE: We are formalizing our plans to enter AWS SDK for JavaScript (v2) into maintenance mode in 2023.

Please migrate your code to use AWS SDK for JavaScript (v3).
For more information, check the migration guide at https://a.co/7PzMCcy
(Use `node --trace-warnings ...` to show where the warning was created)

これについては Issue にも上がっているので、そのうち対応されると思います。

参考

以上