GitHub ActionsとAWSの連携におけるOIDC認証の活用

# はじめに
2021年10月に、GitHub ActionsはOpenID Connect（OIDC）のサポートを開始しました。
最近、IAMユーザーのアクセスキーを利用した認証から、OIDCを用いた認証方式へ移行する機会があったので、そのために必要な作業と考慮すべき重要なポイントを本記事でまとめてみました。

https://github.blog/changelog/2021-10-27-github-actions-secure-cloud-deployments-with-openid-connect/

## OIDCとは何か？
OpenID Connect（OIDC）は、OAuth 2.0をベースにしたユーザー認証のプロトコルです。
ユーザーの身元を確認し、基本的なプロファイル情報を取得するために使用されます。サードパーティのアプリケーションは、OIDCを通じてユーザー認証を行い、JWT（JSON Webトークン）を使用してユーザー情報を安全に交換します。
他の認証手段と比較して、よりセキュアで柔軟な認証が可能です。

詳細は以下をご参照ください。

https://auth0.com/docs/authenticate/protocols/openid-connect-protocol

## OIDCにすると何が嬉しいの？
以前は、IAMユーザーのアクセスキーとシークレットキーをGitHubリポジトリの`Secrets`に保管し、CIで環境変数としてセットしてAWSへのアクセス認証を制御していました。
* `AWS_ACCESS_KEY_ID`: IAMユーザーに関連づけられるアクセスキー
* `AWS_SECRET_ACCESS_KEY`: アクセスキーに関連づけられるシークレットキー

これらは`Secrets`に保管されているため暗号化されていますが、**永続的なクレデンシャル**をAWS外に持つというセキュリティの課題や漏洩リスクの対処として定期的なローテーションを行う等の運用課題があります。

一方で、OIDC認証方式に移行すると、GitHub ActionsのワークフローからAWSリソースへアクセスする際に、IAMロールに紐付いた**一時クレデンシャル**を利用することになります。
一時クレデンシャルには期限があるため、万が一悪意ある第三者に窃取されたとしても、永続的に利用可能なアクセスキーに比べて危険性は低くなります。

# 認証構成の概要
GitHub ActionsとAWSでの認証にOIDCを使用する場合、通常以下のようなプロセスで認証が行われます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588696/0861c1e7-9792-b11d-62c3-f899ce24f49e.png)

## 認証プロセス
具体的には以下の通りです。
##### 01. GitHub ActionsからGitHub OIDCプロバイダーへのトークン要求
* GitHub ActionsはGitHubのOIDCプロバイダーにOIDCトークンを要求

##### 02. GitHub OIDCプロバイダーからのトークンの応答
* GitHubのOIDCプロバイダーはOIDCトークンをGitHub Actionsに返す

##### 03. AWS IAMへのロール引き受け要求
* GitHub ActionsはOIDCトークンを使用してAWS IAMに一時的なアクセス権限を要求

##### 04. AWS IAMからの一時的なクレデンシャルの応答
* AWS IAMは信頼されたGitHubリポジトリからの認証トークン付きリクエストに基づいて、一時的なアクセストークンを発行

##### 05. AWSサービスへのアクセス
* GitHub Actionsはこの一時トークンを使用して、許可されたAWSサービス（例：ECRへのプッシュ、S3へのアクセスなど）にアクセス

## 構成イメージ
GitHub Actionsを使用してAWS環境にOIDC認証を通じてアクセスする際の関係性を構成図にしてみると、以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588696/99669580-d36d-a9c5-900c-e03cdb3c09e5.png)

# 実装の流れ
GitHub ActionsとAWSの認証にOIDCを使用する際の実装の流れは、以下の通りです。
01. GitHub OIDC プロバイダーをAWSアカウント側で準備
02. 信頼関係を登録したGitHubリポジトリ用のIAM Roleを用意
03. GitHub ActionsワークフローでAssume Roleを指定

本記事では、これらのリソースを設定する際の重要なポイントと具体的な設定例をご紹介します。

## 1. OIDCプロバイダー作成
通常、OIDCプロバイダーの作成には、AWS Management Console、Cloudformation、AWS CLI、またはAWS SDKを使用しますが、今回はCloudformationで作成する例をご紹介します。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-iam-oidcprovider.html

CloudFormationを使用してOIDCプロバイダーを作成するには、OIDCプロバイダーを定義するためのリソースタイプ `AWS::IAM::OIDCProvider`を利用します。
テンプレートでは、以下を指定します。
* プロバイダーのURL
* クライアントIDリスト
* サムプリント指定

yaml形式で記載すると以下のようになります。
```yaml
GithubOidc:
 Type: AWS::IAM::OIDCProvider
 Properties:
   Url: 'https://token.actions.githubusercontent.com'
   ClientIdList:
     - 'sts.amazonaws.com'
   ThumbprintList:
     - 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
```
プロバイダーのURLやクライアントIDリストは固定値となりますが、サムプリントについては[こちら](https://github.blog/changelog/2023-07-13-github-actions-oidc-integration-with-aws-no-longer-requires-pinning-of-intermediate-tls-certificates/)を参照すると、Thumbprint IDを使ってIdp通信をすることはなく、Dummy値（適当な40桁）でも良さそうです。


2023年6月27日に [GitHub Changelog でサムプリントを2種類設定する](https://github.blog/changelog/2023-06-27-github-actions-update-on-oidc-integration-with-aws/)という記事が公開されていましたが、2023年7月6日から[AWS側で証明書の検証](https://github.blog/changelog/2023-07-13-github-actions-oidc-integration-with-aws-no-longer-requires-pinning-of-intermediate-tls-certificates/)が可能になったため、サムプリントの指定は不要のようです。


ただし、Cloudformationの`AWS::IAM::OIDCProvider`でリソースを作る場合 `ThumbprintList`は必須項目なので、ランダム文字列「40文字（0-9 or A-F）」を指定すれば良いということになります。

## 2. OIDC認証用IAMロール作成
OIDC認証用IAMロールをCloudFormationで作成する際の設定ポイントを以下に示します。
yaml形式で記載すると以下のようになります。

```yaml
    Type: AWS::IAM::Role
    Properties:
      RoleName: '<Roleの名前>'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action: sts:AssumeRoleWithWebIdentity
            Principal:
              Federated: !GetAtt GithubOidc.Arn
            Condition:
              StringEquals:
                token.actions.githubusercontent.com:aud:
                  - 'sts.amazonaws.com'
              StringLike:
                token.actions.githubusercontent.com:sub:
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/<ブランチ名>'
      Policies:
        - PolicyName: 'ECRAccess'
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action: 
                  - ecr:GetAuthorizationToken
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - ecr:BatchCheckLayerAvailability
                  - ecr:PutImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                Resource: 'arn:aws:ecr:ap-northeast-1:123456789012:repository/my-repository'  # ここは適切なECRリソースARNに変更
```
### 解説
* `AssumeRolePolicyDocument`は、このロールを引き受けることができるエンティティ（この場合はOIDCプロバイダー）を定義
* Principalに`Federated: !GetAtt GithubOidc.Arn`を使用して、先に作成したGitHub OIDCプロバイダーのARNを参照
* `token.actions.githubusercontent.com:aud`は、トークンの発行者（audience）を指定します。ここでは 'sts.amazonaws.com' を使用
* `token.actions.githubusercontent.com:sub`は、トークンのサブジェクトを指定し、特定のGitHubリポジトリやブランチに対するアクセスを制限します。
* `Policies`セクションで、OIDC認証用IAMロールにAWSリソースへのアクセス権を付与します。（上記例ではECR関連のアクセスと操作を許可）

### 重要なポイント
ここで重要なのは、`token.actions.githubusercontent.com:sub`クレームになります。
`repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/<ブランチ名>`のように指定することで、**特定のリポジトリの特定のブランチからのGitHub ActionsワークフローによるAWSリソースへのアクセスを制御**できます。
これは、セキュリティを強化し、不正なアクセスや意図しない変更からリソースを保護するために重要です。

また、[こちら](https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims)を確認すると、`token.actions.githubusercontent.com:sub`クレームは主にリポジトリ名とGitリファレンス（ブランチやタグ）に関連する情報と、`pull_request`イベントも制御可能なようです。
つまり、リポジトリやブランチ以外にも`pull_request`をトリガーとしたワークフローによるAWSリソースへのアクセス制限が可能なようです。

### 補足
筆者は実装当初、上記の`pull_request`イベントを許可しておらず、Assume Roleの接続試行に失敗しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588696/db4346ca-c42d-a3f2-c6f7-15f6c2e33542.png)


Cloud TrailでGitHub ActionsからAWSへのアクセスログを確認していくと、`pull_request`をトリガーにしたGitHub Actionsワークフローからのアクセスは、`userName`が`repo:<GitHubユーザー名>/<GitHubリポジトリ名>:pull_request`になることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588696/16e45aad-b376-a386-12ef-e5c4abbdd3dd.png)


完全に盲点でした。

CronやWorkflow Dispatch、Merge、Pushをトリガーとしたイベントは指定しなくていいのか？ですが、GitHub ActionsのOIDCトークンのsubクレームでは`pull_request`イベント以外、直接特定できないようなので、リポジトリやブランチ制限に依存することになります。


#### 参考記事
https://stackoverflow.com/questions/75890374/github-action-failing-on-error-not-authorized-to-perform-stsassumerolewithwebi

https://docs.github.com/ja/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#example-subject-claims

以下にsubクレームによるアクセス制限のユースケースをいくつかご紹介します。


### ユースケース1. 特定のリポジトリの全てのブランチを許可
特定のリポジトリの全てのブランチを許可する場合は、ワイルドカード（`*`）を使用します。

```yaml
              StringLike:
                token.actions.githubusercontent.com:sub:
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/*'
```
### ユースケース2. 特定のリポジトリの特定のプレフィックスを持つブランチを許可
特定のプレフィックスを持つブランチにアクセスするためのポリシーを設定したい場合、ワイルドカードを使って以下のように指定できます。

```yaml
              StringLike:
                token.actions.githubusercontent.com:sub:
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/prefix-*'
```
この設定により、「prefix-」で始まる任意のブランチに対するアクセスが許可されます。


### ユースケース3. 特定のリポジトリの複数ブランチを許可
複数のブランチに対するアクセスを許可する場合、`ForAnyValue:StringLike`を使用できます。
これにより、指定されたパターンに一致する任意のブランチに対して条件が適用されます。

```yaml
              ForAnyValue:StringLike:
                token.actions.githubusercontent.com:sub:
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/develop'
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/feature'
```
この設定により、特定のリポジトリの「develop」ブランチと「feature」ブランチに対するアクセスが許可されます。

### ユースケース4. 特定のリポジトリの複数ブランチを許可 かつ `pull_request`をトリガーとしたワークフローも許可
```yaml
              ForAnyValue:StringLike:
                token.actions.githubusercontent.com:sub:
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/develop'
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:ref:refs/heads/feature'
                  - 'repo:<GitHubユーザー名>/<GitHubリポジトリ名>:pull_request'
```
この設定により、特定のリポジトリの「develop」ブランチと「feature」ブランチに対するアクセスが許可され、かつ、特定のリポジトリから`pull_request`をトリガーとしたワークフローによるAWSリソースへのアクセス制限が可能になります。

上記のようにアクセス制限をかけた場合に「test」ブランチからアクセスを行うと、「**Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity**」といったエラーメッセージで、Assume Roleに失敗します。

## 3. GitHub Actions ワークフローの設定
最後に、GitHub Actionsのワークフローで、先に作成したIAMロールを指定するよう設定します。
AWSへのアクセス認証には、[aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials)アクションを使用します。
`role-to-assume`に、先に作成したIAMロールの`ARN`を指定することで、OIDCを使用したAWS認証を行うことができます。

GitHub Actionsを使用してOIDC認証を行い、AWS Elastic Container Registry (ECR) にログインし、イメージをECRにプッシュするワークフローの場合、以下のように記述できます。
``` yaml
name: ECR Push Workflow

on:
  push:
    branches:
      - develop  # このワークフローをトリガーするブランチを指定

jobs:
  deploy:
    name: Deploy to ECR
    runs-on: ubuntu-latest

    permissions:
      id-token: write
      contents: read

    steps:
      - name: Checkout repository
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-region: YOUR_AWS_REGION  # AWSリージョンを指定
          role-to-assume: YOUR_OIDC_ROLE_ARN  # OIDCを使用してアクセスするIAMロールのARN

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: YOUR_ECR_REPOSITORY_NAME  # ECRリポジトリ名を指定
          IMAGE_TAG: latest
        run: |
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

      # その他の必要なステップを追加


```
この例では、GitHub Actionsのワークフローが特定のブランチにプッシュされたときにトリガーされるように設定されています。

### 重要なポイント
ここで重要なのは、`permissions`セクションです。
`id-token: write`を設定しないと OIDC を使えないので注意です。
これにより、GitHub ActionsがIDトークンを生成し、外部サービスに対して認証情報として使用できるようになります。

https://docs.github.com/ja/actions/security-guides/automatic-token-authentication#using-the-github_token-in-a-workflow

`permissions`キーを用いて、IDトークンの利用を明示的に許可することに伴い、
<u>他のScopeもデフォルトの権限が「無効」となります。</u>
そのため、「**必要な権限は全て明示的に記載**」する必要があリます。

各種Scopeの説明は以下をご参照ください。

https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions#permissions

ワークフロー内で実行されるジョブによって、必要な`permissions`は異なりますが、筆者が確認し、主に利用する`permissions`は以下の通りでした。

| Scope            | R/W         | Remark |
|----------------|--------------|--------|
| `actions`  | `write`    | `slack-notice-action`の利用など。ワークフローが他のジョブやワークフローをトリガーする、またはアーティファクトやログを書き込む場合に必要。 |
| `contents`     | `read`       | `actions/checkout`の利用など。 |
| `id-token`     | `write`      | GitHubのOIDCプロバイダから発行されるトークンを利用するために必要。 **(MUST)**|
| `pull-requests`| `write`      | プルリクエストにコメントを投稿したり、ステータスを更新するなどの操作を行う場合など。 |
| `deployments`  | `write`      | リポジトリのデプロイメント情報を取得する場合など。|

https://docs.github.com/ja/rest/overview/permissions-required-for-github-apps?apiVersion=2022-11-28#repository-permissions-for-deployments

これらの権限が不足すると、ワークフロー実行時に「**Resource not accessible by integration**」というエラーメッセージが返ってくることがあります。

#### GitHub ActionsワークフローでOIDC認証を利用した結果例
GitHub ActionsがAWSのIAMロールを引き受けるプロセスが成功した例は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3588696/53f19a04-2df0-2bd7-8079-54a89ab2ff3e.png)

**Assuming role with OIDC:**
GitHub ActionsがOIDC認証を使用してAWSのIAMロールを引き受けています。

**Authenticated as assumedRoleId xxxx:GitHubActions:**
この部分は、GitHub Actionsが特定のIAMロール（この例ではIDがxxxxのロール）を引き受け、そのロールを利用して認証されたことを示しています。


以上の3点が、OIDC認証を利用するまでの実装手順になります。

# まとめ
ここまで、GitHub ActionsがサポートするOpenID Connect（OIDC）を用いてAWS環境にセキュアにアクセスする方法についてまとめてみました。

OIDC認証の実装には、GitHub OIDCプロバイダーの準備、IAMロールの設定、GitHub ActionsワークフローでのAssume Roleの指定が主な流れとなります。

特に、IAMロールで指定する`token.actions.githubusercontent.com:sub`は、特定のGitHubリポジトリやブランチに対するアクセス制限を行う設定となるため、よりセキュアな環境とするには重要な設定です。

また、Gihub Actionsワークフローで指定する`permissions`の`id-token: write`設定を含め、セキュリティを確保するための適切な設定が何かをワークフローの特性によって判断し、明示的に定義してあげる必要があります。

最後に、本記事の情報が少しでも誰かのお役に立てれば幸いです。