Docs · Glidepath
Release guardrails: adding and removing a gate
The release stage’s governance checks (docs/release.md) are the platform’s release
guardrails - independent, required GitHub Checks on every gitops-repo release PR.
This doc covers the mechanism that makes a gate a one-file (or two-file) addition rather
than a change scattered across half a dozen places, and the checklist for actually doing
that. See governance-stubs.md for how a stub stays honestly a
stub, and ADR-0003 for why stubs are structurally loud
rather than a silent exit 0 - this doc is the “how to add one” companion to that
decision, not a new one.
The single source of truth
charts/glidepath-catalog/values.yaml’s releaseGuardrails list is the canonical
registry of every gate enforced on a release PR - name, a human description,
status (real or stub, cosmetic only - see below), class (content, process,
integrity, approval), optional targets, and required (default true; see
“Required and advisory gates” below). The chart resolves the list once
(_helpers.tpl, glidepath-catalog.guardrails: required defaulted, a cluster’s
releaseGuardrailOverrides applied) and every consumer renders off that instead of
hardcoding the gate list (a gate advisory for one app only, releaseGuardrailOverridesByApp, is
applied on top at runtime - “One app only” below):
- the gate Pipelines (
sast-check,image-scan-check,sbom-check,qa-check,values-check) - whether the hard task renders at all; catalog/tasks/detect-bypass-merge.yaml- the break-glass Slack alert’s “did every required check actually pass” loop;catalog/tasks/release-log-emit.yaml- every gate’s conclusion in the release log,bypassfrom the required ones;catalog/tasks/wait-for-release-guardrails.yaml- the sibling set a shape-B gate waits on;catalog/tasks/open-release-pr.yaml- the release PR body’s governance-checks table;hack/apply-guardrail-ruleset.sh- the GitHub ruleset’s required status checks (offline, with-ffor a cluster’s values).
Everything else about a gate - which Pipeline it runs, what it actually checks, whether
it posts a PR comment - is standard Tekton/PaC config, not driven by this list. Adding a
name to releaseGuardrails alone does nothing; the gate exists once its own
.tekton/pull-request-<name>.yaml onboarding template and Pipeline exist too (below).
Branch protection (which checks are actually required to merge) is a separate, manual
GitHub setting - see “Branch protection” below.
Two shapes, depending on what the gate needs
A. Independent stub or real check (the common case) - most gates (sast, image-scan,
provenance, sbom, itsm, qa, policy-validation) run in parallel off the same PR
webhook, with no dependency on each other’s outcome. (itsm, since 2026-10-19, is a third shape:
its Pipeline itsm-check renders as the stub while the catalog names no change tool and as the real
gate once itsm.provider is set, so the onboarding template never changes, see
change-management.md.)
- Stub, fully generic (least effort): add one onboarding template file,
charts/glidepath-app/files/onboarding-templates/gitops-repo/pull-request-<name>.yaml, pointing at the existing sharedgovernance-checkPipeline withgate-name: <name>(pull-request-policy-validation.yamlbefore 2026-10-26, in git history, is the shape). That Pipeline already callsgovernance-gate-stub(loud “no real check implemented yet” logging + agovernance.stub=truespan + a"stub"result, never a silent pass) and posts a PR comment. No new Pipeline or Task needed - this is the whole change, plus the registry entry above. Good default when the gate’s eventual real shape isn’t known yet. - Stub, dedicated (scaffolded for graduation): if you already know roughly what the
gate will eventually need to check, give it its own Pipeline+Task now, still calling
governance-stubunder the hood.qawas built this way and graduated on 2026-10-19:qa-check.yaml/qa-gate.yamlnow show the real shape, and git history (before that date) the scaffold. Same honest stub behavior andstatus: stubas the fully-generic case (this is NOT the “Real” case below - no stub marking gets removed), but graduating later is then a swap of the Task’s own step body, instead of writing a new Pipeline+Task and repointing the onboarding template all at once. - Real: once real logic exists, give the gate its own dedicated Pipeline if it
doesn’t already have one (see
sbom-check.yamlfor the shape: one or more real Tasks, afinally: comment-prblock identical to every other gate’s), and repoint that gate’s onboarding template file at it instead ofgovernance-check. Flip itsreleaseGuardrailsentry tostatus: real. Nothing about the onboarding template file’s shape changes, only thepipelineRefname and droppinggate-name(seepull-request-sbom.yamlvs. this doc’s stub example for the before/after). If the gate was already scaffolded dedicated-stub-style, this step shrinks further: just replace the Task’s stub step with real logic, keeping its name/params/results contract the same - no Pipeline or onboarding-template change at all.policy-validationgraduated this way from the generic stub on 2026-10-26 (ADR-0028): its ownpolicy-validation-checkPipeline andpolicy-validation-gateTask,class: integritylikevalues(so no rollback-advisory copy), see policy-validation.md.
B. A gate that depends on the others (none today) - Tekton has no cross-PipelineRun
runAfter (every gate is its own independently PaC-triggered PipelineRun off the same
webhook, run in parallel - see governance-check.yaml’s own header), so a gate that must
run after every other gate succeeds needs its own dedicated Pipeline that waits for the
rest. catalog/tasks/wait-for-release-guardrails.yaml does that by polling the GitHub
Checks API (Helm-rendered off releaseGuardrails, minus the gate named in self-gate).
image-promotion was the one such gate until 2026-10-09, when it was retired as a stub
check (ADR-0025: it could not block a
merge on this GitHub plan, its result was the AND of its siblings, and there is no upper
registry to promote into). That ADR also records how a real promotion should be ordered:
event-driven fan-in on the release record rather than a polling pod. It came back that way with the Flight registry
(ADR-0029 phase 3, registries.md):
with catalog promotion.enabled, image-promotion is a required entry of the effective registry (not of
releaseGuardrails, and no .tekton file), started by the last gate’s finally instead of a waiting Pipeline.
Timeouts: always split tasks/finally, never a single pipeline budget alone
Found live 2026-08-23: every onboarding template originally set only
timeouts.pipeline (e.g. 10m) - a single shared budget for the real check tasks and
the finally: comment-pr step that reports the outcome. A check that ran merely slow
(not wrong) could burn the whole budget, leaving comment-pr no time to even start -
confirmed live: its pod sat Pending/"containers with incomplete status" for minutes,
then got TaskRunCancelled the instant the PipelineRun’s own timeout hit, so the PR
never got a comment even though the underlying check itself had already produced a real
(if slow, or in one case never-finished) result.
Every onboarding template now sets timeouts.tasks and timeouts.finally explicitly
(not just pipeline) so finally/comment-pr always gets its own reserved window,
regardless of how long the real check tasks take:
timeouts:
tasks: "14m" # the check task(s) AND the finally block - see below
finally: "4m" # reserved, separate budget for comment-pr - never starved
pipeline: "18m" # must be >= tasks + finally
tasks must cover finally too (found live 2026-10-29). Tekton (v1.15) checks timeouts.tasks
against the time since the run started, including the time finally takes, when it computes the
final status. A run whose tasks all succeeded inside the budget still ends PipelineRunTimeout, its
check run red, when its finally finishes past startTime + tasks (sbom-rntvj on
gitops-backstage#159: every task and comment-pr succeeded, message “tasks failed to finish within
8m0s”). So size tasks as what the check needs plus the finally budget, and pipeline as
tasks + finally. The 2026-10-29 defaults: 14m/4m/18m (sast, sbom, itsm, qa, api-compat,
policy-validation), 19m/4m/23m (image-scan), 17m/4m/21m (provenance), 12m/4m/16m (values), each the
old tasks plus 4m for finally plus 2m for a loaded cluster.
A new gate’s onboarding template must set all three, not just pipeline - copy the
pattern from pull-request-sast.yaml or, if the real check genuinely needs more room (see
pull-request-image-scan.yaml for why Trivy scans got more), size tasks to what the check
actually needs plus finally. A shape-B gate (one that waits on its siblings) is the exception: its
tasks must out-wait wait-for-release-guardrails’ own timeout, which in turn must
out-wait the slowest sibling’s PipelineRun timeout. image-promotion carried those
larger numbers until ADR-0025 retired it.
Already-onboarded gitops repos keep their old .tekton timeouts until their next onboarding
resync (a cicd.yaml push to the app repo).
With promotion on, the promotion fan-in never records a success the run will not keep: a green run
already past its tasks deadline goes into the gate ledger as a failure, and one within a minute of
it records nothing and does not ask (registries.md).
Required and advisory gates (required, 2026-10-19)
Every registry entry carries required (default true). A real gate with required: false is
advisory: its Pipeline renders only the -advisory copy of its check (onError: continue,
the same copy every content gate already runs on an eligible rollback, ADR-0021 decision 8), so
the check run is green whatever the check finds, and comment-pr-check-result posts what it
found with the “advisory until the gate is required” wording (advisory-reason: not-required).
Everything downstream agrees: detect-bypass-merge and release-log-emit do not count a merge
past it as a bypass, wait-for-release-guardrails does not wait on it, the release PR body’s
table says “real, advisory”, and hack/apply-guardrail-ruleset.sh leaves it out of the required
status checks. The gate still runs and still reports on every release PR: advisory is not off.
Two gates refuse it. sast and provenance are the image’s integrity, and advising on them is
the “advises rather than enforces” weakness the SDLC review named; required: false on either
fails the chart render with a message naming the gate, so no cluster can ship such a catalog. A
stub ignores the field (its check is always green; it stays in every required set so a missing
check run is still noticed).
Who sets it. The default is the registry entry in this chart (a glidepath PR, platform
maintainers, CI runs gate_required_test.sh). One cluster changes it in its catalog Application
values through the releaseGuardrailOverrides map, never by restating releaseGuardrails (a
list is replaced whole on override, so a gate added upstream would silently vanish there):
releaseGuardrailOverrides:
qa: { required: true }
It is not in cicd.yaml and not a Tower setting: no app team or release approver can turn a
gate advisory for one release. On GitHub Free a failed check still cannot block a merge
(GATE-03), so required: true makes a bypass detectable (GATE-04), not impossible; re-run the
ruleset script after changing it so the required status checks match.
One app only: releaseGuardrailOverridesByApp (2026-10-28)
Sometimes one app has a real, known finding that cannot be fixed yet and should not hold its
releases, while every other app keeps the gate required. The first case: backstage’s
image-scan fails on 85 HIGH Debian trixie base-package CVEs that have no fixed package, and with
promotion on (registries.md) a red required gate means the release is never
promoted. The per-app map, in the same cluster catalog values as the per-cluster one:
releaseGuardrailOverridesByApp:
backstage: # the <app> of gitops-<app>
image-scan: { required: false }
It still runs; it does not hold the release. This is narrower than the per-cluster form. The
gate Pipelines pick their hard or advisory variant at render time, once per cluster, so for
backstage image-scan still runs as the hard check and posts its real verdict: red stays red
on the PR, because the findings are real. What changes is everything that decides at runtime,
for a known app, whether the release may go ahead. Each of these skips a gate that is advisory
for the app it is looking at:
- the promotion fan-in (
comment-pr-check-result,promotion-readystep): does not wait for it, and still asks for promotion when that gate itself finishes last and red; promotePROM-03: does not refuse on it (the promotion attestation’sgatessnapshot leaves it out, as it leaves out every advisory gate);detect-bypass-merge: a merge past it is not a bypass;release-log-emit: records its real conclusion, but it does not setbypass;wait-for-release-guardrails: does not wait on it;open-release-pr: the PR body’s table says “real, advisory for” on that app’s PRs; hack/apply-guardrail-ruleset.sh: leaves it out ofOWNER/gitops-<app>’s required status checks (with-ffor the cluster’s values), and keeps it for every other repo.
The app is read from the gitops repository’s name (gitops-<app>; open-release-pr uses its
app-name, which names that repository). A repository not named that way gets no per-app
exemption.
Refusals (the chart render fails, _helpers.tpl glidepath-catalog.advisoryGatesByApp):
sast and provenance, as above; image-promotion (the Flight copy is what the release
deploys); a gate that is not in releaseGuardrails; an app key that is not a plain
gitops-<app> name (lowercase letters, digits, dashes); and anything but required: false - a
per-app entry can make a gate advisory for that app, never stricter than its cluster.
Who sets it is the same as for the per-cluster map: a platform admin, in the cluster’s catalog
Application values. It is not in cicd.yaml and not a Tower setting. Treat an entry as a dated
exception with a reason next to it, and remove it once the finding is fixed.
itsm is required by default once a change tool is configured; a cluster that wants to ease into
change management sets it advisory the same way. Today qa and api-compat (api-compat.md) are the advisory gates; qa is report-only while every app gets its check ride
(testing.md); flip it once every app with a Flight environment has
apps/<app>/check-ride.yaml in the check-rides repository and a role: check-ride test step.
Current gates
| Gate | What it verifies | Status | Required |
|---|---|---|---|
sast |
Static analysis (Semgrep) of the promoted commit’s source | real | yes (locked) |
image-scan |
Vulnerability scan (Trivy) of the promoted image | real | yes |
provenance |
Commit signature (gitsign, or a GitHub PR merge by an allowed login) + SLSA provenance (Conforma) | real | yes (locked) |
sbom |
Software bill of materials | real | yes |
values |
Values-file validation by the validator image (Airframe’s by default); check Glidepath / values- |
real | yes |
itsm |
An approved or scheduled change request for the release in the change tool (change-management.md) | stub until itsm.provider is set, then real |
yes once real |
api-compat |
The promoted image’s signed API spec diffed against the target environment’s, with the operations each breaking change touches and whether they are in use (api-compat.md) | real | no (advisory) |
qa |
A passing, recent check ride from the shared check-rides repository, attested on the promoted image (testing.md) | real | no (advisory) |
policy-validation |
The release’s environments rendered and evaluated offline (Kyverno CLI) against the target cluster’s admission policies from its GitOps repo; only Deny/Enforce findings block (policy-validation.md) | real | yes |
agent-gates |
Agent identity and scope: Clearance’s record of the PR its agent opened, the same head and trailers, only paths that agent may change; a person’s or release PR passes as not-agent (agent-gates.md) | real | yes |
clearance-gate |
Clearance’s deployment gate: the release classified by what its rendered manifests change (tier 0-3) and cleared by policy or by the app’s owners in Tower; red while holding (clearance-gate.md) | real | no (advisory until every gitops repo has the check; the flip and its order are in clearance-gate.md) |
image-promotion |
Only with catalog promotion.enabled: the release image copied by digest to the Flight registry with its signatures and attestations, plus a promotion attestation (registries.md) |
real | yes |
How qa judges a release (2026-10-19, phase 3 of testing.md). The gate never runs tests. It
resolves the promoted image like the other image gates, downloads the digest’s attestations, verifies each SLSA
provenance against the platform’s Fulcio identity and a real Rekor entry (the same cosign flags as
verify-sast-attestation), and keeps the run-testworkflow entries whose role is check-ride. Tekton Chains
attests every completed test PipelineRun, failed ones included, so presence is not a pass: one entry must have
outcome: passed, tests-failed: 0, workflow-source: argocd:<qa.checkRidesApplication> (default check-rides,
the fence) and be younger than qa.maxAge (default 72h), measured from the attestation’s Rekor integratedTime
(the provenance’s buildFinishedOn only if the entry has none). The PR comment carries the counts and the Testkube
execution id on a pass; on a failure it says which of the four was missing: no check ride ran for this digest, it
failed, it was not from the shared repository, or it is too old. A new build is a new digest and needs its own
ride; /retest qa- re-reads the registry. qa is a content gate: on an eligible rollback it reports without
blocking (ADR-0021 decision 8), which is where an older ride would otherwise trip it.
qa is advisory (its registry entry has required: false, see “Required and advisory gates” above; this
replaced the one-off qa.required value on 2026-10-19). While it is, the gate runs on every release PR and posts
what it found, but never fails the check, so an app that has no check ride yet is not blocked while every app
gets one.
provenance on an image with check rides (2026-10-19). Every check-ride test PipelineRun’s SLSA provenance is
attached to the tested digest too (testing.md), so the Conforma half of provenance keeps only the
build provenance (tekton.dev/pipeline=build label or a build-image task) before ec validate input; the test
provenances belong to qa. Its commit-signature half resolves the PR number from the revision commit on a
/retest provenance- run, where PaC hands over the raw body.pull_request.number template string, the way the
other gates’ resolve step and PR comment already did (provenance-policy.md).
pr-number comes from {{ pull_request_number }} (2026-10-28). The delivered .tekton/pull-request-*.yaml files
used {{ body.pull_request.number }}, which reads the raw webhook body: a /retest arrives as an issue_comment
whose body has no pull_request, so every retest run showed the template string as its pr-number input (the gates
and the comment-pr finally task resolved it from the revision, so the result was right, but the input was not). PaC’s
own pull_request_number variable is set from the PR for pull_request, /retest comments and check re-runs alike.
The tasks’ resolve-from-revision fallback stays, for gitops repos still carrying the old files until their next resync.
provenance accepts a GitHub PR merge (2026-10-20). The commit-signature half has two paths
(commit-signing.md): gitsign, always on and final for any gitsign signature; and, only for an app
whose admin-owned release-signing policy enables it (off by default), a commit GitHub’s web-flow key signed as the
merge of a PR into the default branch, merged by a login in allowedMergers, with optional strict mode (every PR
commit gitsign-signed) and required approvals. Before this, 10 of the last 12 releases failed the gate because GitHub’s
merge button replaces the signature, and two were merged past it. The policy, signers included, moved out of the app’s
cicd.yaml into the catalog’s releaseSigning values (ConfigMap release-signing-policy); cicd.yaml can only
tighten it. What changes in the trust:
| Claim | gitsign | GitHub merge |
|---|---|---|
| Commit unmodified since signing | yes (Sigstore cert, Rekor) | yes (GitHub’s pinned web-flow key) |
| Who signed | an allowed person | GitHub’s servers |
| Who caused it | the signer | the PR’s merged_by, an allowed login |
| Who wrote the code | the signer | the PR’s authors; gitsign-verified in strict mode |
A stolen GitHub session can merge, so the GitHub-merge path is weaker on non-repudiation; the gate’s results
(signature-path, signature-detail) and its PR comment say which path verified each release. Since phase 3 the
comment also carries a machine-readable <!-- glidepath:signature {...} --> line that Tower seals into the Release
Record and scores (commit-signing.md: Evidence).
How the image gates find the image (2026-10-09). sast, image-scan, sbom, qa and the provenance half of
provenance share the resolve-promoted-image StepAction: two GitHub API reads against the PR (its base, the
files changed between that base and the head commit, then the one manifest’s content) - no clone of the gitops
repo. The manifest is the first changed */release.yaml, else */values.yaml, else glidepath/releases/*.yaml;
a PR that changes none of them is not a promotion and the gate says so. verify-commit-signature reads the PR’s
commits the same way for the X-App-Repo/X-App-Commit trailers, and validate-values fetches the PR head one
commit deep and takes its changed files from the same compare. Every gate pipeline passes pr-number for this;
without it (a manual run) each falls back to the revision’s own commit. Before this every gate full-cloned the
gitops repo to diff one commit, and a release branch with more than one commit broke the three image gates.
Release Record PRs are not releases. Tower’s Release Record poller opens PRs on the gitops repo from
release-record-<app>-<hash> branches that only add releases/<app>@<version>.yaml. Every gate template’s
trigger excludes them (!head.ref.startsWith("release-record-")); before 2026-10-09 they matched
startsWith("release-") and the image gates failed on each one - 135 of 284 historical extract-promoted-image
runs.
provenance and policy-validation are deliberately separate gates, not one - the
former is about the commit’s signer identity and the image’s provenance attestation
(docs/commit-signing.md, docs/provenance-policy.md), the latter is about the rendered
manifest’s compliance with cluster admission policy. Different failure modes, different
fixes, kept as independent required checks rather than folded together.
The values gate (values, registered 2026-09-27)
values-validation runs a validator image against every values.yaml a gitops PR changes (a release PR, or a
human’s or an agent’s hand edit) and fails the check on a non-zero exit. Glidepath does not know what the files
mean: the image is valuesValidatorImage in the catalog values, and it must provide
validate-values [--app NAME] FILE... on its PATH. The Hangar default is Airframe’s airframe-validate image
(strict keys, component XRD specs, helm template), so Glidepath stays installable without Airframe: use another
image, or never register the gate.
Pieces: Task validate-values, Pipeline values-check, onboarding template
gitops-repo/pull-request-values.yaml (runs on every PR to main that touches a values.yaml).
Rollout order matters. Registering the gate in releaseGuardrails makes detect-bypass-merge treat its
check as required on every merge, so:
- Publish the validator image and make its package public.
- Let the onboarding-resync PR (which adds
.tekton/pull-request-values.yaml) merge in every gitops repo. - Only then add
valuestoreleaseGuardrails(statusreal; the name must match the PipelineRun’sgenerateNameprefix,values-, becausedetect-bypass-mergelooks up the checkGlidepath / values-). From then on a merge with that check red is reported as a bypass. (Since 2026-10-19 the registry entry can also be added early withrequired: false: the gate then reports without blocking, thevalidate-advisorytask in place ofvalidate, and flipping it totruelater is a one-line change.) It does not make the check required on other PRs (a human’s or an agent’s hand edit to avalues.yaml): that is the GitHub ruleset’s required-status-checks list, a separate setting kept in sync by hand (see “Branch protection” below). AddGlidepath / values-there. Before step 3 the check runs and reports but does not block.
Removing a gate
Delete its onboarding template file
(charts/glidepath-app/files/onboarding-templates/gitops-repo/pull-request-<name>.yaml)
and its releaseGuardrails entry. If it had a dedicated Pipeline/Task (a promoted-to-real
gate), remove those too once nothing else references them. Worked example: the
image-promotion retirement, 2026-10-09 (ADR-0025). Three things to know:
- Already-onboarded gitops repos still have the old
.tekton/pull-request-<name>.yamluntil their next onboarding-resync PR. Since 2026-10-09 that resync removes a platform-generated gate file whose template no longer exists (it recognises its own files by their# STUB/# BOILERPLATE/# The real ... gatefirst line; a tenant’s own.tektonfile carries no such marker and is left alone, as before). Trigger a resync for every app after the gate is removed. - The catalog Application does not prune. Removing a Pipeline/Task from the chart
leaves the live object in
platform-catalog(OutOfSync, still resolvable). Delete it by hand once every gitops repo’s resync has merged - until then the stale.tektonfile still runs against it, which is the safe order. - Branch protection on the gitops repo’s
mainstill lists the old check name as required (see below) - a required check that no PR will ever produce again blocks every future release PR from merging until removed.
Branch protection
Gap (2026-09-27): in practice the enforcement is a GitHub ruleset on the gitops repos, and it is an integral, mandatory part of a Glidepath install: without its required-status-checks list none of these gates block a merge. This section still describes classic branch protection. The ruleset is documented, and audited against a real export, in github-ruleset.md; generate and apply it with
hack/apply-guardrail-ruleset.sh. Read that first.
Not IaC-managed by this platform (docs/release.md’s own “Onboarding” section covers the
one-time per-repo setup) - releaseGuardrails and branch protection’s required-status-
checks list are two independent settings that must be kept in sync by hand. Adding a
gate here without also adding it to branch protection means the check runs and reports,
but never actually blocks a merge. Removing a gate without removing it from branch
protection means every future release PR is permanently unmergeable (a required check
that will never run again). Neither mismatch is caught automatically - update both
together.
RESOLVED 2026-09-05: sast no longer fails closed - Rekor is live
Rekor was deployed for real (docs/admin/provenance-policy.md), Tekton Chains now
uploads to it, and verify-image-provenance.yaml/verify-sast-attestation.yaml do real
tlog verification (--insecure-ignore-tlog=false) instead of checking the cert against
wall-clock now. Live-verified through a real release PR: both gates’ cosign checks now
print Verified OK using the Rekor entry’s own integratedTime, independent of how long
release-time re-verification takes. The rest of this section is left as written for
historical accuracy - the structural problem it describes is real, it’s just fixed now
via the first option listed under “not fixed here” below.
Historical: sast failed closed on essentially every real release
**Found live 2026-08-23. Two
gates failed in the same test run for what first looked like the same reason; live
evidence showed they’re actually two different problems (see the provenance fix
just above for the other one - a slow-but-legitimate ec validate policy evaluation
tripping its own 5-minute internal cap, now given more headroom).
sast’s failure is different and NOT fixed by a bigger timeout.
verify-sast-attestation.yaml re-verifies the Fulcio leaf certificate that Tekton
Chains attached to the build-time attestation, at release-PR time - necessarily
minutes later, since build->test->deploy->release all run first. This platform’s Fulcio
issues leaf certs with a 10-minute validity window (confirmed live: openssl x509 -noout -dates on the real cert used in the failed check, notBefore/notAfter exactly
10 minutes apart), and deliberately runs with Rekor/tlog verification disabled
(transparency.enabled=false, --insecure-ignore-tlog=true - see
docs/provenance-policy.md). Without a tlog entry to anchor “this cert was valid at
signing time”, cosign has no choice but to check the cert against wall-clock now -
and by release-PR-check time it’s routinely already expired. Confirmed with openssl verify, not just cosign’s own terse error: error 10 at 0 depth lookup: certificate has expired. (verify-image-provenance.yaml’s own cosign check does the same
leaf-cert-against-now verification and is exposed to the identical risk - it happened to
still be inside the 10-minute window on this particular run, “Verified OK” printed right
before the unrelated ec validate timeout, but that’s timing luck, not a guarantee.)
This is structural, not a resource-contention flake: any release where more than
~10 minutes elapse between the build stage and the release-PR check running (i.e.
essentially all of them - test+deploy+release plus PR-check scheduling delay reliably
exceeds 10 minutes) will fail sast closed, every time, regardless of cluster load, and
provenance is one slow evaluation away from the same fate. The image-scan/sbom
gates aren’t affected the same way - they scan/sign fresh at check time rather than
re-verifying an old cert.
Not fixed here because the real fixes are all bigger platform decisions than “reroute the guardrails,” and one of them was already tried and deliberately shelved:
- Enable Rekor (the sigstore-native fix - a tlog entry lets a later verification check cert validity at signing time instead of now). Previously attempted and abandoned after repeated live failures caused by the dev cluster’s then-unstable local runtime, not a design objection - see the prior Rekor-install session notes. Would need to be re-attempted deliberately, not as a side effect of this work.
- Run a Timestamp Authority (RFC3161) as a lighter-weight alternative to full Rekor -
cosign supports
--timestamp-server-urlfor exactly this. Still new infrastructure to build and operate, not yet evaluated. - Do not widen
--insecure-ignore-*further to also ignore cert expiry - that discards the one thing short-lived-cert verification is actually for (proving the key that signed this wasn’t already compromised/rotated away by the time anyone checks), not a fix.
Until one of these is decided and built, treat sast/provenance failures on a real
release PR as expected/known, not a signal something regressed - /retest will not help
(the cert only gets more expired). Bypass sits behind existing break-glass tooling
(detect-bypass-merge.yaml, docs/release.md’s “Break-glass” section) if a real release
needs to merge in the meantime.