# Github App Private Key AWS Kms

> Rewrites GitHub Actions workflows so that GitHub App installation access tokens are generated by delegating JWT signing to AWS KMS instead of storing the private key in GitHub Secrets. Prevents the private key from leaking. Use when migrating from actions/create-github-app-token to suzuki-shunsuke/create-github-app-token-aws-kms, when registering a GitHub App private key in KMS, or when setting up OIDC so GitHub Actions can assume an AWS IAM role.

- Skill: `suzuki-shunsuke/github-app-private-key-aws-kms` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add suzuki-shunsuke/github-app-private-key-aws-kms`
- Raw SKILL.md: https://api.skillmd.com/api/skills/suzuki-shunsuke/github-app-private-key-aws-kms/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: suzuki-shunsuke (https://skillmd.com/u/suzuki-shunsuke)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/suzuki-shunsuke/github-app-private-key-aws-kms

---


## 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](https://github.com/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

1. **AWS setup** (once per GitHub App) → [references/aws-setup.md](references/aws-setup.md)
2. **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_ID` or `APP_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](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](https://github.com/suzuki-shunsuke/pinact) (used to pin actions)

## 1. Add `id-token: write` to the job's permissions

```yaml
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:

```yaml
- 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:

```yaml
- 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](https://github.com/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-key` becomes `kms-key-id`. Drop the `secrets.APP_PRIVATE_KEY` reference.
- `role-to-assume` is added. The action assumes the IAM role itself with the GitHub OIDC token, so no `aws-actions/configure-aws-credentials` step is needed.
- At least one `permission-*` input is required, unlike `actions/create-github-app-token` which 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.

```sh
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.

