Skip to content

Refactor org GitHub-App auth into 5 least-privilege apps + central manifest #37

Description

@zircote

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ci-cdCI/CD pipelines, GitHub Actions, or deployment automationeffort: largeSignificant effort (> 2 days or requires breakdown)priority: highShould be addressed in current sprint or iterationtype: epicLarge body of work spanning multiple related issues or PRstype: securitySecurity vulnerability or hardening

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions