Background
Reference: https://zenn.dev/aws_japan/articles/b311c3710826a6
When a workflow generates a GitHub App installation access token by storing the private key in GitHub Secrets and passing it to actions/create-github-app-token, the private key is at risk of leaking. The private key never expires, so anyone who obtains it can generate access tokens indefinitely.
Registering the private key in AWS KMS and delegating only the JWT signing to KMS removes that leak path entirely.
Overview
- AWS setup (once per GitHub App) → references/aws-setup.md
- Workflow changes (every time, for each workflow that generates a token) → this file
Check whether the AWS setup is already done
The setup is complete for a GitHub App when all of the following exist:
- The GitHub App private key has been imported into a KMS key
- An IAM role that GitHub Actions can assume via OIDC
- GitHub Variables
ROLE_TO_ASSUME,KMS_KEY_ID, and one identifying the app (APP_CLIENT_IDorAPP_ID)
If any of these are missing, or this is the first time using this approach for the GitHub App, read and follow references/aws-setup.md first. Skip that file when the setup is already done.
Note that if the workflow lives in a repository that hasn't used this setup before, the repository must be added to the sub condition of the IAM role's trust policy even when the rest of the setup is done (see step 2 of aws-setup.md).
Requirements
- pinact (used to pin actions)
1. Add id-token: write to the job's permissions
jobs:
<job name>:
permissions:
id-token: write # Required. Add this
# Any other permissions the job needs
contents: read
Adding a permissions: block to a job discards all of its default permissions.
Writing only id-token: write breaks existing steps such as checkout, so re-declare every permission the job needs.
2. Replace the token generation step
Before:
- uses: actions/create-github-app-token@v3
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
permission-contents: read
repositories: |
aqua-registry
After:
- uses: suzuki-shunsuke/create-github-app-token-aws-kms@91a3afd26b06729357ac310a02b658f0e3910ba9 # v0.0.2
id: app-token
with:
client-id: ${{ vars.APP_CLIENT_ID }}
kms-key-id: ${{ vars.KMS_KEY_ID }}
role-to-assume: ${{ vars.ROLE_TO_ASSUME }}
aws-region: ap-northeast-1
permission-contents: read
repositories: |
aqua-registry
suzuki-shunsuke/create-github-app-token-aws-kms follows the inputs and outputs of actions/create-github-app-token, so only three things change:
private-keybecomeskms-key-id. Drop thesecrets.APP_PRIVATE_KEYreference.role-to-assumeis added. The action assumes the IAM role itself with the GitHub OIDC token, so noaws-actions/configure-aws-credentialsstep is needed.- At least one
permission-*input is required, unlikeactions/create-github-app-tokenwhich grants every permission the app holds when none is given.
client-id and app-id both work, so keep whichever the workflow already passes.
repositories, owner, enterprise, and the outputs are unchanged, and the token is still revoked when the job ends.
The region
aws-region can be dropped when kms-key-id is a key ARN, which carries its own region, or when AWS_REGION is already set for the job.
Otherwise set it, or the action fails because nothing says where the key is.
3. Pin the action
Don't copy the SHA and version comment above verbatim. Check the latest release and pin to it.
pinact run -u -i suzuki-shunsuke/create-github-app-token-aws-kms
4. Verify
Run the workflow and confirm that the token generation step and the subsequent steps that use the token both succeed.
Gotchas
Don't add configure-aws-credentials alongside role-to-assume
The AWS session the action creates stays inside the action and is never exported, so the rest of the job can't use it to sign.
aws-actions/configure-aws-credentials gives that up: it exports the credentials as environment variables, or writes them to ~/.aws/credentials when aws-profile is set, where every later step can read them.
Reach for it only when you need an option this action doesn't offer, such as an external ID, a session policy, or a custom STS endpoint.
Leave role-to-assume unset in that case, and the action reads the credentials from AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN, which is what aws-actions/configure-aws-credentials exports.
Those environment variables are the only other source, so leave aws-profile unset: a ~/.aws/credentials profile isn't read, and neither is IMDS on a self-hosted EC2 runner nor the credentials of an ECS or EKS task.
The session is 900 seconds and doesn't need sizing
The action assumes the role for 900 seconds, the shortest AWS STS accepts, and each token generation step assumes it again. Nothing has to be sized to the job: once the installation access token exists, the AWS session is no longer needed no matter how long the remaining steps take.