Docs · Glidepath
ADR-0027: The provenance gate accepts a GitHub PR merge; the signing policy is admin-owned
Status: Accepted (2026-10-20). Built: the GitHub-merge path in verify-commit-signature.yaml, the
releaseSigning catalog values and release-signing-policy ConfigMap, the tighten-only
governance.githubMerge in cicd.yaml, gpgv and GitHub’s web-flow keyring in the toolbox, the weekly
key check. Not built: the Tower Release Record line naming the signature path (the Task’s results carry
it). Detail: commit-signing.md.
Context
The provenance gate verified the promoted app commit only with gitsign verify. Every commit GitHub
makes when it merges a PR (merge commit, squash or rebase) is signed by GitHub’s web-flow OpenPGP key
instead, so every GitHub-merged release failed with unsupported signature type: not a PEM block.
Measured over the last release PR of every gitops-* repo on 2026-10-20: 10 failures, 2 passes, both
passes gitsign-signed commits pushed straight to main. gitops-backstage#49 and #52 were merged past
the red gate. A gate that is red on almost every release teaches people to merge through it.
The signer list (governance.allowedCommitSigners) lived in the app’s own cicd.yaml, which the team
being gated can edit. The same team could add itself to the list that gates its release.
Decision
- Two paths. gitsign stays first and is final for any non-OpenPGP signature. An OpenPGP-signed
commit passes only on the GitHub-merge path, when the app’s policy enables it and every check holds:
GitHub’s pinned web-flow key, committer GitHub, the
merge_commit_shaof a merged PR into the default branch, on that branch (a merge commit’s second parent is the PR head), merged by a login inallowedMergers. Optional:mode: strict(every PR commit gitsign-signed by an allowed signer) andrequiredApprovals: N(approvals of the final head by other allowed mergers). - Off by default;
mergermode by default when on; 0 approvals by default. Strict would fail every PR whose commits are written where gitsign’s browser login cannot run. One approval proves nothing while one person holds both accounts. - Admin-owned policy. Signers, the path’s switch, its mode, mergers and minimum approvals are the
catalog chart’s
releaseSigningvalues, set per cluster in the cluster repo. A product owner asks; a platform admin merges.cicd.yamlkeeps only tighten-only knobs (governance.githubMerge.mode: strict, a higherrequiredApprovals); the stricter value wins.allowedCommitSignersincicd.yamlis accepted and ignored. - Keys are pinned, never fetched at verify time. The keyring is checked in and baked into the toolbox with a fingerprint assert; the Task pins the same fingerprints. A weekly workflow warns when GitHub publishes a different set.
- Approvals are checked by the gate, not left to a ruleset. Rulesets are not enforced on private repos on GitHub Free, can be bypassed by repo admins, and leave no record on the release. Keep them where they are enforced; the gate’s check holds on every plan and records the approvers.
release-on-taggets the same rule at no extra cost: its release PR names the tagged commit and runs the same Task.
Consequences
- GitHub-merged releases can pass, so a red
provenancemeans something again. - The trust for those releases moves from a person’s key to GitHub’s key plus GitHub’s record of who merged. Non-repudiation is weaker: a stolen GitHub session can merge. Strict mode and approvals narrow it. security.md lists it as an accepted gap, per app, decided by the admin who enables it.
- A GitHub key rotation fails GitHub-merged releases closed until three files change and the toolbox is rebuilt; the weekly check gives warning.
- The policy ConfigMap is readable by every ServiceAccount (emails and logins, not secrets), the same scope as the catalog’s Task definitions.
- Each app’s signer list now has to be in the cluster’s catalog values; an app missing from them falls back to the cluster default, and an empty default fails gitsign closed, as before.