【小ネタ】GitHub Actions用のIAMロールをAWSマネジメントコンソールから作成する際の注意点

GitHub Actionsで利用するIAMロールの信頼関係には、Conditionとしてtoken.actions.githubusercontent.com:subで組織とリポジトリとジョブ環境名を指定する必要があります。
2021.11.22

コンサル部のとばち(@toda_kk)です。

先月、GitHub ActionsがOpenID Connect(OIDC)に対応したことが発表されました。

実はそれ以前から対応は進んでおり、公式なアナウンスはないものの、ちらほらと「試してみた」系の記事が上がっていました。

具体的には、AWS IAMのIDプロバイダーを利用することで、GitHub ActionsにAWSユーザーにアクセスキーなど永続的なクレデンシャルを渡すことなく、IAMロールをベースとした権限管理によってAWSリソースの操作ができるようになる、という内容です。

2021年現在、GitHub側でも公式ドキュメントが作成されており、AWSとの連携について説明されています。

また、AWS側からも "Configure AWS Credentials" という形でActionが提供されています。詳細については、公式のGitHubリポジトリや、下記のブログなどをご参照ください。

さて、あとは記載されている手順の通りに、IAM IDプロバイダーやIAMロールを作成し、GitHub ActionsのWorkflowでIAMロールを利用するように設定すれば準備完了です。

IAMリソースの作成についてはCloudFormationテンプレートも用意されており、もうこれをそのまま流せば良いわけです。

IAMロールをマネジメントコンソールから作成する

とはいえ、今回は検証ということで、用意されたCloudFormationテンプレートを使わずにマネジメントコンソールから手動でIAMリソースを作成していました。

まずは、IAM IDプロバイダーを作成します。設定内容は上記のブログそのまんまです。作成すると下記のような表示になります。

続いて、IAMロールを作成するわけですが、ここで注意点があります。

マネジメントコンソールからIAMロールを作成する際、最初の画面で「信頼されたエンティティ」を指定する必要があります。一般的にはAWSサービスを指定すると思いますが、今回はOIDCプロバイダーとして先ほど作成したIAMプロバイダーを指定する必要があるわけです。

ここで、IDプロバイダーのリストからtoken.actions.githubusercontent.com:audが選択できるのですが、この項目では設定不備となります

このまま作成を続けていくと、信頼関係を示すポリシードキュメントは下記のような内容で作成されます。

マネジメントコンソールから作成した場合の信頼関係

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        }
      }
    }
  ]
}

そのままだと認証エラーになる

しかし、このままではWorkflowは正しく実行されません。エラーとなり、Not authorized to perform sts:AssumeRoleWithWebIdentityというメッセージが出てしまいます。

実行したWorkflow

name: "test_github_oidc"

on: [ workflow_dispatch ]

env:
  AWS_ROLE_ARN: arn:aws:iam::${AWS_ACCOUNT_ID}:role/test-github-oidc-role

jobs:
  test_github_oidc:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: write
    steps:
      - uses: actions/checkout@v2
      - uses: aws-actions/configure-aws-credentials@master
        with:
          role-to-assume: ${{ env.AWS_ROLE_ARN }}
          role-session-name: test-github-oidc-role-session
          aws-region: ap-northeast-1
      - run: aws sts get-caller-identity
      - run: aws s3 ls

原因がわからずCloudTrailイベントも確認しましたが、単にAccessDeniedとしか出力されておらず、調査にだいぶハマってしまいました。

エラーのイベントレコード

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "WebIdentityUser",
        "principalId": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com:sts.amazonaws.com:repo:${AWS_ACCOUNT_ID}/${RepositoryName}:ref:refs/heads/main",
        "userName": "repo:${AWS_ACCOUNT_ID}/${RepositoryName}:ref:refs/heads/main",
        "identityProvider": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
    },
    "eventTime": "2021-11-22T04:28:55Z",
    "eventSource": "sts.amazonaws.com",
    "eventName": "AssumeRoleWithWebIdentity",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "xxx.xxx.xxx.xxx",
    "userAgent": "aws-sdk-nodejs/2.1029.0 linux/v12.13.1 configure-aws-credentials-for-github-actions promise",
    "errorCode": "AccessDenied",
    "errorMessage": "An unknown error occurred",
    "requestParameters": {
        "durationSeconds": 3600,
        "roleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/test-github-oidc-role",
        "roleSessionName": "deploy-role-session"
    },
    "responseElements": null,
    "requestID": "09a6efcb-877c-4e05-8d9f-5ebad36356ee",
    "eventID": "3fdbb00f-0206-41cc-8988-6f7f808c294b",
    "readOnly": true,
    "resources": [
        {
            "accountId": "${AWS_ACCOUNT_ID}",
            "type": "AWS::IAM::Role",
            "ARN": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/test-github-oidc-role"
        }
    ],
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "recipientAccountId": "${AWS_ACCOUNT_ID}",
    "eventCategory": "Management",
    "tlsDetails": {
        "tlsVersion": "TLSv1.2",
        "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
        "clientProvidedHostHeader": "sts.ap-northeast-1.amazonaws.com"
    }
}

必要な信頼関係

実は上述のドキュメントなどでしっかり記載されているのですが、実際に必要な内容は下記のようになります

必要な信頼関係

{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:${GitHubOrg}/${RepositoryName}:*"
        }
      }
    }
  ]
}

具体的には、Condition.StringLikeの箇所が異なっています。

マネジメントコンソールからそのまま作成していくとtoken.actions.githubusercontent.com:audが入りますが、必要な内容はtoken.actions.githubusercontent.com:subであり、ここにはWorkflowでIAMロールを利用したいGitHubリポジトリとジョブ環境名が入ってくることになります。

GitHub OIDCプロバイダーが発行するアクセストークンについて

この点については、GitHubの公式ドキュメントでも言及されています。デフォルトでは検証にaudience(aud)しか含まれないので、手動でsubject(sub)を追加する必要がある、とのことです。

By default, the validation only includes the audience (aud) condition, so you must manually add a subject (sub) condition. Edit the trust relationship to add the sub field to the validation conditions.

OIDCを用いた認証に馴染みのない方はとっつきにくいかもしれませんが、GitHubとAWS(Cloudプロバイダー)間で信頼関係を結んだ上で、発行されたトークンをやりとりすることで、認証認可の仕組みを実現しています。

OIDCプロバイダー概要図
GitHub公式ドキュメントより引用

GitHub ActionsではWorkflowごとにJson Web Token(JWT)を発行するのですが、このトークンの形式と内容に基づいてAWSのIAM IDプロバイダー側では信頼関係を検証するわけです。

GitHub ActionsのWorkflowから発行されるJWT

{
  "typ": "JWT",
  "alg": "RS256",
  "x5t": "example-thumbprint",
  "kid": "example-key-id"
}
{
  "jti": "example-id",
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "environment": "prod",
  "aud": "https://github.com/octo-org",
  "ref": "refs/heads/main",
  "sha": "example-sha",
  "repository": "octo-org/octo-repo",
  "repository_owner": "octo-org",
  "run_id": "example-run-id",
  "run_number": "10",
  "run_attempt": "2",
  "actor": "octocat",
  "workflow": "example-workflow",
  "head_ref": "",
  "base_ref": "",
  "event_name": "workflow_dispatch",
  "ref_type": "branch",
  "job_workflow_ref": "octo-org/octo-automation/.github/workflows/oidc.yml@refs/heads/main",
  "iss": "https://token.actions.githubusercontent.com",
  "nbf": 1632492967,
  "exp": 1632493867,
  "iat": 1632493567
}

JWTの仕様として、subject(sub)には発行者のローカルもしくはグローバルで一意の値となることが求められます。

GitHub OIDCプロバイダーでは、subject(sub)として組織とリポジトリとジョブ環境名が入り、一意となるようです。

The following example OIDC token uses a subject (sub) that references a job environment named prod in the octo-org/octo-repo repository.

IAMロールの信頼関係にはConditionとしてtoken.actions.githubusercontent.com:subを指定する

上述の通り、マネジメントコンソールからIAMロールを作成すると、信頼関係の検証条件としてtoken.actions.githubusercontent.com:audを指定してしまうことになります。

そこで、信頼関係を手動で編集しtoken.actions.githubusercontent.com:subに変更し、IAMロールを利用したいリポジトリ名を指定することで、正しい設定にすることができます。ワイルドカードが利用できるため、リポジトリまで指定してジョブ名は指定しないといった設定が可能です。

信頼関係を変更した後、GitHub ActionsのWorkflowを再度実行してみると、今度は成功していました。やったぜ。

手順で用意されている通りCloudFormationを実行すれば発生しなかった事象ですが、おかげでOIDCの仕組みについて少し詳しくなれた気がします。

IAMに限らず、もし同じようにマネジメントコンソールからリソースを作成する際は、用意されているCloudFormationテンプレートと内容を見比べてみてください。思わぬ落とし穴があるかもしれません。

以上、コンサル部のとばち(@toda_kk)でした。