Docs · Glidepath
ADR-0028: The policy-validation gate evaluates the release offline against the target cluster’s policies from git
Status: Accepted (2026-10-26). Built: policy-validation-check / policy-validation-gate, the broker’s
/policy-bundle, the Kyverno CLI in the toolbox, policyValidation in glidepath-control-plane. Detail:
policy-validation.md.
Context
policy-validation was the last stub release gate (ADR-0003): the shared governance-check Pipeline, always green.
Its stated intent was to evaluate a release’s rendered manifest against the target cluster’s admission policies
before the merge, so that a release the cluster would refuse is caught on the PR, not as a failed Argo CD sync after
it.
Four questions decide the shape:
- Which policies. The target cluster’s, not a platform-wide set: Flight clusters carry their own (hangar-prod’s
are native ValidatingAdmissionPolicies, hangar-dev’s Kyverno ValidatingPolicies). Apron renders them into the
cluster’s GitOps repo under
30-policy/, and the cluster’s own Argo CD syncs that repo’smain. Git is the desired state; the live cluster lags it at most by a sync. - How to read them. Upper-zone clusters take changes only through a reviewed merge their own Argo CD syncs
(ADR-0024): no pipeline credential to them exists, and none should be added for a read. The GitOps repo is
readable by the platform’s GitHub App, but it also holds per-cluster generated passwords (committed by design,
Apron
cluster.yaml), and an app’s pipeline namespace runs the app team’s own build scripts under the same ServiceAccount the gate uses. A repo token handed to the gate is a token handed to every app. - How to render. The values gate already renders a release (the validator image: helm and the airframe-application chart at the cluster’s default-chart tag; an own chart fetched per ADR-0023).
- How to evaluate. The clusters run Kyverno 1.19; its CLI evaluates ValidatingPolicy, ClusterPolicy and native
VAPs offline, with two gaps found while building this (both covered by the gate’s tests): a policy with
background evaluation off, which is every admission-only policy, is skipped; and a native VAP’s
matchConstraintsare ignored, so it is applied to every resource.
Decision
- Source: the cluster’s GitOps repo, at the revision its Argo CD syncs.
policyValidationin glidepath-control-plane:repoURL(defaulthttps://github.com/{owner}/gitops-{cluster}.git),path(30-policy),revision(main), per-cluster overrides.{cluster}is the environment’s cluster, the first path segment of<cluster>/<env>/values.yaml. - The broker reads it; the gate gets documents, never a token.
POST /policy-bundle {cluster, owner}on the existing broker (it already holds the GitHub App key): TokenReview with audiencepolicy-bundle, the caller’s namespace must hold a PaC Repository CR, acontents: readinstallation token scoped to that one repo that never leaves the broker, and only ValidatingPolicy / NamespacedValidatingPolicy / ClusterPolicy / Policy / ValidatingAdmissionPolicy / ValidatingAdmissionPolicyBinding documents in the response, with the commit read. Policies are not secret, so any Glidepath app may read any cluster’s bundle. - Render like the values gate. Same validator image and chart resolution; the Flight ApplicationSet’s injected
values and value files (base < values < release); namespace
<type>-<app>-<env>. Every environment the PR changes, and every environment under a changed<cluster>/base.yaml. - Normalize, then evaluate with the Kyverno CLI at the clusters’ version (1.19.0, in the toolbox). Background
evaluation is turned on in the evaluated copy; each VAP binding becomes the equivalent ValidatingPolicy (same CEL,
the binding’s actions, its selectors); stand-in CRDs for custom kinds (Airframe’s XRD plurals, else the API
server’s guess); the rendered Namespaces’ labels feed
namespaceSelector. - Only the policy’s own mode blocks. A failure under
Deny(KyvernoEnforce) fails the check;Audit/Warnfailures and rules offline evaluation cannot decide (error, typically a field the API server would default) are warnings. A render or evaluation that fails outright fails the check: an unreadable policy source is not a pass. - Registry:
class: integrity,required: true,targets: [k8s-rollout], no rollback-advisory copy. Likevalues, it judges whether the release can be applied, not the image; admission makes no exception for a rollback. Required from the start because the policy mode already separates blocking from reporting, and a Deny violation would fail the sync anyway; one cluster can still make it advisory throughreleaseGuardrailOverrides.
Consequences
- A release the target cluster would refuse is red on its PR with the policy, rule, resource and message.
- What the gate sees is what is in git: a policy applied by hand to a cluster, or one outside
path(pipeline-runner-application-refresh-onlyon hangar-dev lives in the control-plane chart), is not evaluated. ImageValidatingPolicy (needs registry access), mutating policies, VAPs withparamKind, and Pod Security Admission labels are not evaluated. - Offline evaluation has no API-server defaulting and no
request.userInfo; policies that guard optional fields withhas()(the platform rule since 2026-10-11) evaluate exactly; others surface as warnings. - Today’s Flight policies (break-glass identity rules) do not match application objects, so every current release
passes; the gate starts mattering the day an app-facing policy lands in
30-policy/. - The repo naming default (
gitops-{cluster}) is a convention, not read from the fleet file; a cluster with another repo name needs an override. Adding the cluster repo URL to the fleetclusters.yamlrecord would make this generator-driven.