Skip to content

SRE-924: Sign the Renovate App JWT in Vault - #95

Merged
TimDiekmann merged 10 commits into
mainfrom
t/sre-924-sign-the-github-app-jwt-with-vault-transit-instead-of
Aug 6, 2026
Merged

SRE-924: Sign the Renovate App JWT in Vault#95
TimDiekmann merged 10 commits into
mainfrom
t/sre-924-sign-the-github-app-jwt-with-vault-transit-instead-of

Conversation

@TimDiekmann

Copy link
Copy Markdown
Member

Renovate authenticated as a GitHub App whose private key was a Vault secret the
job read into the runner. A composite action now has Vault's transit engine sign
the App JWT with a non-exportable key, and the installation token it exchanges
that for is scoped to the calling repository.

The $/ self-repository syntax went generally available on 2026-07-30, which
retires the TODO in this workflow: the OIDC step existed only to work out which
ref to check the Renovate lockfile out from. The lockfile now lives beside the
action that installs it, so the checkout goes away too, and lint.yml shares the
same pin instead of running its own npm ci.

Shell in this repo now needs a linter, hence the shellcheck job.

Note that actionlint 1.7.12 does not know $/ yet and reports "ref is missing".
No workflow here runs it.

The App's private key was a Vault secret any `dev`-role job could read, which
is how the 2026-08-04 compromise reached it. A composite action now has Vault
sign the App JWT instead, so the key never lands on the runner.

The same action carries the Renovate pin, and `$/` resolves both at the
running commit. That replaces the OIDC dance that existed only to work out
which ref to check the lockfile out from, and the checkout with it.

Shell in this repo now needs a linter, so lint.yml grew a shellcheck job.
The role is the per-caller boundary, so it cannot be baked into an action that
more than one workflow uses.
A second App means the caller has to name which one it signs as, and the key and
role have to agree with it.
Copilot AI lite review requested due to automatic review settings August 5, 2026 16:40
@cursor

cursor Bot commented Aug 5, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches CI authentication for Renovate and adds a mandatory caller secret; Vault/GitHub misconfiguration could break dependency PRs, though keeping the signing key off runners reduces exposure.

Overview
Renovate no longer reads the GitHub App private key onto the runner. A new composite action logs into staging Vault (JWT + Cloudflare Access), uses transit to sign the App JWT, and exchanges it for a repository-scoped installation token. The centralized dependencies workflow removes the OIDC ref hack and sparse lockfile checkout in favor of $/ github-app-token and install-renovate.

Reusable workflow callers must supply CF_ACCESS_STAGE_CLIENT_SECRET; token minting is tied to ci-renovate, transit key github-app-hash-dependencies, and a fixed app id.

Lint adds shellcheck for tracked *.sh files and shares install-renovate instead of a separate npm ci. PR title checks exempt hash-dependencies[bot] and hash-release[bot] (replacing hash-worker[bot]).

Reviewed by Cursor Bugbot for commit c182b67. Bugbot is set up for automated code reviews on this repo. Configure here.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the centralized Renovate reusable workflow to mint GitHub App installation tokens by having Vault Transit sign the App JWT (so the private key never reaches the runner), and refactors Renovate installation into a pinned composite action using the new $/ self-repo syntax.

Changes:

  • Replace “read GitHub App private key from Vault secret” with a composite action that uses Vault Transit to sign the App JWT and exchanges it for a repo-scoped installation token.
  • Introduce a composite install-renovate action that installs a centrally pinned Renovate version and puts it on PATH, removing per-workflow npm ci setup.
  • Add a shellcheck lint job for repository shell scripts.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
.github/workflows/lint.yml Adds shell linting and switches Renovate validation to the shared install action.
.github/workflows/housekeeping-dependencies.yml Removes OIDC ref/checkout workaround; uses the new Vault-signed GitHub App token action and shared Renovate installer.
.github/actions/install-renovate/action.yml New composite action that sets up Node and installs Renovate from a centrally pinned lockfile.
.github/actions/install-renovate/package.json Defines Renovate as the pinned dependency for the installer action.
.github/actions/install-renovate/package-lock.json Locks the Renovate dependency tree used by the installer action.
.github/actions/github-app-token/action.yml New composite action to authenticate to Vault and mint a repo-scoped GitHub App installation token.
.github/actions/github-app-token/token.sh Implements JWT construction, Vault Transit signing, and GitHub installation token exchange.
Suppressed comments (2)

.github/actions/github-app-token/action.yml:60

  • inputs.repository may be empty (and the input default cannot safely reference contexts). Use an expression fallback here so the action scopes to the current repo when callers don't provide repository.
        TRANSIT_KEY: ${{ inputs.transit-key }}
        APP_ID: ${{ inputs.app-id }}
        REPOSITORY: ${{ inputs.repository }}

.github/actions/github-app-token/token.sh:46

  • Similarly, fail fast if the installation lookup or access token response doesn't contain the expected fields; otherwise jq -r can produce null and the script will continue with invalid values.
# Looked up rather than configured, so a new caller needs no extra input.
installation=$(github_api "https://api.github.com/repos/${REPOSITORY}/installation" | jq --raw-output '.id')
token=$(github_api --request POST \
    --data "$(jq --null-input --arg repo "${REPOSITORY#*/}" '{repositories: [$repo]}')" \
    "https://api.github.com/app/installations/${installation}/access_tokens" | jq --raw-output '.token')

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread .github/actions/github-app-token/token.sh Outdated
Comment thread .github/actions/github-app-token/action.yml
Comment thread .github/actions/github-app-token/token.sh

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6784697. Configure here.

Comment thread .github/workflows/housekeeping-dependencies.yml
A 200 without the expected field made `jq --raw-output` print `null` and exit
zero, so a missing signature surfaced later as an unexplained 401.

`repository` loses its expression default. Whether action input defaults evaluate
expressions is undocumented, and a caller naming the repository costs one line.
Copilot AI review requested due to automatic review settings August 5, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated no new comments.

Declaring it required would have broken every caller on merge: SRE-901 removed
`secrets: inherit`, and a caller cannot pass a secret the called workflow does
not declare yet, so there is no order in which required lands cleanly.
Copilot AI review requested due to automatic review settings August 5, 2026 17:27
Reverts the previous commit's required: false. The workflow cannot reach Vault
without it, so the declaration says so and the nine callers pass it.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

.github/workflows/housekeeping-dependencies.yml:77

  • This secret is declared optional, but the workflow always passes it into the github-app-token action, whose script hard-requires CF_ACCESS_CLIENT_SECRET. Callers that have not been updated to pass this secret will still be able to dispatch the workflow (because required: false) but will then fail at runtime. If this workflow cannot function without it, it should be marked required so callers fail at dispatch time as intended.
  contents: read

Comment thread .github/actions/github-app-token/token.sh
Copilot AI review requested due to automatic review settings August 5, 2026 17:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.github/actions/github-app-token/token.sh:17

  • The App JWT payload encodes iss as a JSON string ("iss":"${APP_ID}"). GitHub App JWTs expect iss to be the numeric App ID, and string-encoding can cause authentication failures when exchanging for an installation token.
header=$(printf '%s' '{"alg":"RS256","typ":"JWT"}' | base64url)
payload=$(printf '{"iss":"%s","iat":%d,"exp":%d}' "${APP_ID}" "$((now - 60))" "$((now + 540))" | base64url)
signing_input="${header}.${payload}"

Copilot AI review requested due to automatic review settings August 5, 2026 17:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated no new comments.

Suppressed comments (2)

.github/actions/github-app-token/token.sh:28

  • The Vault transit API request body is JSON, but the curl call doesn’t set a JSON Content-Type. Setting it explicitly avoids relying on server-side content sniffing and prevents regressions if Vault becomes stricter about request headers.
signature=$(curl --silent --show-error --fail-with-body --request POST \
    --header "X-Vault-Token: ${VAULT_TOKEN}" \
    --header "CF-Access-Client-Id: ${CF_ACCESS_CLIENT_ID}" \
    --header "CF-Access-Client-Secret: ${CF_ACCESS_CLIENT_SECRET}" \
    --data "${request}" \

.github/actions/github-app-token/token.sh:48

  • This GitHub API call sends a JSON body via --data, but no Content-Type: application/json header is set. Adding it makes the request unambiguous and avoids relying on GitHub accepting JSON with curl’s default form content type.
token=$(github_api --request POST \
    --data "$(jq --null-input --arg repo "${REPOSITORY#*/}" '{repositories: [$repo]}')" \
    "https://api.github.com/app/installations/${installation}/access_tokens" | jq --exit-status --raw-output '.token')

Vault parses the body either way, so this is not a fix; it matches what the
Python client already sends and says what the body is.
Copilot AI review requested due to automatic review settings August 5, 2026 18:48
@TimDiekmann
TimDiekmann requested a review from a team August 5, 2026 18:51

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 7 changed files in this pull request and generated no new comments.

indietyp
indietyp previously approved these changes Aug 6, 2026
Renovate shared an App with the release flow, so one key covered both. It now
signs as hash-dependencies, and the title check exempts that account and the one
changesets opens the release PR as.
Copilot AI review requested due to automatic review settings August 6, 2026 10:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (2)

.github/workflows/preflight-pr-title.yml:10

  • The exemption list in the workflow if: includes hash-dependencies[bot] and hash-release[bot], but this comment doesn’t name them, making it unclear which accounts are exempt (and why). Consider naming the exact logins to keep the documentation accurate and easy to audit.
# Exempt are `dependabot[bot]`, the account that opens renovate PRs, and the one
# changesets opens the release PR as. Any other account — including other bots
# and GitHub Apps such as Claude — must reference a Linear issue ID.

.github/actions/github-app-token/token.sh:49

  • This POST to the GitHub REST API sends a JSON body, but no Content-Type: application/json header is set. curl --data defaults to application/x-www-form-urlencoded, which can cause the API to reject the payload as invalid JSON.
token=$(github_api --request POST \
    --data "$(jq --null-input --arg repo "${REPOSITORY#*/}" '{repositories: [$repo]}')" \
    "https://api.github.com/app/installations/${installation}/access_tokens" | jq --exit-status --raw-output '.token')

Copilot AI review requested due to automatic review settings August 6, 2026 10:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.

@TimDiekmann
TimDiekmann added this pull request to the merge queue Aug 6, 2026
Merged via the queue into main with commit 89ff06b Aug 6, 2026
11 checks passed
@TimDiekmann
TimDiekmann deleted the t/sre-924-sign-the-github-app-jwt-with-vault-transit-instead-of branch August 6, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants