Summary
The org's workflow authentication grew two overlapping, inconsistently named GitHub-App
identities, one of whose private-key secret was never provisioned. That gap is why the
notify org Pages job (and every dependabot-automerge and scorecard caller) mints an
empty key and fails. The same broad CI identity is also used for narrow jobs (Pages
writes, auto-merge), which is more authority than those jobs need.
This epic splits the auth into five least-privilege Apps under one consistent naming
scheme, adds a central manifest as the source of truth with a CI validation gate, and
repoints every workflow.
The five Apps
| App |
Purpose |
Repository permissions |
client |
General CI cross-repo identity; primary consumer OpenSSF Scorecard |
metadata:read, contents:read, administration:read, pull_requests:read, issues:read, actions:read, checks:read |
catalog |
Marketplace catalog updates (plugin-catalog-update-hub) |
metadata:read, contents:write, actions:write, pull_requests:write |
pages |
Cross-repo org Pages deploy/notify |
metadata:read, contents:write, pages:write, actions:write |
automerge |
Dependabot auto-approve and merge |
metadata:read, contents:write, pull_requests:write |
release |
gh release / contents auth in release.yml (OIDC attestation untouched) |
metadata:read, contents:write, packages:write where publishing |
Naming scheme
Each App: its OAuth client ID (Iv23...) in an org variable <ROLE>_CLIENT_APP_ID, private
key in an org secret <ROLE>_CLIENT_APP_PRIVATE_KEY, both visibility all. Tokens are minted
with the client-id: field, per GitHub's current best practice (the legacy app-id is
deprecated and emits a "Use client-id instead" warning). The legacy MIF_CI_CLIENT_APP_* and
CATALOG_UPDATER_APP_* names are retired.
Central pieces (this repo)
auth/apps.yml manifest: App to permissions to repo scope to consuming workflows.
auth/apps.schema.json plus a reusable validation workflow run in CI, so the manifest
cannot drift from reality silently.
actions/mint-app-token composite action for minting. It runs in the caller's job so
the token never crosses a job boundary. Minting is deliberately a composite action, not
a reusable workflow: a reusable workflow that outputs a token leaks it past the job
barrier because reusable-workflow outputs are not masked downstream. The ADR records
this.
- ADR documenting the decision.
- Badge ribbon: per-App badges on the org profile README, an auth-model summary badge on
this repo's README.
release.yml scope
Only the authenticated gh release and contents-write steps move to the release App.
The id-token: write and attestations: write keyless OIDC attestation stays exactly as
is, so the attestation signer SAN remains the workflow and gh attestation verify is
unchanged. Each release is re-verified after the swap.
Children
Eight child issues, one per affected repo. Draft PRs land first and stay red until the
Apps and their secrets exist; they go green once provisioning is complete.
Provisioning handoff
Apps are created by the maintainer. Once each <ROLE>_CLIENT_APP_ID variable and
<ROLE>_CLIENT_APP_PRIVATE_KEY secret is set, the draft PRs are marked ready, CI goes green, and
every release is re-verified to still attest and verify.
Summary
The org's workflow authentication grew two overlapping, inconsistently named GitHub-App
identities, one of whose private-key secret was never provisioned. That gap is why the
notify org Pagesjob (and every dependabot-automerge and scorecard caller) mints anempty key and fails. The same broad CI identity is also used for narrow jobs (Pages
writes, auto-merge), which is more authority than those jobs need.
This epic splits the auth into five least-privilege Apps under one consistent naming
scheme, adds a central manifest as the source of truth with a CI validation gate, and
repoints every workflow.
The five Apps
clientmetadata:read,contents:read,administration:read,pull_requests:read,issues:read,actions:read,checks:readcatalogplugin-catalog-update-hub)metadata:read,contents:write,actions:write,pull_requests:writepagesmetadata:read,contents:write,pages:write,actions:writeautomergemetadata:read,contents:write,pull_requests:writereleasegh release/ contents auth inrelease.yml(OIDC attestation untouched)metadata:read,contents:write,packages:writewhere publishingNaming scheme
Each App: its OAuth client ID (
Iv23...) in an org variable<ROLE>_CLIENT_APP_ID, privatekey in an org secret
<ROLE>_CLIENT_APP_PRIVATE_KEY, both visibilityall. Tokens are mintedwith the
client-id:field, per GitHub's current best practice (the legacyapp-idisdeprecated and emits a "Use client-id instead" warning). The legacy
MIF_CI_CLIENT_APP_*andCATALOG_UPDATER_APP_*names are retired.Central pieces (this repo)
auth/apps.ymlmanifest: App to permissions to repo scope to consuming workflows.auth/apps.schema.jsonplus a reusable validation workflow run in CI, so the manifestcannot drift from reality silently.
actions/mint-app-tokencomposite action for minting. It runs in the caller's job sothe token never crosses a job boundary. Minting is deliberately a composite action, not
a reusable workflow: a reusable workflow that outputs a token leaks it past the job
barrier because reusable-workflow outputs are not masked downstream. The ADR records
this.
this repo's README.
release.yml scope
Only the authenticated
gh releaseand contents-write steps move to thereleaseApp.The
id-token: writeandattestations: writekeyless OIDC attestation stays exactly asis, so the attestation signer SAN remains the workflow and
gh attestation verifyisunchanged. Each release is re-verified after the swap.
Children
Eight child issues, one per affected repo. Draft PRs land first and stay red until the
Apps and their secrets exist; they go green once provisioning is complete.
Provisioning handoff
Apps are created by the maintainer. Once each
<ROLE>_CLIENT_APP_IDvariable and<ROLE>_CLIENT_APP_PRIVATE_KEYsecret is set, the draft PRs are marked ready, CI goes green, andevery release is re-verified to still attest and verify.