Docs · Hangar
This document also has its own page, with every rule searchable by strength: Business rules.
Business rules
Every rule Hangar applies to people, releases and clusters, in one place. Each component’s own docs explain how a
rule works and why; this page lists every rule, how strongly it holds, and what holds it. If a rule is not on this
page, it is not a rule yet.
Last checked against the code on 2026-10-19: backstage e40008f (+ branch tests-route), tower 2228fbc (v0.80.0), glidepath 0e66378,
airframe 4b3cde1 (v0.3.157), gitops-hangar-dev 6a6f055, gitops-hangar-prod d751f6e, check-rides 193afc2.
REL-17 and REL-18 (commit proof on the Release Record and in the score) added 2026-10-20 with Tower v0.83.0 and
glidepath 36eb223. GATE-05, GATE-06 and GATE-15 to GATE-17 (release-signing policy, GitHub-merge path) re-checked on 2026-10-20: glidepath
9c42d81 (ADR-0027), tower be8111b (v0.82.0), backstage 61634fb, gitops-hangar-dev 0d48da0, apron 24d23d0,
airframe 8aea2f6.
GATE-01 and GATE-18 (policy-validation real) checked on 2026-10-27 against glidepath 22cd9ff (ADR-0028).
PROM-01 to PROM-11, POL-11 and the SC-05 change (Ground and Flight registries, ADR-0029) added 2026-10-30, checked
against glidepath 983d2e0, apron 2493b02, gitops-hangar-dev 52b1223, gitops-hangar-prod fd9082a, and live on
both clusters.
OPS-06, POL-01 and TEST-10 changed 2026-10-30 with Glidepath API P6 (Tower on glidepath-api, no Tekton writes), checked
against gitops-hangar-dev 52b1223 and live (kubectl auth can-i).
API-01 to API-12 (the CI/CD API, glidepath-api, and the cicd-validate check) added 2026-11-01, checked against
glidepath d48e1b7 (contract 0.11.0) and gitops-hangar-dev fd0ed74, and live on hangar-dev.
CRED-07 to CRED-09, POL-10) re-checked on 2026-10-17: glidepath 1ad8dae, airframe 0710485 (v0.3.148),
gitops-cluster-dev #108.
CLR-01 to CLR-35, OBS-01 to OBS-18, AI-03 to AI-05 and the section 21 rows for Clearance added or changed 2026-11-10,
checked against clearance 4ccf3dd (v0.39.3, docs and code at origin/main), gitops-hangar-dev 446ea51 (live
clearance: block and 55-clearance/clearance/application.yaml, chart 0.39.0), gitops-hangar-prod d957792 (clearance:
block, 56-clearance-gate/admission.yaml) and hangar-infra 737907c. Verified from those files and from GitHub’s
API for the gitops repos; nothing was read from a running cluster or AWS account for this change. Rows marked
Building (CLR-14, OBS-10, OBS-15) are decisions of 2026-11-10 and describe code that has not shipped.
How to read it
Each rule has an ID that never changes and is never reused. Strength says what happens when someone tries to break
the rule:
| Strength |
Meaning |
| Enforced |
A system refuses the action. |
| Fails closed |
Enforced, and if the check itself cannot run, the action is refused too. |
| Soft |
The rule is checked, and breaking it is flagged, announced and recorded, but nothing stops it. |
| Display |
Shown to help a person decide. Nothing is checked or blocked. |
| Convention |
Written down. Nothing checks it. |
| Off here |
Built and documented, but not in effect in this lab (usually the GitHub plan). |
| Building |
Decided and in development. Not in effect until the release named in the row ships. |
Source links to the doc that explains the rule in full, or to the code when no doc does.
Contents
- People and roles
- Access in the portal
- Environments and promotion
- Release gates
- Testing and the check ride
- Change management (itsm)
- Release records, modes and sign-off
- Rollback
- Supply chain
- Live operations from Tower
- Break-glass debug sessions
- Images and retention
- Service catalog (Airframe)
- Cluster admission policies
- Credentials and secrets
- Notifications
- AI agents
- The CI/CD API (glidepath-api)
- Clearance: the deployment gate
- Clearance: observation, evidence and the packet
- Rules that are not enforced yet
- Keeping this page true
1. People and roles
| ID |
Rule |
Strength |
Held by |
| PPL-01 |
You sign in with GitHub, and only if the catalog has a User whose name is your GitHub login. Guest sign-in works only in local development. |
Enforced |
Backstage auth (usernameMatchingUserEntityName); the guest provider refuses outside NODE_ENV=development |
| PPL-02 |
Groups and memberships come from the catalog (examples/org.yaml, each User’s memberOf). Nothing syncs them from GitHub: a personal account has no org to sync from. |
Convention |
backstage examples/org.yaml |
| PPL-03 |
An app’s owners are its catalog Component’s spec.owner, which comes from the XR’s terasky.backstage.io/owner annotation. You own an app if you, or a group you belong to, are its owner. |
Enforced |
the catalog’s isEntityOwner rule |
| PPL-04 |
Admins are the members of group:default/admins. Admin is a grant, and some actions need it whoever owns the app (ACC-04). |
Enforced |
permissionPolicy.ts |
| PPL-05 |
Break-glass reviewers (group:default/breakglass-reviewers) may review a glass-broken session. They get nothing else from that group. |
Enforced |
Tower’s break-glass routes |
| PPL-06 |
Tower’s write routes accept only a signed-in user. A service token is refused. |
Enforced |
towerAuthz.ts |
Today’s groups are admins, team-passenger, team-flight-ops, team-skyport, team-platform and
breakglass-reviewers.
2. Access in the portal
| ID |
Rule |
Strength |
Held by |
| ACC-01 |
Anyone signed in can read everything: the catalog, every app’s Tower tabs, TechDocs, release records, sign-off states and the release calendar. |
Enforced |
permission policy (every read allowed) |
| ACC-02 |
Only admins can delete catalog entities, register or delete catalog locations, run software templates (create a scaffolder task, run an action) or manage templates. |
Enforced |
permissionPolicy.ts WRITE_PERMISSIONS |
| ACC-03 |
Every Tower write needs the app’s owners or an admin. The route finds the app’s catalog entity and asks the permission framework for that user and that app. |
Enforced |
towerPermissions.ts, towerAuthz.ts |
| ACC-04 |
Some writes need an admin even from an owner: changing an app’s release mode, syncing or force-syncing a Flight environment, ending someone else’s break-glass session, and breaking the glass. |
Enforced |
towerGrantOnlyPermissions |
| ACC-05 |
A request can only act on the repository the catalog names for that app. The GitHub owner in the request must match the entity’s github.com/project-slug. |
Enforced |
towerAppFromEntity |
| ACC-06 |
The Kubernetes proxy is read-only. Anything but GET, HEAD or OPTIONS is refused, and so is a WebSocket upgrade (exec or attach). Every write goes through an owner-checked route. |
Enforced |
kubernetesProxyGuard.ts |
| ACC-07 |
Tower greys out the actions you may not take and says why. That is a courtesy: every route checks again. |
Display |
Tower /argo/capabilities |
| ACC-08 |
Every Tower write is audited (tower-write), refusals included. Severity is medium by default, high for Flight permissions, rollback, force sync and image pruning, and critical for a Flight analysis bypass or a broken glass. |
Enforced |
Backstage auditor |
Full action-by-action table: Tower security model.
| ID |
Rule |
Strength |
Held by |
| ENV-01 |
Every environment is Ground (lower, on the dev cluster) or Flight (upper clusters). The tier comes from the app’s cicd.yaml, read on the server, never from a request. An environment that can’t be placed counts as Flight. |
Enforced |
Backstage routes; Glidepath |
| ENV-02 |
Ground is self-service: a deploy is a direct commit to the app repo’s glidepath/envs/<env>.yaml by the deploy bot. No release gates run on Ground. |
Enforced |
Glidepath deploy-manifests |
| ENV-03 |
Flight changes only through a release PR to gitops-<app>. The release pipeline can open a PR but cannot touch a cluster; Argo CD syncing the merged PR is the only way in. |
Enforced |
Glidepath ADR-0004 |
| ENV-04 |
A promotion carries forward only what already runs in the source environment. The image and commit are read from the source environment’s file on the server, never from the request, and never rebuilt. |
Enforced |
glidepathPromote.ts resolveSource |
| ENV-05 |
No workload, no release. An environment whose values turn the service off (rollout.enabled: false) gets no release: Glidepath refuses before any PR or commit, Tower refuses Promote, and the chart refuses to render a tracked release with nothing to run. |
Enforced |
Glidepath release.md |
| ENV-06 |
Every environment’s Argo CD Application syncs automatically, heals drift and prunes what the chart no longer renders. Argo CD refuses a sync that would delete everything, and deleting a generator never deletes a running environment. |
Enforced |
Argo CD (selfHeal, prune, allowEmpty: false, preserveResourcesOnDeletion) |
| ENV-07 |
No cluster holds a credential for another cluster’s API. A Flight environment is created by a commit the target cluster’s own Argo CD acts on. Separate lower and upper AppProjects keep an app’s Ground identity away from Flight destinations. |
Enforced |
Glidepath security.md |
| ENV-08 |
Each app environment has its own namespace (<type>-<app>-<env>), and pipelines run in the app’s -cicd namespace. RBAC never spans them. |
Enforced |
Glidepath, Airframe |
| ENV-09 |
A PR preview environment lives as long as its PR. Its namespace is deleted with its Application, and a sweep every 30 minutes deletes any preview namespace whose Application is gone. |
Enforced |
Glidepath ephemeral-environments.md |
| ENV-10 |
A Flight destination must be a registered upper cluster marked crossplaneReady. A dev cluster is refused. |
Enforced |
Airframe ApplicationEnvironment schema and cluster registry |
Ground and Flight registries (PROM)
Builds push to Ground (ghcr.io/jfillman); upper clusters run only what was promoted to Flight
(ghcr.io/riley-roooo), copied by digest before the release merges. Detail:
Glidepath registries.md, ADR-0029.
| ID |
Rule |
Strength |
Held by |
| PROM-01 |
A Flight environment’s release file pins the Flight repository and a digest (release.image.digest), with the Ground repository it came from as source, from the moment the release PR opens. A release that pins Ground, or no digest, is never promoted. |
Enforced |
open-release-pr; promote refuses |
| PROM-02 |
Only the promoter writes Flight: the promote workflow in riley-roooo/flight-promoter, whose own short-lived GITHUB_TOKEN is the only credential with write access. Builds, clusters and Glidepath cannot write it; Glidepath can only start the workflow (the hangar-flight-promoter App, actions: write on that one repo, minted per call by the broker for the glidepath-promoter ServiceAccount only). |
Enforced |
GHCR package permissions; broker /promoter-token |
| PROM-03 |
Promotion starts only when every required gate is green on the release PR’s current head commit. The last gate to finish starts it (an event, not a polling pod), exactly once; promote re-checks every gate itself and refuses a red or missing one. image-promotion is then a required check on the PR, and it cannot be made advisory. |
Enforced |
comment-pr-check-result fan-in; promote |
| PROM-04 |
The copy keeps every digest: the image index, every platform manifest, the .sig/.att tags and the OCI 1.1 referrers. Nothing is re-signed, so Rekor entries and the check ride’s age stay valid. The copy is verified in Flight with a read-only credential before the check goes green: digest, .sig, .att, and a promotion attestation naming this request. |
Enforced |
promote workflow; promote-image StepAction |
| PROM-05 |
Upper clusters run tenant images only from Flight, each with a promotion attestation (https://hangar.io/attestations/promotion/v1) signed by the promote workflow’s GitHub Actions identity on public Sigstore and verified offline. Third-party images no build produces (CloudNativePG, OTel auto-instrumentation, bitnamilegacy) are allow-listed by prefix. Tenant pods pull with a read-only Flight credential; the Ground credential is not on the cluster. In force on hangar-prod since 2026-10-30 (Audit from 2026-10-29). |
Fails closed |
Kyverno flight-only-images (ValidatingPolicy) and flight-images (ImageValidatingPolicy), Deny, failurePolicy: Fail, rendered by Apron admission.flight (POL-11) |
| PROM-06 |
Only the app’s own Ground repository is promotable: never a PR preview build (<app>-pr) or another app’s image. |
Enforced |
promote |
| PROM-07 |
Promoting a digest already in Flight (a rollback, a second environment) copies nothing new and adds a new promotion attestation for that release. |
Enforced (not yet exercised by a live rollback) |
promote workflow (digest copy is idempotent) |
| PROM-08 |
A release merged before its promotion is a bypass and is never promoted afterwards: the merged pin names a Flight digest that does not exist, so the new pods cannot start and the previous ReplicaSet keeps serving; the missing image-promotion check is reported as a bypass. |
Enforced (promote refuses a merged PR); the bypass alert is Soft |
promote; detect-bypass-merge (GATE-04) |
| PROM-09 |
Flight retention never deletes a release that a release file pins, a Release Record names, or an open release PR pins, nor the 10 newest releases of a package, nor one younger than 30 days. The platform plans, and only the promote repo’s prune workflow deletes. A package without a readable gitops repo is never pruned. |
Off here (Flight only grows: promotion.retention.enabled is false; the prune workflow is in the promoter repo since 2026-10-30 but has not run) |
Glidepath flight-prune CronJob; prune workflow |
| PROM-10 |
With promotion on, a cloud target (aws-ecs, aws-lambda, azure-container-apps) in a Flight environment is refused before it deploys: a cloud deploy copies from Ground, so nothing promoted it. Ground environments of the same app are not affected. |
Enforced |
resolve-deploy-target |
| PROM-11 |
The registries and promotion settings are admin-owned: the catalog’s registries and promotion values and the cluster’s admission.flight, never cicd.yaml or Tower. |
Enforced |
Glidepath catalog values; Apron cluster.yaml |
4. Release gates
| ID |
Rule |
Strength |
Held by |
| GATE-01 |
Every release PR to a gitops repo runs every registered gate: sast, image-scan, provenance, sbom, values, qa and policy-validation are real; itsm is real once the catalog names a change tool (itsm.provider) and a stub until then. |
Enforced (they run) |
Glidepath release-guardrails.md |
| GATE-02 |
A stub never reports a pass. It logs that no check exists, returns stub, and shows as a grey STUB everywhere. |
Enforced |
Glidepath governance-stubs.md, ADR-0003 |
| GATE-03 |
A gate stops a merge only through the gitops repo’s GitHub ruleset: every gate a required check from the Pipelines as Code app, 2 approvals, no bypass actors. |
Off here |
Glidepath github-ruleset.md. Private repos on GitHub Free cannot enforce rulesets, so anyone with write access can merge past a red gate. |
| GATE-04 |
Merging past a failed or missing gate is a break-glass bypass. It is detected after the merge and alerted (not subject to notification settings), and the PR keeps its failed checks. |
Soft |
bypass-check (detect-bypass-merge) |
| GATE-05 |
The commit being released must be proven one of two ways: gitsign-signed (public Sigstore) by an address in the app’s allowedCommitSigners from the admin-owned release-signing policy (GATE-16), or a GitHub PR merge that passes GATE-15. A gitsign signature is judged by gitsign alone; failing it never falls through to GATE-15. An empty signer list fails closed. Changed 2026-10-20: the list was the app’s own cicd.yaml. |
Fails closed |
provenance gate (verify-commit-signature); Glidepath commit-signing.md |
| GATE-06 |
For an app without the GitHub-merge path, land app changes by fast-forwarding a gitsign-signed commit, never with GitHub’s merge button: all three button options make a new commit GitHub signs, which fails GATE-05. With the path on, the merge button is fine when an allowed login presses it. The web editor, the contents API, GitHub Apps and “Update branch” never count. |
Convention (GATE-05 and GATE-15 catch it) |
commit-signing.md: merge strategy |
| GATE-07 |
The image must carry a valid SLSA provenance attestation from the platform’s own Fulcio (the Tekton Chains identity), and the attestation must pass the Conforma policy. |
Enforced |
provenance gate; Glidepath provenance-policy.md |
| GATE-08 |
Every changed values.yaml passes airframe-validate: no secret-looking literal (AF-SECRET-001), no release-owned key in a human file or the reverse (AF-OWNER-001), and no unknown component or output (AF-COMP-001/002). |
Enforced (subject to GATE-03) |
values gate |
| GATE-09 |
A Release Record PR (release-record-* branch) is not a release, and the gates skip it. |
Enforced |
gate triggers |
| GATE-10 |
/retest re-runs a gate. It never skips one. |
Enforced |
Pipelines as Code |
| GATE-11 |
A new or removed gate goes in the releaseGuardrails registry, and the ruleset is regenerated from it in the same change. |
Convention |
hack/apply-guardrail-ruleset.sh |
| GATE-12 |
qa passes only when the promoted image’s own test provenance (TEST-06) carries a check ride that is verified, passed, with tests-failed: 0, from the shared check-rides Application (workflow-source: argocd:check-rides), and younger than qa.maxAge (72h). It names what was missing otherwise. An eligible rollback is advisory. |
Enforced as a check; Soft today (advisory, GATE-13) |
qa-gate; Glidepath testing.md |
| GATE-13 |
Every registry entry has required (default true). A gate with required: false is advisory: it runs and posts what it found, its check stays green, a merge past it is not a bypass, and the ruleset leaves it out. Only a glidepath PR (the chart default) or a cluster’s catalog values (releaseGuardrailOverrides) change it: never cicd.yaml, never Tower, never per release. On hangar-dev qa is advisory until every app has a check ride. |
Enforced |
glidepath-catalog.guardrails; release-guardrails.md: Required and advisory gates |
| GATE-14 |
sast and provenance cannot be made advisory: a catalog that sets either to required: false fails to render. |
Enforced |
glidepath-catalog chart render |
| GATE-15 |
GitHub-merge path (2026-10-20, off unless the policy turns it on for the app). A commit signed with GitHub’s web-flow key passes GATE-05 only when every check holds: the key is one of GitHub’s pinned keys (GATE-17); the committer is GitHub; it is the merge_commit_sha of a PR merged into the default branch; it is on that branch, and a merge commit’s second parent is the PR’s head; merged_by is in allowedMergers (an empty list fails closed). mode: strict also requires every PR commit gitsign-signed by an allowed signer. requiredApprovals: N requires N approvals of the PR’s final head by other allowed mergers. Defaults when on: merger mode, 0 approvals. The PR comment and the gate’s results say which path verified the commit. Applies to tag-triggered releases too. |
Fails closed |
verify-commit-signature; commit-signing.md: the GitHub-merge path, ADR-0027 |
| GATE-16 |
The release-signing policy is admin-owned: signers, whether the GitHub-merge path is on, its mode, allowedMergers and the minimum approvals are set only in the catalog’s releaseSigning values (a glidepath PR, or a cluster’s 50-glidepath/glidepath-catalog values), never in cicd.yaml or Tower. A product owner asks for a change; a platform admin merges it. An app’s cicd.yaml can only tighten it (governance.githubMerge.mode: strict, a higher requiredApprovals); governance.allowedCommitSigners there is ignored. |
Enforced (the gate reads only the catalog’s ConfigMap; the schema refuses enabled/allowedMergers in cicd.yaml) |
release-signing-policy ConfigMap; commit-signing.md: who sets the policy |
| GATE-17 |
GitHub’s web-flow keys are pinned: checked in, baked into the toolbox image with a fingerprint assert, and pinned again in the gate. A signature by any other key, including a new GitHub key not yet pinned, fails closed; the retired 2017 key counts only for signatures made before 2024-01-16. The gate never fetches a key while verifying. A weekly workflow warns when GitHub publishes a different key set. |
Fails closed |
toolbox image, verify-commit-signature, web-flow-key-check workflow |
| GATE-18 |
policy-validation renders every environment the release PR changes and evaluates it offline (Kyverno CLI) against the target cluster’s own admission policies, 30-policy/ on main of its gitops-<cluster> repo. Only a Deny/Enforce failure fails the check; Audit/Warn findings are warnings. A render failure or an unreadable policy source fails it (fail closed). The policies are fetched by the token broker (POST /policy-bundle) with a read-only token the pipeline never sees. |
Enforced |
Glidepath policy-validation.md (ADR-0028) |
5. Testing and the check ride
The test stage, the shared check ride and the evidence a release carries: Glidepath testing.md, the check-rides repository’s README, and the library paper A test contract for Glidepath, and a QA gate that reads it.
| ID |
Rule |
Strength |
Held by |
| TEST-01 |
A test step runs the TestWorkflow it names with the platform’s context as config (app, env, image, url, revision, gitUrl, chain, for the keys the workflow declares) and as execution tags. A workflow never hardcodes its target. |
Enforced |
run-testworkflow |
| TEST-02 |
A test or execute step whose workflow does not exist fails, naming the file. Only test.enabled: false skips, and it says skipped. |
Enforced |
run-testworkflow |
| TEST-03 |
A check-ride step runs <app>-check-ride exactly as Argo CD synced it from the check-rides repository, never a file from the app repo: it refuses a workflow without the sync annotation or with another app’s hangar.io/app label. A flow has at most one, it takes no testName, and it is always required. |
Enforced |
run-testworkflow, validateFlows |
| TEST-04 |
The standard (templates/) is owned by SDETs and product owners; an app’s ride (apps/<app>/) by its team and an SDET. The repo’s own check requires the name, the label, a template from templates/, the declared config and only the app’s own Secret. |
Convention (code owners on a private repo on GitHub Free); Enforced with the ruleset |
check-rides CODEOWNERS, tools/check.py |
| TEST-05 |
In the shared testkube namespace a tenant pipeline may reference only its own <app>-app-secrets, and a workflow synced by Argo CD only <hangar.io/app label>-app-secrets, in container env and content.git.tokenFrom. |
Enforced |
Kyverno testkube-secret-usage (ADR-0008) |
| TEST-06 |
Every completed test PipelineRun’s SLSA provenance is signed by Tekton Chains onto the tested image’s digest, carrying the run’s results (outcome, role, test counts, workflow-source, workflow-digest, execution-id). Failed runs are attested too, so a reader selects by task name and reads the outcome, never presence. |
Enforced |
Chains; test.yaml type hints |
| TEST-07 |
The environment’s NetworkPolicy admits same-namespace pods only; an app lets the testkube namespace in through its own glidepath/base.yaml (networkPolicy.allowIngressFrom). |
Convention |
airframe-application |
| TEST-08 |
Tower shows every attested test run on an image (the Tests step), the check ride first, each downloadable; the build provenance is the one whose pipeline ran build-image. |
Display |
Tower v0.79.0 supplyChain.ts |
| TEST-09 |
A release record carries the check ride the qa gate read (outcome, counts, source, execution, verdict, how many runs were attested) frozen in when the record is sealed; before that Tower reads it live from provenance. The run shown is the newest verified pass, else the newest check ride, else the newest run of any role, so a failure is never hidden behind an older pass and “the team’s tests ran, no check ride” reads differently from “nothing ran”. |
Enforced (record) / Display (live) |
backend testEvidence.ts; Tower v0.80.0 findReleaseTestRun |
| TEST-10 |
The Pipelines tab’s Tests view lists every Testkube execution tagged with the app and downloads its JUnit report, read from glidepath-api (listTests, getTestReport) as Tower’s read identity. An execution Testkube marked aborted although every step passed is shown as passed, with the note. Changed 2026-10-30: Tower read Testkube itself through the control plane’s service proxy; that grant is gone. |
Display |
backend /tests, /tests/report; glidepath-api |
6. Change management (itsm)
The itsm gate wraps the company’s change tool (ServiceNow first, a webhook adapter for any other): Glidepath change-management.md. Every rule below applies once the catalog names a provider; with none the gate is a loud stub and the hooks are no-ops. This lab runs the webhook provider against a mock change tool.
| ID |
Rule |
Strength |
Held by |
| CHG-01 |
Every release PR creates a change request, or links one the caller or the PR body already names: a normal change for a promote, an emergency change for a rollback whose parent is the change it rolls back. The PR body and the release record carry it. |
Enforced |
open-release-pr |
| CHG-02 |
The itsm gate passes only when the change exists and is approved or scheduled, and fails naming the change, its state and its approval. |
Enforced (subject to GATE-03 and GATE-13) |
itsm-gate |
| CHG-03 |
The change follows the release: implementing on merge, closed successful when the release is healthy, closed unsuccessful when it is rolled back, with the rollback’s emergency change related. A later rollback re-closes a successful change. |
Enforced |
bypass-merge-check, release-outcome-notify, release-rollback-notify |
| CHG-04 |
A change tool that is down stops a release PR from opening and fails the gate; the later hooks tolerate it visibly (a red TaskRun, the release flow continues). |
Enforced |
onError: continue on the hooks |
| CHG-05 |
Change-tool credentials come from a platform Secret in each -cicd namespace, fanned out from the platform’s Infisical project; never a value. |
Enforced |
itsm.credentials ClusterExternalSecret |
| CHG-06 |
Glidepath never approves, creates or moves a change on the gate’s behalf. A release is held by leaving its change unapproved; a merge past an unapproved change is a bypass (GATE-04). |
Enforced |
itsm-gate |
| CHG-07 |
A release record carries its change request (id, number, URL, state, approval, and the change that rolled it back), frozen in when sealed and read from the newest promote tracking record for the image before that; Tower shows it on the record and as a state chip in the list. |
Display |
backend changeRequestOf; Tower v0.80.0 ReleaseRecordEvidence.tsx |
7. Release records, modes and sign-off
| ID |
Rule |
Strength |
Held by |
| REL-01 |
Each app has a release mode in gitops-cluster-dev-tenants/release-modes.yaml: Assisted (the default) or Hand-flown. Autoland has a name but isn’t available. Only an admin can change a mode, and only through a PR. |
Enforced |
tower.release.mode (admins only) |
| REL-02 |
The sign-off environment is the one cicd.yaml marks production: true, else the last Flight environment. |
Enforced |
releaseFlow.ts |
| REL-03 |
For a Hand-flown app, a release PR to the sign-off environment needs a sign-off. |
Soft |
the hangar/release-signoff commit status; GitHub Free can’t make it required |
| REL-04 |
A sign-off counts only if it is the reviewer’s latest review, it is an approval, and it came before the merge. The reviewer must not be the person who merged or opened the PR, and must be in the app’s owning team or an admin (GitHub login matched to a catalog User). If the catalog can’t be read, no approval counts. |
Fails closed |
releaseApprovers.ts, releaseFlow.ts |
| REL-05 |
A release that merges without a sign-off that counts is announced at high severity. Its record says shipped without or self-approved, and keeps saying so. |
Soft |
release-signoff-poll |
| REL-06 |
Assisted apps record who merged and who reviewed. They get no status and no alert. |
Enforced |
release-signoff-poll |
| REL-07 |
The next release’s draft (notes, summary, owner, risk, target date, milestone, GitHub Release opt-in) is glidepath/releases/next.yaml in the app repo. Owners and admins save it as a direct commit that names them and builds nothing. Unknown fields are dropped, and text is capped at 50 lines of 300 characters per list and 2,000 characters per field. |
Enforced |
tower.release-record.edit; sanitizeDraft |
| REL-08 |
When a release is first healthy in a Flight environment, the backend opens the record PR (releases/<app>@<tag>.yaml in gitops-<app>) with the draft sealed into it, and clears next.yaml, within a few minutes. Nothing merges it. A record is history once a person merges it. |
Enforced |
release-record-seal-poll (2 min), release-record-poll (hourly) |
| REL-09 |
Release names are tree names given in order (ASPEN, BIRCH, CEDAR…), skipping U, V and X, with numerals after the last (ASPEN II). A name is the release’s place in the app’s committed records, so it never changes. |
Enforced |
releaseNames.ts (Tower and backend, kept identical) |
| REL-10 |
A release’s scope is the PRs merged after the previous release’s commit landed on main, up to its own. A PR is never in two releases. |
Enforced |
Tower |
| REL-11 |
Readiness checks the leading candidate: changes merged, running on Ground, 24 hours on Ground, build and scans passed, signed and attested, check ride passed (a verified, passed ride from the shared repository attested on the image: what the qa gate reads), version bumped, and a healthy previous release to roll back to. A target date and closed planned issues are added when the draft has them. |
Display |
Tower nextRelease.ts. Nothing is blocked. |
| REL-12 |
The suggested version is the last version plus the bump the merged PRs call for: breaking means major (minor before 1.0), a feature means minor, anything else patch. The real version is the image’s tag. |
Display |
Tower |
| REL-13 |
A release’s outcome is decided in the sign-off environment. Clean means healthy, then replaced or healthy through the 7-day bake. Rolled back, replaced before healthy and stopped before production are the other outcomes. It is written back once, by PR. |
Enforced |
releaseOutcome.ts (BAKE_DAYS = 7) |
| REL-14 |
A GitHub Release is opt-in per release, published only once the release is healthy in production, and tagged v<version> at the image’s commit. An existing tag is reused only at that commit, and a tag is never moved or replaced. |
Enforced |
releaseOutcome.ts |
| REL-15 |
Everyone hears when a release PR to production opens (“ready for prod”) and when it merges (“released”). After a restart, nothing is announced twice. |
Enforced |
release-signoff-poll |
| REL-16 |
The backend writes only PRs, commit statuses, GitHub Releases and the draft commit. It never merges. |
Enforced |
Tower security.md: release records |
| REL-17 |
The Release Record names how the released commit was proven: the gitsign signer, or the GitHub merge (PR, merger, web-flow key, approvers). It is shown green for every passing path, with a hint on a merge that the proof is GitHub’s key and record, not a personal key, and frozen into the record when it is sealed. It is read only from the provenance gate’s own PR comment posted by the platform’s GitHub App (glidepath.gateCommentApps); a person’s comment is never trusted for it. Records sealed before 2026-10-20 say “not recorded” (no backfill). The same proof shows on the record rows, the release cards, the pending-promotion task and the Release matrix. |
Display |
Tower commitProof.ts; backend parseGateComments; Glidepath commit-signing.md: Evidence |
| REL-18 |
The readiness score’s Commit proof line weighs how the commit was proven: gitsign 6, GitHub merge strict 4, GitHub merge 2, nothing when not recorded or not required (cloud targets). It only adds points; no older record’s score moves. |
Display |
Tower computeConfidence / confidenceBreakdown |
Full detail: Tower release records.
8. Rollback
| ID |
Rule |
Strength |
Held by |
| RB-01 |
A rollback is an ordinary release of an earlier image: a PR, the gates and a merge. Owners start it (tower.release.rollback, audited high). It applies to Kubernetes Flight environments; a cloud target rolls back by a release pin PR (tower.release.pin, ADR-0020). |
Enforced |
Glidepath rollback-release; Tower |
| RB-02 |
The target must be one of the five newest releases of that app, environment and cluster that reached healthy. The gates work this out from the release records, never from the PR’s text. |
Enforced |
extract-promoted-image rollback-eligibility |
| RB-03 |
On an eligible rollback, sast, image-scan and sbom are advisory, because that image already ran there. Provenance, values, commit signing and approvals still apply. |
Enforced |
Glidepath release-state.md: rollback |
9. Supply chain
| ID |
Rule |
Strength |
Held by |
| SC-01 |
Every pod in an app’s -cicd namespace, image builds included, meets Pod Security baseline: not privileged, no added capabilities, no host namespaces or mounts, no Unconfined seccomp/AppArmor. Builds run rootless BuildKit as UID 1000; they can’t meet restricted because the builder needs a user namespace. Each -cicd namespace carries pod-security.kubernetes.io/enforce: baseline, so a pod that breaks it is refused at admission (since 2026-10-17). Until then this rule said restricted (kaniko), which never held: kaniko ran as root. |
Enforced |
Pod Security admission, labels set by Glidepath’s tenant-onboarding ApplicationSet; security.md, rootless-builds.md, ADR-0026 |
| SC-02 |
Tekton Chains signs every image and its provenance with a short-lived certificate from the build cluster’s own Fulcio (its own trust root, never the public one), and logs the signatures in the platform’s Rekor. Rekor’s signing key persists, is never replaced, and is never in git, Infisical or a terminal. |
Enforced |
Glidepath image-signing.md |
| SC-03 |
Tower colours a supply-chain step by its verification. Green: verified. Amber: present but unverified, for a reason that isn’t tampering. Red: contradicted, so don’t promote it. Grey: missing. A signature with no Rekor entry counts as unsigned. |
Display |
Tower supply-chain.md |
| SC-04 |
Readiness’s “Signed and attested” passes only on a verified attestation, never on one that is merely present. |
Display |
Tower isSignedAndAttested |
| SC-05 |
Build signatures and provenance are checked at release (GATE-07), not when a cluster admits an image. Since 2026-10-30 hangar-prod admits tenant images only with a promotion attestation (PROM-05), which records that the image went through the promoter after the gates; the Chains provenance itself is not re-checked at admission (it carries no Rekor inclusion proof, and the build cluster’s Rekor is unreachable from prod). The Tower debug image is checked at admission too (BG-04). hangar-dev checks nothing at admission. |
Enforced at release; at admission on hangar-prod through PROM-05 |
see section 21 |
10. Live operations from Tower
| ID |
Rule |
Strength |
Held by |
| OPS-01 |
Tower has no standing write access to any cluster. Sync, force sync, terminate, Rollout actions and pod restarts run through one Argo CD account, which can act only on app projects: deny rules cover the platform projects. |
Enforced |
Argo CD RBAC |
| OPS-02 |
Through Argo CD, Tower deletes nothing but a single pod. It never rolls back to an Argo CD revision and never edits an Application’s spec. |
Enforced |
Argo CD role tower-pod (delete//Pod/* only) |
| OPS-03 |
Sync and force sync: owners on Ground, admins only on Flight. |
Enforced |
ACC-04 |
| OPS-04 |
Restarting a pod never takes the last ready pod of a Flight ReplicaSet. On Ground it needs an explicit “accept downtime”. |
Enforced |
podRestart.ts |
| OPS-05 |
Skipping canary analysis on Flight (promote full, skip step) is allowed to owners as a bypass: audited critical and announced to everyone. Every Rollout action is announced. |
Soft |
tower.rollout.bypass |
| OPS-06 |
Re-running and cancelling a PipelineRun is for owners, and Tower does both through glidepath-api, never by writing the PipelineRun itself. The cluster refuses any PipelineRun write from tower-actor, and admits from glidepath-api only Glidepath’s own run shape in an app’s or InfraService’s -cicd namespace, and on an existing run only a cancel. Changed 2026-10-30: tower-actor wrote the runs itself, held to the shape by tower-pipelinerun-shape. |
Enforced |
glidepath-api-pipelinerun-shape policy (POL-01) |
| OPS-07 |
Configuration changes (values, env XR, ConfigMap files, cicd.yaml) are PRs, never direct edits. |
Enforced |
tower.config.edit |
11. Break-glass debug sessions
| ID |
Rule |
Strength |
Held by |
| BG-01 |
Only the app’s owners can ask for a session. On Ground it starts at once. On Flight a second person approves first: an owner who isn’t the requester, or an admin. |
Fails closed |
Tower routes, and the cluster refuses a grant whose approver is the requester |
| BG-02 |
An admin may break the glass: a Flight session with no second person. It is audited critical, announced to everyone, and must be reviewed afterwards by someone other than the requester (an owner, an admin or a break-glass reviewer). |
Soft (the review) |
tower.breakglass.self |
| BG-03 |
A session lasts 15, 30 or 60 minutes, with one 15-minute extension. It ends after 10 minutes idle or 2 minutes disconnected. Each person may have one open session. |
Enforced |
backend timers; expiry labels the cluster acts on |
| BG-04 |
Never exec. A session is a separate debug container: the signed, digest-pinned Tower debug image, non-root, read-only, all capabilities dropped. On Flight the cluster refuses kubectl exec into app namespaces for everyone but operators. |
Fails closed |
tower-breakglass-* admission policies |
| BG-05 |
Backstage has no standing debug rights. For each session it binds one of two debug roles to itself, in one namespace, with an expiry. |
Fails closed |
tower-breakglass-rolebinding |
| BG-06 |
Every byte is recorded. A session that can’t be recorded never starts. Recordings are object-locked for 365 days, their hashes anchored in Rekor, and the app’s owners can read them. |
Fails closed |
Tower break-glass guardrails |
| BG-07 |
Admins can end anyone’s session, and on Flight so can the session’s approver. Anyone can end their own. |
Enforced |
tower.breakglass.terminate |
12. Images and retention
| ID |
Rule |
Strength |
Held by |
| IMG-01 |
Owners can prune only their app’s own packages (<app>, <app>-pr, <app>/cache). Platform packages are refused whatever the app is called. |
Enforced |
imagePrunePlan.ts PLATFORM_PACKAGE |
| IMG-02 |
A prune shows its plan first and deletes only that plan. If anything changed, it refuses and shows the new plan. At most 300 versions go per run, audited high and announced. |
Enforced |
imagePrune.ts |
| IMG-03 |
Always kept: anything running or that a ReplicaSet can scale back to; anything named in git (default branch or an open PR); each environment’s last five healthy releases; anything in flight; an open PR’s head image; anything under 14 days old; the 10 newest builds. For the cache: the 20 newest layers and anything under 14 days. A release is deleted whole or not at all. |
Enforced |
imagePrunePlan.ts |
| RET-01 |
Release-tracking records on the dev cluster are deleted after 14 days. Never deleted: a merged or progressing release, or the five newest healthy releases per app, environment and cluster. Outcomes are copied into the git record before the 14 days are up. |
Enforced |
release-record-sweeper |
| RET-02 |
A release that is merged or progressing with no new fact for 45 minutes raises ReleaseStalled, once per state. |
Enforced |
release-record-sweeper |
| RET-03 |
A pipeline stage that hasn’t started its next stage 10 minutes after finishing is reported as stalled. |
Enforced |
stalled pipeline detector |
13. Service catalog (Airframe)
Airframe is where policy turns into code. Its own register is the authority on these rules:
Airframe governance: what is enforced today.
In short:
| ID |
Rule |
Strength |
Held by |
| AF-01 |
You can ask only for the kinds and fields the XRDs offer, within their enums, bounds and cross-field rules. There is no “raw VM” kind, on purpose. |
Enforced |
XRD schema and CEL at admission |
| AF-02 |
Composed resources get least-privilege roles, the baseline NetworkPolicy, dedicated data stores per environment, and hangar.io/* tags and labels. |
Enforced |
compositions |
| AF-03 |
Removing a Kubernetes object never deletes developer data: scaffolded repo files and Infisical projects. Scaffold files and the deploy surfaces Glidepath owns are never overwritten. |
Enforced |
management policies |
| AF-04 |
Upper-environment and catalog changes are PRs a person merges. A catalog change ships as a tag, pinned per cluster, dev first. |
Enforced |
GitOps; Argo CD self-heal reverts hand edits |
| AF-05 |
Only control-plane clusters can be marked autopilotReady. |
Enforced |
cluster registry chart |
| AF-06 |
extraManifests, networkPolicy and httpRoute changes need a person. |
Convention |
AGENTS.md |
| AF-07 |
A security reviewer approves changes to XRDs and compositions. |
Convention |
not enforced |
14. Cluster admission policies
Rules the API servers themselves enforce, so a bug in Backstage, or a compromised Backstage, can’t get around them.
Every one is native ValidatingAdmissionPolicy that fails closed, and each is scoped to its own objects. The exceptions
are the debug-image signature check and glidepath-cloud-deployer-pods, which run in Kyverno’s webhook (also fail
closed).
| ID |
Policy |
Cluster |
Rule |
| POL-01 |
glidepath-api-pipelinerun-shape |
dev |
Backstage (tower-actor) may not create or patch a PipelineRun; glidepath-api’s have Glidepath’s shape only (OPS-06). Kyverno ValidatingPolicy. Was tower-pipelinerun-shape until 2026-10-30, which held Backstage’s own PipelineRuns to that shape. |
| POL-02 |
tower-rolebinding-delete |
dev |
Backstage may delete only tower-breakglass-* RoleBindings. |
| POL-03 |
tower-breakglass-rolebinding |
both |
The only grant Backstage may create. On Flight it must name an approver other than the requester, or carry the glass-broken mark. |
| POL-04 |
tower-breakglass-debug-container, -debug-image |
both |
The exact debug image digest and its cosign signature, hardened settings, and process access only for the approved session. |
| POL-05 |
tower-breakglass-pod-delete, -pod-annotate |
both |
Backstage deletes only debug or copy pods, and patches only the process-access annotation. |
| POL-06 |
tower-breakglass-no-exec |
prod |
No exec into app pods for anyone but operators. Attach only to a debug container. |
| POL-07 |
no-long-lived-sa-tokens |
dev |
No long-lived ServiceAccount token Secrets in backstage-system. |
| POL-08 |
testkube-secret-usage |
dev |
A tenant’s TestWorkflow may reference only that tenant’s own Secrets in the shared testkube namespace. |
| POL-10 |
glidepath-cloud-deployer-pods, -taskruns, -pipelineruns, -jobs |
dev |
Only a cloud deploy may run as glidepath-cloud-deployer, the one identity AWS deploy roles trust. Decided by who created the object, never by labels: pods only from Tekton’s controller or the Job controller, TaskRuns only from Tekton, Jobs only from Crossplane, and only they may change a TaskRun’s or Job’s spec later (others may add metadata, as Chains, Results and the garbage collector do), and a PipelineRun may give it only to the catalog deploy Pipeline’s deploy-aws-lambda and deploy-aws-ecs, not run-wide, with no pod-template env or volumes. |
| POL-11 |
flight-only-images, flight-images |
hangar-prod |
Tenant pods run only Flight images with a promotion attestation, or an allow-listed third-party prefix (PROM-05). Scoped to namespaces labelled hangar.io/managed-secrets. Kyverno, Deny, fails closed. |
| POL-09 |
(how policies are written) |
both |
Every optional field is guarded with has(), and every policy is scoped by match conditions, so a CEL error can only refuse its own objects. A policy change widens before it narrows. |
Full detail: Tower break-glass admission policies.
15. Credentials and secrets
| ID |
Rule |
Strength |
Held by |
| CRED-01 |
Secrets are references to Infisical, never values in git or an XR. |
Enforced on gitops repos (GATE-08), convention elsewhere |
AF-SECRET-001 |
| CRED-02 |
People create credential material themselves, in Infisical. It is never pasted into a chat, a terminal transcript or a repo. |
Convention |
Glidepath secrets-management.md |
| CRED-03 |
Tower holds no long-lived cluster credential. Reads use the pod’s projected token (600 s). Writes on dev use a tower-actor token minted for each action. |
Enforced |
Tower credentials; POL-07 |
| CRED-04 |
The GitHub App’s private key never leaves platform-system. A release gets an installation token for its own gitops repo only. |
Enforced |
token-review-interceptor |
| CRED-05 |
The platform has one identity per external system (GitHub, Infisical, AWS, Azure), in crossplane-system. Apps never hold one. Each app reads only its own Infisical project. On AWS that identity is an IAM role assumed with a projected ServiceAccount token; nothing is stored (since 2026-10-16). |
Enforced |
Airframe provider configs, External Secrets; Airframe cloud-credentials.md |
| CRED-07 |
A Lambda deploy reaches AWS as glidepath-deploy-<functionName>, a role made for that one function: push to its repository and update its code, nothing else. An ECS deploy reaches AWS as glidepath-deploy-ecs-<service>, made for that one service: push to its ECR repository, register task definitions, update that service and pass its execution role to ECS, nothing else. It is assumed with a one-hour token for glidepath-cloud-deployer; no other pipeline step has AWS access, and there is no stored-key path: a deploy that can’t assume the role fails. The target’s bootstrap Job uses the same role; an AwsLambdaTarget has no access-key mode, and on a cluster without web identity it stops at AwsLambdaTargetNeedsWebIdentity (AwsEcsTargetNeedsWebIdentity for ECS). |
Enforced (Lambda since phase 4, 2026-10-16; ECS since 2026-10-17, glidepath#119 and airframe#95; aws_credentials_test.sh in catalog-ci; Airframe no-web-identity render cases) |
Glidepath cloud-deploy-identity.md; POL-10 |
| CRED-08 |
Every IAM role a cloud-target composition creates carries the permissions boundary HangarCloudTargetBoundary. The Crossplane identity can’t create a role, or add rules to one, without it, and nothing may remove it. |
Enforced (IAM) |
HangarCrossplaneCloudTargets v3; Airframe cloud-credentials.md |
| CRED-09 |
A composition never removes something that already exists because a condition it was created under stopped holding, or because an observed value it was rendered from is missing for a reconcile. Conditions gate creation only; deleting the XR is the only way to delete its cloud resources. |
Enforced (render tests) |
Airframe v0.3.144 and v0.3.145; incident 2026-10-16 in cloud-credentials.md |
| CRED-06 |
The image-prune token can only read and delete packages. The backend holds it, never the browser. |
Enforced |
IMG-01 |
16. Notifications
| ID |
Rule |
Strength |
Held by |
| NOT-01 |
SLO alerts go to the app’s owners, or to everyone if the app has no owner. Budget exhausted fires above a burn of 1.0 and clears below 0.9. Burning fast fires when both 5-minute and 1-hour burn exceed 14.4x, and clears below 12.96x. No data fires after 15 empty polls (about 30 minutes); a failed query doesn’t count. |
Enforced |
Hangar SLO docs, sloTransitionPoll.ts |
| NOT-02 |
Release announcements, Rollout actions, analysis bypasses, broken glass, Flight pod restarts and image prunes go to everyone. |
Enforced |
Backstage notifications |
| NOT-03 |
A bypass merge (GATE-04) is alerted however notifications are set. |
Enforced |
detect-bypass-merge |
17. AI agents
| ID |
Rule |
Strength |
Held by |
| AI-01 |
An agent passes the same pre-merge checks a person does, and works within AGENTS.md. |
Enforced (the checks), convention (AGENTS.md) |
Airframe |
| AI-02 |
Agent runs go only to clusters marked autopilotReady (AF-05). |
Enforced |
AgentRun composition |
| AI-03 |
Every tool call, model call, session open and spawn an agent makes is decided by Clearance and recorded. A tool above the agent’s tier ceiling (R007), a repo, path or field outside its allow lists (R014), and a spend over its budget are denied; a child run may only narrow what its parent has left; anything that fails to evaluate is denied (R000). A denial carries a rule id and a fix hint. Updated 2026-11-10: this row said the limits were built but not deployed. |
Fails closed (the decision); the Autopilot gateway, which asks first, enforces it |
Clearance index: the runtime boundary; core.md; gitops-hangar-dev 55-clearance/clearance/application.yaml (the registered autopilot-gateway provider and the hangar policy bundle) |
| AI-04 |
Every agent is denied, whatever its definition says, these paths: .tekton/**, CODEOWNERS, .github/**, AppProject files and release.yaml files; and these fields: /extraManifests, /networkPolicy, /httpRoute, /release, /releaseTracking and /rollout/image. For an agent this makes AF-06 a check, not a convention. |
Enforced |
gitops-hangar-dev 55-clearance/clearance/application.yaml vocabulary.baselineDenyPaths, baselineDenyFields |
| AI-05 |
An agent’s change to a protected environment is tiered again at the deployment gate on its own merits, whoever proposed it (CLR-02, CLR-07). |
Enforced |
section 19 |
18. The CI/CD API (glidepath-api)
Every read and write of an app’s CI/CD, from Tower, Clearance, a script or a person, goes through glidepath-api: runs,
logs, release records, evidence, tests, environments, events, re-run, cancel, promote and roll back. Tower has no
other path since 2026-10-30 (OPS-06). Detail: Glidepath api.md.
| ID |
Rule |
Strength |
Held by |
| API-01 |
Every /v1 call needs a bearer token. Who the caller is comes from the cluster’s TokenReview, or, for a token whose issuer is people-dex, from glidepath-api’s own check of that ID token: signature, issuer, expiry, and an audience of glidepath-api (a token minted for Clearance is refused). A token that fails is GP-AUTH-001/002. |
Fails closed |
api.md: Who may read what |
| API-02 |
What a caller may do is a SubjectAccessReview in the app’s -cicd namespace on glidepath.hangar.io (runs, runs/logs, releases, evidence, tests, environments, events, cicd). Three roles hold the verbs: reader, operator (re-run, cancel) and releaser (promote, roll back). None includes another, and a fresh install binds nobody. On hangar-dev the readers are Tower’s read identity and Clearance; operators and releasers are tower-actor and Clearance. |
Enforced |
glidepath-control-plane api.readers, api.operators, api.releasers |
| API-03 |
An app that doesn’t exist answers 403 to a caller who couldn’t read it if it did, and 404 only to one who could, so the API never tells a stranger which apps exist. |
Enforced |
glidepath-api |
| API-04 |
Only a listed delegate (Tower’s two identities and Clearance on hangar-dev) may name a person with X-Glidepath-On-Behalf-Of (GP-AUTH-003). A delegate’s write must name one (GP-AUTH-004). The API authorizes the delegate and records the person; it can’t check the person, so it trusts the delegate’s own check (ACC-03 in Tower). |
Enforced (the header); the person check is the delegate’s |
api.delegates |
| API-05 |
Writes are tiered. T1: re-run and cancel a PipelineRun, only in the app’s own -cicd namespace, taken from the path, never the request. T2: promote and roll back a Flight environment, which only starts the app’s release pipeline; the change is a gitops PR a person merges (ENV-03). A Ground target is GP-API-422. T3 (anything that changes production directly) is never offered. The tier of every operation is in the contract. |
Enforced |
glidepath-api; api/openapi.yaml x-glidepath-tier |
| API-06 |
Ground promotion and cloud release pins are not offered by the API: both are commits to a repo, and the API holds no GitHub write identity. They stay in Tower. |
Enforced (no route) |
api.md |
| API-07 |
Every write takes an Idempotency-Key, and the run’s name is derived from it: the same key returns the first run (200, replayed), never a second one; a name held by something else is GP-API-409. Every write has a dry run. |
Enforced |
glidepath-api |
| API-08 |
glidepath-api holds no GitHub credential. For each request the broker mints an installation token scoped to one of the app’s own repos (matched to a Pipelines as Code Repository in the app’s namespace) with contents, pull_requests and metadata read only, and only for glidepath-api’s ServiceAccount. The token is never cached, logged, returned or put in an error. |
Enforced |
broker POST /github-read-token (githubRead.callers) |
| API-09 |
Every call, allowed or refused, writes one audit line: operation, tier, app, caller, on-behalf-of, decision, status, time and trace id; a write adds its key’s hash, never the key. No token is ever in it. |
Enforced |
glidepath-api audit log |
| API-10 |
People call the API over the Gateway route (/v1 only) with a people-dex ID token, as people:<GitHub login>, and only if that login is in api.oidc.allowedUsers (GP-AUTH-005); the chart refuses people sign-in without the list. An allowed person is bound as a reader; writing needs the name under operators or releasers. A person is never a delegate. |
Enforced |
api.oidc, api.route |
| API-11 |
An event on the stream is one the API re-verified when it arrived: the sender’s token is checked again by TokenReview, and its ServiceAccount must be in the event’s own app -cicd namespace (the relay excepted). A forged event is dropped. Replicas share events only with each other, under their own token audience. |
Enforced |
/internal/events/tap, peer fan-out |
| API-12 |
Every PR to an onboarded app repo runs cicd-validate: when cicd.yaml changes, the schema, GP-ENV and GP-PIPE rules, and for an agent’s PR the GP-SCOPE rules too; a PR that removes cicd.yaml fails. The same validator answers POST /v1/apps/{app}/cicd/validations before a PR exists. It stops a merge only as a required check (GATE-03). |
Enforced as a check; Off here as a blocker |
Glidepath contract.md |
19. Clearance: the deployment gate
Clearance decides whether a change may land in a protected environment and records the answer as evidence. It
decides and records; Argo CD’s plugin, the cluster’s admission policy and GitHub do the holding. Live on hangar-dev
(core, clearance-api, the evidence ledger, the GitHub App) and gating hangar-prod’s gate-api through the Argo CD agent
and plugin, and the Terraform lab workflow in jfillman/hangar-infra. Detail:
Clearance gate.md,
terraform-gate.md,
Glidepath clearance-gate.md.
The refusal codes (DG-*) are stable and each carries a fix hint, so a caller can correct itself. Rows marked
Building are decided and in development: they are not in effect until the release named in the row ships.
Which changes are gated, and who clears them
| ID |
Rule |
Strength |
Held by |
| CLR-01 |
Protected environments are gated: hangar-prod/*, */prod and */lab (<cluster>/<env> globs; a Terraform plan has no cluster, so */lab matches its workspace). Any other environment clears at tier 0 as unprotected and is recorded like any other change. An empty list protects every environment. A caller cannot opt a change out (protected: false is DG-PROTECTED). hangar-prod’s own agent and plugin list hangar-prod/prod and cover one app today (CLR-20). |
Enforced (fails closed when the list is empty) |
hangar-dev cluster.yaml clearance.protectedEnvironments; gate.md: protected environments |
| CLR-02 |
A change is tiered 0 to 3 from the rendered objects (or the Terraform plan), as the highest tier of anything it touches: 0 docs, tests and label-only edits; 1 an image bump with every check green; 2 runtime shape (replicas, resources, strategy, env, probes, NetworkPolicy, HTTPRoute); 3 Secrets and their references, RBAC, AppProject, and anything in clearance.yaml. Tier is a floor: one tier-3 object makes the whole change tier 3. The platform owns the baseline; a repo’s clearance.yaml can only tighten it, and a file that loosens it denies the change (DG-POLICY). |
Enforced |
gate.md: tiers |
| CLR-03 |
Tiers 0 and 1 clear by policy only when every check holds (CI green, provenance confirmed, the tree renders, admission policies pass offline). A tier-1 image bump from a registry outside ghcr.io/riley-roooo/ (Flight) is tier 2. A change whose checks are missing is not cleared by policy and holds for people. |
Enforced |
gate.md: who clears; gate.trustedRegistries in gitops-hangar-dev 55-clearance/clearance/application.yaml |
| CLR-04 |
Tier 2 needs one person from the app’s owner group, never the proposer. |
Enforced |
DG-PROPOSER, DG-FILLED; gate.md |
| CLR-05 |
Tier 3 needs two people: one approver from the app’s owner group and one from the outside groups (group:default/admins, group:default/breakglass-reviewers), never the proposer and never one person twice. A change with no owner uses the admins group as owner. Tier 3 is never cleared by policy. |
Enforced |
hangar-dev cluster.yaml clearance.gateOutsideGroups; DG-TWICE, DG-FILLED |
| CLR-06 |
Approvers are people. A service account, GitHub Actions identity, bot or agent (*[bot], agent:*, a Kubernetes or ID-JAG issuer) cannot approve (DG-APPROVER-AUTOMATION), and one person under two names or sessions counts once (DG-APPROVER-SAME). A delegate (Tower’s backend) naming a person is not refused; the person named is tested. An alias is known only if the deployment lists it. |
Enforced |
gate.md: tier 3 approvers are people |
| CLR-07 |
A PR that looks like an agent’s but has no receipt is denied (DG-PROVENANCE). A confirmed agent PR is never cleared at tier 0 by policy alone. |
Enforced |
gate.md: tiers |
| CLR-08 |
Only a registered verifier, or a GitHub Actions workflow of the change’s own repository, can register a change (DG-REPO). Clearance matches a workflow by repository name and ignores GitHub’s immutable owner and repo ids, so repo:jfillman@7268337/hangar-infra@1433775017:... matches repo:jfillman/hangar-infra:*. The Hangar verifiers are app-*-cicd/pipeline-runner, hangar-prod’s repo-server plugin, clearance-deploy-scratch and hangar-infra. |
Enforced |
hangar-dev 55-clearance/clearance/application.yaml verifiers, githubActions.allowed; provenance-app.md |
| CLR-09 |
A change’s policy is recorded on every deploy.* record: the baseline version, the repo’s clearance.yaml commit and the offline admission verdict. The verdict is recorded, not enforced by Clearance; Glidepath’s policy-validation gate (GATE-18) is the check that blocks, subject to GATE-03. |
Display |
gate.md: the policy in the record |
| ID |
Rule |
Strength |
Held by |
| CLR-10 |
A promotion PR’s change must be cleared before the PR merges. Glidepath’s clearance-gate- check registers the change at the PR head and reports cleared, holding or denied; the Clearance check should be a required status check on the gitops repo (the owner is setting it on jfillman/gitops-gate-api). The check is also advisory in Glidepath’s registry (required: false, no hangar-dev override), so the promotion fan-in does not wait on it (GATE-13). |
Off here (GitHub Free cannot require a check on a private repo, so the check reports and nothing stops the merge; see the section 21 row) |
clearance-gate- check; Glidepath clearance-gate.md; gate.md: enforcement |
| CLR-11 |
If the PR merges before it is cleared, Argo CD’s plugin renders the merge commit, finds no cleared change for that tree, and registers a new one with every object treated as new. Its tier comes from the whole tree, its checks are missing so policy cannot clear it, and the sync holds for people. |
Enforced (the hold) |
clearedByTree, POST /v1/clearances with pr: 0; gate.md: the lookup by tree |
| CLR-12 |
A cleared, unexpired change for the same app, environment, cluster, repository and manifest tree covers the merge commit (clearedByTree): the commit and the PR it was cleared under do not matter, nothing new is registered, and a push of an already-cleared tree supersedes nothing. |
Enforced |
internal/gate/service.go clearedByTree; Glidepath’s task renders as <app>-<env> like Argo CD (G-ARGO-8) |
| CLR-13 |
A new head supersedes the older holding and cleared changes of the same PR, or, for a pushed commit, of the same app, environment and cluster. The superseded change’s token is revoked at once (deploy.revoke), and the old head cannot be rendered or deployed as cleared. |
Enforced |
gate.md: the state machine |
| CLR-14 |
An applied change stays valid for the objects it stamped until its successor applies: a superseded change with a recorded applied outcome keeps its token in the signed feed (it is no clearance and cannot render), and it is revoked when a later change for the same app, environment and cluster records an applied outcome. |
Building (clearance#113, not released; the incident is in section 21) |
clearance#113 |
Tokens, expiry, outage and break-glass
| ID |
Rule |
Strength |
Held by |
| CLR-15 |
A clearance is a signed token (DSSE, ECDSA P-256) bound to the tree, the object hashes, the cluster, the environment, the tier, the policy and the named approvers. It lives 15 minutes and is re-issued with the same nonce while its change stays cleared, up to the change’s horizon. A token whose nonce is not the live one is a replay. Every move out of cleared revokes it in the same transaction. |
Enforced |
CLEARANCE_TOKEN_TTL; gate.md: the clearance token, core.md |
| CLR-16 |
A change holding for people expires after 7 days (a repo’s ttlDays can only shorten it); a saved Terraform plan expires after 24 hours. denied, expired, superseded, degraded and exception are final: a new head registers a new change. |
Enforced |
gate.md: the state machine; terraform plan hold |
| CLR-17 |
When the core cannot be reached, tiers 0 and 1 are allowed and recorded, and tiers 2 and 3, and any tier a provider cannot know, are held. A repo can tighten tiers 0 and 1 to hold, never loosen 2 or 3 (DG-POLICY). |
Fails closed (tiers 2 and 3) |
gate.md: the outage policy |
| CLR-18 |
While the evidence ledger cannot be checkpointed (every witness failing, one rejecting the checkpoint, or records waiting too long), a tier-3 change whose approvals are all in stays holding. Tiers 0 to 2 are not held by it. |
Fails closed |
ledger.tier3Wait: true in hangar-dev’s Application; core.md: the outage knob |
| CLR-19 |
Break-glass is for admins only (admins: user:jfillman, or Tower’s backend naming one). It opens one window on one protected environment, for at most 4 hours, with a reason, and the breakglass.open record is written before any change it covers. It clears nothing and applies nothing: a change made in the window is still an exception, tagged with the window. When the window ends the core records what it holds and says what it cannot know. A report that arrives after the window closed is not tagged with it. |
Enforced |
breakglassMaxTTL: "4h"; gate.md: break-glass |
The Argo CD path (hangar-prod)
| ID |
Rule |
Strength |
Held by |
| CLR-20 |
On hangar-prod the gate covers one app, gate-api (clearance.gate.apps, protected namespace app-gate-api-prod). Other hangar-prod apps are not gated. |
Enforced for gate-api; Off here for the rest |
gitops-hangar-prod cluster.yaml clearance.gate |
| CLR-21 |
Argo CD’s Clearance plugin renders the chart, stamps every object with its hash, the tree and (once cleared) the grant, and replaces any grant annotation a chart wrote. A cleared tree syncs. A tree held for people syncs only after a PreSync Job (clearance-awaiting-approval) sees it cleared. A denied, expired, superseded, exception or degraded change, an outage hold, or a failed render is a ComparisonError and nothing syncs. |
Enforced (for Applications that use the plugin) |
gate.md: the plugin and the PreSync wait |
| CLR-22 |
The PreSync wait Job is not a control. argocd app sync --strategy apply skips hooks and so skips the wait. Admission is what refuses (CLR-24). |
Display |
gate.md: the PreSync wait |
| CLR-23 |
A cluster’s grants come only from the core’s signed grant feed, read by a listed feed reader (hangar-prod’s Argo CD agent only). The agent checks the signature, the issuer, its own cluster, freshness (5 minutes), that the sequence number does not go back within an epoch, that the tokens are exactly the feed’s entries, and each token. Any failure changes no grant. Grants past their own expiry are marked invalid on the agent’s clock; a grant no entry names is deleted. The agent never creates, updates or deletes a workload. |
Fails closed |
feedReaders in hangar-dev cluster.yaml; gate.md: the signed grant feed, the agent |
| CLR-24 |
The admission policy clearance-protected-writes admits a write to a protected namespace only if the object’s stamp is a hash listed by the namespace’s one active, valid grant (or another live cleared tree it carries). On hangar-prod it runs in Warn: it warns and audits and refuses nothing. |
Soft (Warn); Deny not enabled |
gitops-hangar-prod 56-clearance-gate/admission.yaml (validationActions: [Warn, Audit], clearance.gate.enforce) |
| CLR-25 |
Before the policy goes from Warn to Deny on hangar-prod, its false exceptions must be resolved, and accepting the flip is the owner’s decision alone. With Deny, a protected namespace with no active, valid grant refuses every matched write. |
Convention (owner decision, 2026-11-09 and 2026-11-10) |
owner; clearance.gate.enforce in gitops-hangar-prod cluster.yaml |
| CLR-26 |
A change that lands outside the gate is an exception: an object with no stamp, a stamp no valid grant lists, or content whose hash is not the stamp. The agent scans every 60 seconds and records one deploy.exception per finding, naming the writer. A change it names moves to exception and its token is revoked. |
Soft (detected and recorded, not stopped) |
gate.md: exceptions |
| CLR-27 |
An applied sync is reported (deploy.outcome) with the applied tree’s digest, and the core signs a change attestation for it with a short-lived certificate from the cluster’s own Fulcio, logged in its Rekor. |
Enforced (keyless is on in hangar-dev’s config; no live attestation was read for this page) |
hangar-dev clearance.attestation.keyless; core.md: change attestations |
| ID |
Rule |
Strength |
Held by |
| CLR-28 |
A saved Terraform plan is a change. clearance hold sends the plan’s digests, resource addresses, actions and changed attribute names, never a value; sensitive variables contribute their name only. clearance release lets the apply run only when the change is cleared and the plan file’s digest is the cleared one. The apply’s outcome is compared with the cleared digest, and a mismatch is a digest-mismatch exception that revokes the token. |
Fails closed |
terraform-gate.md: guardrails |
| CLR-29 |
In a protected workspace, any destroy or replace is tier 3 and so is any change to an IAM, security-group, network-ACL, KMS or database resource (a tag included); a -target or -replace plan is one tier higher. A destroy in a protected workspace never clears by policy. hangar-infra’s clearance.yaml protects network/* and observe/*. |
Enforced |
gate.md: the terraform-plan subject; hangar-infra clearance.yaml |
| CLR-30 |
The observed account’s collector infrastructure (observe/lab in hangar-infra) is applied only through the gated workflow (observe-lab.yaml): plan with a read-only role, hold in Clearance, apply with a separate role that only the lab environment job can assume. |
Enforced by the workflow; the apply role’s trust is read from the docs, not live |
hangar-infra docs/observe-aws.md |
| CLR-31 |
The logging account (observe/logging, the management account) is outside the gate. It is applied by hand with management credentials, because the point of that account is that production administrators and the gated workflow cannot write to it. |
Convention |
hangar-infra docs/observe-aws.md |
| CLR-32 |
In the private hangar-infra repo the lab environment has no Clearance App protection rule (GitHub Free), so the apply job starts at once and fails with “holding, not cleared” until a person clears the plan in Tower. Then the failed job is re-run (gh run rerun <run> --failed) within a day, while the plan artifact is kept and before the plan’s 24 hours end. |
Fails closed (the apply does not run); the protection rule is Off here |
hangar-infra docs/observe-aws.md step 4; CLR-16, CLR-28 |
| CLR-33 |
GitHub’s immutable OIDC subject is in force for hangar-infra: AWS role trust must name the repository’s immutable subject, repo:jfillman@7268337/hangar-infra@1433775017:environment:lab. A trust on repo:jfillman/hangar-infra:... fails with “Not authorized to perform sts:AssumeRoleWithWebIdentity”. |
Enforced (by AWS and GitHub; checked against GitHub’s subject setting, not the role) |
hangar-infra docs/observe-aws.md; gh api repos/jfillman/hangar-infra/actions/oidc/customization/sub |
| CLR-34 |
A GitHub Actions deployment to an environment that has Clearance’s App as a protection rule is approved when its change is cleared, rejected when denied, expired, superseded, degraded or an exception, and rejected (DG-UNREGISTERED) when no change matches the repository, commit and environment. While the change holds for people the deployment waits. |
Enforced where the rule is on (clearance-deploy-scratch); Off here on hangar-infra’s private lab (CLR-32) |
provenance-app.md: the deployment protection rule |
How the gate ships
| ID |
Rule |
Strength |
Held by |
| CLR-35 |
Clearance policy and tool changes ship as a Clearance release, then a version bump where they are consumed (hangar-dev pins the chart, targetRevision, and the image by digest; the Autopilot gateway bundle is vendored at its pinned version). They are never edited in a consumer. |
Convention |
Clearance CHANGELOG; gitops-hangar-dev 55-clearance/clearance/application.yaml |
20. Clearance: observation, evidence and the packet
Observation is what Clearance sees, whoever made the change: the Kubernetes audit collector (observe-k8s, on both
clusters), the AWS collector (observe-aws) and the pipeline behind them. Seeing is not clearing: every observation
is observed-not-attributed until an assessment says otherwise. Detail:
evidence.md,
observe-pipeline.md,
observe-k8s.md,
observe-aws.md.
What counts as an exception
| ID |
Rule |
Strength |
Held by |
| OBS-01 |
A change made outside the gate (kubectl apply, a console change) is an exception: it records the actor, the diff and a resolution. |
Soft (seen and recorded, not stopped; the stop is CLR-24, in Warn) |
observe-pipeline.md |
| OBS-02 |
A stamp on an object proves nothing by itself. A change is cleared only when its grant annotation names a grant the store knows and the live object’s hash is among that grant’s own rendered hashes. A grant with another hash is out-of-band (high), a grant the store does not know is out-of-band, and Argo CD’s controller writing with no grant is out-of-band. |
Enforced |
observe-pipeline.md: attribute |
| OBS-03 |
A change to a Secret is recorded without its value. The shipped audit policy records Secrets at Metadata at most; the receiver cuts any body before the queue write; the pipeline never reads, hashes or diffs one; the collector’s service account has no get, list or watch on Secrets; and the core refuses (400) any observation that carries a Secret body. A Secret change shows its namespace, name, actor, verb and time, and no key names. |
Fails closed |
observe-k8s.md: the guardrails; evidence.md |
| OBS-04 |
A collector that stops makes a blind window, never a clean one. A health record holds for twice its interval (default 15 minutes), and after that the feed is blind until the next. The packet and the coverage statement print “no exceptions detected” only beside a window in which every observed scope was healthy throughout; a scope with no collector reads blind. |
Fails closed |
evidence.md: health; clearance-api.md: coverage |
| OBS-05 |
The Argo CD agent’s exception and the collector’s observation of the same object are one exception: when they are within 15 minutes the row is the observation’s (actor and diff) with the gate record’s seq in also_reported_seq. Only out-of-band rows fold; a refusal, a monitoring operation and a break-glass window are never merged away. An apply followed within 15 minutes by a revert is one exception. |
Enforced |
observe-k8s.md: one exception, not two |
| OBS-06 |
An operation on the controls that watch the environment (AWS StopLogging, DeleteTrail, UpdateTrail, PutEventSelectors, recorder and rule deletes; Kubernetes webhook configurations and the audit-policy ConfigMap) is tier 3 whether or not it succeeded, alerts at once, and flips the scope’s health: blind when it succeeded, degraded when it was refused. Only the provider’s next healthy record clears the flip. It is never noise and never folded away. |
Enforced |
CLEARANCE_MONITORING_PROTECTION_OPS; evidence.md: operations on monitoring |
| OBS-07 |
A refused attempt (a 403, an errorCode) is recorded as attempted-refused and is never noise. Nothing landed, so a change it names is not moved to exception. |
Enforced |
observe-pipeline.md |
Noise and attribution
| ID |
Rule |
Strength |
Held by |
| OBS-08 |
A noise rule is one exact identity plus the exact shape it may touch (exact kinds and verbs), with a stated reason. The loader refuses anything wider: a pattern, a service-account group, “every service account of a namespace”, no kinds or verbs, an unconditional when, or a monitoring control. A rule that does not compile or is over the cost limit stops the whole bundle from loading; there is no partial rule set. The packet prints the count per rule. |
Fails closed |
observe-pipeline.md: noise (ErrBlanketNoise) |
| OBS-09 |
Routine controller webhook writes are named exceptions (monitoring.kubernetes_exempt), each with a name, a why, one exact subject, exact verbs, the two webhook kinds only and name globs. Only a succeeded write counts. Anything else on a webhook, another verb or name, a person, a refused attempt, and any write to kube-system/audit-policy, stays monitoring protection (OBS-06). A compromised named controller can change its own webhooks without an alert. |
Enforced |
observe-pipeline.md: named exceptions |
| OBS-10 |
Objects a controller creates for an object that a valid grant stamped (ReplicaSet, Pod, Job) take that grant. A controller write that leaves the rendered fields unchanged is attributed. A stamp naming a grant the collector does not know yet waits a settle window before it is judged. |
Building (not in Clearance main at 4ccf3dd; no open PR found) |
owner decision, 2026-11-10 |
| OBS-11 |
The AWS collector governs only its configured account (ruleset.governedAccounts, default the observed account). Attribution uses the exact session-issuer ARNs of the apply roles (ruleset.applyRoles, required) and the SSO admin role glob (ruleset.ssoAdminRoles). The root user is always an exception. A user-agent claim is self-reported, so a plan or grant named only there clears nothing: it needs the apply role and a recorded apply window. A Config item and the CloudTrail event that caused it are one change. |
Enforced in the chart and pipeline (built; not yet registered on hangar-dev, see section 21) |
observe-aws.md: the rule set |
| OBS-12 |
A successful StopLogging (or any other monitoring operation) flips the AWS account’s health to blind through the core, a refused one to degraded. The AWS collector’s health record covers trail enabled, regions, event selectors, digests arriving and validating, recorder running, rules enabled and collector current, and states what scope was not evaluated. |
Enforced (built; see section 21) |
observe-aws.md: health |
Providers, resolutions and who can read
| ID |
Rule |
Strength |
Held by |
| OBS-13 |
Collectors are read-only to the systems they watch and are registered providers with fixed record kinds. observe-k8s patches only its own heartbeat ConfigMap and canary Secret; observe-aws consumes only its own queue. A provider cannot write a kind its registration does not name, and the core sets provider itself; an event cannot. observe-k8s is registered for observation, assessment, resolution and observation.health; the AWS collector also reads clearance-api’s outcomes as an auditor. |
Enforced |
providers in hangar-dev 55-clearance/clearance/application.yaml; provider-protocol.md |
| OBS-14 |
A resolution is how an observed change ended. The collector posts the first one (still-live, or reverted when it saw the revert). A person sets or changes it, to reverted (with one of the fixed seven causes: agent-caused, policy-triggered, human-initiated, automated-remediation, requirement-changed, external-dependency, unattributed), ratified, declared-break-glass or still-live. Only admins can (DG-RESOLUTION; a service, an agent or a non-admin gets 403). Resolutions are append-only: the newest wins when read, and every earlier one stays in the chain with who set it. |
Enforced |
gate.md: a person’s resolution |
| OBS-15 |
Admins can also resolve gate exceptions (deploy.exception), not only observations. |
Building (today the route resolves observations only) |
owner decision, 2026-11-10 |
| OBS-16 |
Evidence read access: auditors (api.auditors) read everything, including the chain check and observations, health and coverage, which are auditor-only. A person reads their own sessions and evidence, their agents’, and records that target an app they own. Tower’s backend reads for the person it names. A grant-sync agent reads its own clusters’ feed and nothing else. |
Enforced |
clearance-api.md: who sees what; api.auditors, api.feedReaders |
| OBS-17 |
Evidence is one append-only hash chain with one writer (the core, one replica), each record tagging which fields the writer verified and which something else claimed. Free text given as an intent is stored only as a SHA-256. The chain’s hashes go into a Tessera log whose checkpoints hangar-prod witnesses and countersigns. |
Enforced |
evidence.md; ledger in hangar-dev’s Application |
| OBS-18 |
The evidence packet is built from the chain read from genesis and verified as it goes: a chain that does not verify makes no packet. It prints every coverage state, even at zero, and clearance verify rebuilds each scope’s coverage statement offline, so a packet that calls a stopped collector’s scope clean fails. The packet’s window defaults to 30 days (at most 366). The 30-day re-run of the live packet is due 2026-12-09. |
Enforced (the build and the verify); Convention (the re-run: no scheduled job ships) |
packet.md, verify.md |
21. Rules that are not enforced yet
Every place where a rule above is weaker than it reads, in one list:
| Gap |
Rules |
What it means today |
| GitHub Free can’t enforce rulesets or required checks on private repos. |
GATE-03, REL-03, API-12 |
Gates and sign-off report but don’t block. A bypass is detected and announced (GATE-04, REL-05). |
| A GitHub-merged release rests on a GitHub session. |
GATE-15 |
Decided 2026-10-20 (ADR-0027): an allowed merger’s GitHub session is enough to release a PR’s code, and in merger mode nobody gitsign-signed that code. On hangar-dev the path is on for every onboarded app (backstage, boarding-api, flight-api, gate-api), in merger mode with 0 approvals (one person merges). Raise requiredApprovals once a second real person merges; use strict where every author can run gitsign. |
| The ruleset assumes every PR is a release PR. |
GATE-03 |
Once enforced, non-release PRs would wait forever for gates that never run. Fix before enabling GATE-03 (ruleset audit). |
| App repos aren’t gated. |
ENV-02 |
By design: Ground is self-service. Only gitops repos get release gates. App repo PRs run cicd-validate (API-12) and the agent gates, which report but don’t block (GATE-03). |
itsm is real only with a provider. |
GATE-01 |
This lab points it at a mock change tool (CHG-01 to CHG-06 hold against the mock). |
policy-validation checks only validating policies. |
GATE-18 |
ImageValidatingPolicy, mutating policies, parameterised ValidatingAdmissionPolicies and Pod Security Admission labels are not evaluated; admission still enforces them at sync. Every policy on hangar-prod today is a break-glass rule, so every current release passes until app-facing policies land in 30-policy/. |
qa is advisory on hangar-dev. |
GATE-12, GATE-13 |
required: false until every app with a Flight environment has a check ride (boarding-api has one). The check runs and reports; a missing ride does not fail it. |
| The check-rides repository’s owners are not enforced. |
TEST-04 |
A private repo on GitHub Free: code-owner review is convention. The repo holds no secrets and can be made public. |
| An app opens its NetworkPolicy to tests by hand. |
TEST-07 |
A platform default for the testkube namespace is on the test framework roadmap. |
| Production SLO burn isn’t read during the bake. |
REL-13 |
Production’s Prometheus is on and canary analysis uses it, but the outcome is still decided from the rollout alone, so “clean” can’t yet mean “no SLO burn during bake”. |
| Build signatures aren’t checked at admission. |
SC-05 |
hangar-prod checks the promotion attestation (PROM-05), not the Chains provenance; hangar-dev checks nothing, so an unsigned image could run there if it got into git. |
| The promote workflow trusts whoever starts it. |
PROM-02, PROM-03, PROM-05 |
The workflow checks its input’s shape, not the gates. Anyone who can start it (the promoter App through the broker, or riley-roooo, who owns the repo) gets a valid promotion attestation for any Ground digest, and PROM-05 would admit it. Glidepath only starts it after PROM-03. |
| Flight admission depends on Kyverno being up. |
PROM-05 |
failurePolicy: Fail: while hangar-prod’s Kyverno admission controller is down, new pods in tenant namespaces are refused. It restarted 354 times between 2026-10-18 and 2026-10-30. |
| Flight retention is off. |
PROM-09 |
Flight keeps every promoted image until promotion.retention.enabled is turned on (dry run first: neither the package listing with the read credential nor deletion with the workflow’s token has been tried). |
| Cloud environments can’t be Flight while promotion is on. |
PROM-10 |
Refused rather than promoted. No app has a cloud Flight environment today; cloud promotion is a recorded Glidepath follow-up. |
airframe-validate doesn’t run on app repos or the tenants repo. |
GATE-08, CRED-01 |
Airframe roadmap AF-4b. |
Security review and human extraManifests/networkPolicy/httpRoute changes are convention. |
AF-06, AF-07 |
Agents are held to AF-06 by Clearance’s field deny list (AI-04), and tier 3 needs an outside approver for Secrets and RBAC in a protected environment (CLR-05), but nothing requires a security reviewer for XRD or composition changes, and a person’s AF-06 change is not checked. |
| Azure deploys use a stored secret. |
CRED-07 |
Lambda and ECS deploys are keyless (2026-10-16/17). Azure Container Apps still logs in with a stored service principal secret; next is workload identity federation (Glidepath known gap 43). |
| Clearance’s merge-time check cannot be required. |
CLR-10 |
clearance-gate- reports on a release PR but GitHub Free cannot require it on a private repo (the rulesets and branch-protection APIs answer 403 “Upgrade to GitHub Pro” for the four gitops repos checked on 2026-11-10), and it is advisory in Glidepath’s registry. The owner is setting it as a required status check on jfillman/gitops-gate-api; the other gitops repos have none. A PR merged before it is cleared is held at sync (CLR-11), not at merge. |
| Hangar-prod’s admission policy is in Warn. |
CLR-24, CLR-25 |
clearance-protected-writes warns and audits and refuses nothing. The flip to Deny waits on resolving its false exceptions and on the owner’s decision. Until then the gate holds only what Argo CD’s plugin holds (CLR-21), and a sync that skips the plugin’s hook is not stopped (CLR-22). |
| An applied change goes invalid when a newer head supersedes it. |
CLR-13, CLR-14 |
hangar-prod, 2026-11-10: change ch-1dda08661ae5b7b1 was cleared and applied, then superseded by a later pushed head; its token was revoked and its grant left the cluster, and the agent raised a false deploy.exception (seq 9723, as reported by the owner; not re-read for this page) for the live objects until the new head applied. The fix, clearance#113, is not released. |
| Controller-derived objects raise false exceptions. |
OBS-10 |
A ReplicaSet, Pod or Job a controller creates for a stamped object, and a controller write that leaves the rendered fields unchanged, can read as out-of-band until OBS-10 ships. |
| Only gate-api is gated on hangar-prod. |
CLR-20 |
clearance.gate.apps: [gate-api], protected namespace app-gate-api-prod. Other hangar-prod apps deploy ungated, and hangar-prod’s own protected list is hangar-prod/prod, narrower than the core’s. |
| Clearance traffic between clusters is plain HTTP. |
CLR-21, CLR-23 |
hangar-prod’s plugin, wait Job and agent reach the core and clearance-api at http://clearance.hangar-dev.local and http://clearance-api.hangar-dev.local. The tokens are short-lived projected ServiceAccount tokens and the grant feed is signed, but the traffic is readable on the LAN. |
| An outage let-through leaves no record. |
CLR-17 |
The Argo CD plugin keeps no outbox, so a tier 0 or 1 change allowed while the core was down is not followed by a deploy.degraded record. |
| The Terraform lab apply is held by a failing job, not a protection rule. |
CLR-32, CLR-34 |
lab in the private hangar-infra repo has no Clearance App protection rule, so a person clears the plan and re-runs the failed apply job within a day. |
| The AWS collector is built, not yet live. |
OBS-11, OBS-12, OBS-13 |
observe-aws is not registered in hangar-dev’s providers or auditors (gitops-hangar-dev 446ea51), its Terraform is in hangar-infra (observe/), and Clearance’s own doc calls it unverified against a real trail, queue, Config recorder and AWS-signed digest. The rules hold in the code and tests; no live exception from AWS has been read for this page. |
| Kubernetes audit flags are node work. |
OBS-01, OBS-04 |
The collector is enabled on both clusters (clearance.observe.enabled), but the API server’s audit flags are set by hand on each control-plane node (tools/observe-audit) and were not read for this page. Clearance’s observe-k8s.md still says “not yet rolled out”. A collector with no audit feed reads blind (OBS-04), never clean. |
| Approver aliases are only known if listed. |
CLR-06 |
The gate has no directory of a person’s other names. A person who signs in under a second identity that no alias names counts as two approvers. |
| The 30-day packet re-run is manual. |
OBS-18 |
No scheduled job ships for clearance packet (due 2026-12-09). |
| Only admins can run software templates. |
ACC-02 |
App teams can’t scaffold a new service themselves. Intentional for the lab; revisit before onboarding other teams. |
| The API trusts the delegate’s person check. |
API-04 |
A write from Tower or Clearance names a person, but the API can only authorize the delegate. If Tower’s owner check (ACC-03) were wrong, the API would still act; the audit line records both names. |
| People’s route is plain HTTP in this lab. |
API-10 |
The Gateway listener has no TLS, so an ID token crosses the LAN in the clear, as it does for Clearance and Dex (Clearance’s sign-in issuer and gateway URL are http://). Use it on a trusted network until the Gateway has TLS. |
| CI/CD events are best effort. |
API-11 |
Each replica keeps the last 2000 events, for 24 h at most, in memory. If both replicas restart at once, a subscriber that was away misses events. Durable fan-out over AMQP is designed and deferred (Glidepath known gap 44, ADR-0030). |
22. Keeping this page true
- A change that adds, changes or removes a rule updates this page in the same change: here when the change is in
this repo, or with a linked Hangar PR when it isn’t. The component’s own docs keep the detail.
- A rule that exists only in code is a bug in this page. Add it with the next free ID in its section.
- A retired rule keeps its row, struck through, with the date and the reason. Its ID is never reused.
- The commit list at the top says what this page was last checked against. Re-check it after any release of Tower,
Glidepath, Airframe or Backstage that touches permissions, gates, release records or admission policies.