Turning On GitOps Enforcement¶
This guide is for the platform administrator who runs helm upgrade on a
Cloud-Native DCS cluster and wants to flip on gitopsEnforcement.enabled
without breaking the people already using the system.
For the why of the design, see GitOps for Automation Engineers. For the what of the contract, see the GitOps Enforcement RFC.
What enforcement does¶
When gitopsEnforcement.enabled=true, the chart installs a
ValidatingAdmissionPolicy (CEL, GA in Kubernetes 1.30+) that rejects writes
to engineering-class CRDs unless the requester is one of:
- the GitOps reconciler ServiceAccount configured under
gitopsEnforcement.reconciler.serviceAccount - a ServiceAccount in the chart's install namespace
(
system:serviceaccounts:<release-namespace>), which covers all in-cluster DCS operator reconciles (batch / physical / procedural / control) - a member of the
dcs.io:break-glassgroup bound to thegitopsEnforcement.breakGlassRoleClusterRole system:masters(the cluster's built-in admin escape)
The in-cluster operator carve-out exists because reconcile loops legitimately
write to engineering CRDs (e.g. batch-operator annotating a Unit during
allocation). Without it, batches stall in Allocating as soon as enforcement
is turned on (see #372).
The carve-out is namespace-scoped: only SAs in the chart's install namespace
qualify, and an arbitrary SA elsewhere in the cluster stays outside it.
Operational-class CRDs (Batch, Phase, AuditRecord, etc.) accept writes from any authenticated caller, exactly as they did before enforcement was on.
The classification is carried on every CRD as a label
(dcs.io/ownership-class=engineering|operational|hybrid). See
#283 for
the full taxonomy.
Reconcile engine¶
The admission policy is engine-neutral: it allowlists a ServiceAccount
identity (gitopsEnforcement.reconciler.serviceAccount) and does not care
what drives it. Point it at Flux's kustomize-controller, ArgoCD's
argocd-application-controller, a ticket-system bridge SA, or a human's
time-bounded apply identity, and enforcement behaves identically.
Declare the engine so the chart can reason about the other half of the compliance story, audit linkage:
gitopsEnforcement:
reconciler:
engine: flux # flux | argocd | servicenow | other
Audit linkage, the §11.10(k) loop that stamps each
reconcile with its Git commit SHA, is currently implemented for Flux
only (#816).
To keep neutrality honest on the record, the chart fails the render
if you enable enforcement with a non-flux engine:
gitopsEnforcement: reconciler.engine="argocd" has no audit-linkage adapter.
The 21 CFR 11 §11.10(k) traceability loop … will NOT close on this engine …
You have two ways forward:
- set
reconciler.engine=fluxand configure audit linkage, or - set
reconciler.acknowledgeNoAuditLinkage=trueto run enforcement without linkage. You get the admission gate on any engine, and explicitly accept that no SHA-stampedAuditRecords are produced until an adapter ships.
Pre-flight checklist¶
Before you flip the switch:
| Check | Why |
|---|---|
| Kubernetes >= 1.30 | ValidatingAdmissionPolicy is GA at v1; older clusters reject the resources |
| Flux (or equivalent) deployed | The reconciler SA is the only writer the policy admits — without it, all engineering writes are blocked |
gitopsEnforcement.reconciler.serviceAccount.{namespace,name} matches reality |
Verify with kubectl get sa -n flux-system (or your engine's namespace) |
gitopsEnforcement.breakGlassRole exists or is acceptable as auto-created |
The chart creates it if absent. To use a custom name, set the value before upgrade |
| Real humans bound to the break-glass role | Bind via ClusterRoleBinding whose subjects include both the user and the dcs.io:break-glass group |
gitopsEnforcement.classes.engineering.mode chosen |
propose-only is the default; read-only is stricter; direct-write installs no policy |
dcs admin gitops-preflight passes |
Tracked as follow-up to #288; when it lands, run it before every enable |
Step-by-step¶
-
Stage the change in a values file.
# values-prod-gitops.yaml gitopsEnforcement: enabled: true reconciler: engine: flux serviceAccount: namespace: flux-system name: kustomize-controller breakGlassRole: dcs:admin:break-glass classes: engineering: mode: propose-only -
Bind a real human to the break-glass role.
# break-glass-binding.yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: dcs-break-glass-oncall roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: dcs:admin:break-glass subjects: - apiGroup: rbac.authorization.k8s.io kind: User name: oncall@example.com - apiGroup: rbac.authorization.k8s.io kind: Group name: dcs.io:break-glassThe
dcs.io:break-glassgroup must be present, because the CEL policy reads it out ofrequest.userInfo.groupsto grant the override. -
Helm upgrade.
helm upgrade --reuse-values --install cloud-native-dcs \ oci://ghcr.io/cloud-native-dcs/charts/cloud-native-dcs \ --version <pinned-version> \ -f values-prod-gitops.yamlConfirm the installed resources:
kubectl get validatingadmissionpolicy,validatingadmissionpolicybinding \ -l app.kubernetes.io/instance=cloud-native-dcs kubectl get clusterrole dcs:admin:break-glass
-
Smoke-test the deny path.
Apply an engineering CRD as a non-Flux user. The expected outcome is a
Forbiddenfrom the admission policy with apropose-onlymessage pointing at the ChangeRequest workflow.kubectl --as=alice apply -f some-master-recipe.yaml # Error from server (Forbidden): Engineering CRD writes require a # ChangeRequest in propose-only mode...Impersonate someone who is actually authorised. The user you test as must already hold RBAC to write the CRD, or the request never reaches admission and you are testing the wrong thing:
# alice with no RBAC — this is NOT an enforcement result Error from server (Forbidden): masterrecipes.recipe.dcs.io is forbidden: User "alice" cannot get resource "masterrecipes" in API group "recipe.dcs.io"Both messages say
Forbidden, and only the second names the policy. A plain RBAC refusal looks like a passing test and proves nothing about enforcement, so read the message itself. The exit code cannot tell the two refusals apart. Test as an engineer who could write the CRD before enforcement was enabled: that is the person the policy exists to redirect. -
Smoke-test the allow path.
Verify Flux's existing reconcile still admits writes. Flux logs should show successful applies on the next reconcile cycle. If they fail, the most likely cause is
reconciler.serviceAccount.namespaceornamenot matching the real reconciler SA.
Hybrid-mode notes¶
For the MVP, hybrid mode rejects all spec writes from non-Flux SAs: it
treats hybrid CRDs identically to engineering. Field-level enforcement (allow
the dcs.io/command annotation and named UI-mutable fields while gating the
spec) is a follow-up, tracked in
#288.
If you have UI-driven workflows that mutate Batch.spec.parameterOverrides
or set the dcs.io/command annotation directly, leave
classes.hybrid.mode=direct-write (the default) until the field-level policy
ships.
Rollback¶
Set gitopsEnforcement.enabled=false and helm upgrade. The
ValidatingAdmissionPolicy, its binding, and the break-glass ClusterRole
are removed by Helm. Any humans bound to the break-glass role retain their
binding (Helm does not own those ClusterRoleBinding resources). That is
intentional, because the on-call rotation should not be coupled to the chart
release lifecycle.
Related Documentation¶
- Change Control for Production ControlPrograms — the webhook-driven §11.10(k) gate on control-logic edits. GitOps enforcement is complementary: it gates the structural CRDs that engineers write directly via UI / CLI / Flux, while the change-control webhook gates control-program mutations regardless of carrier.
- 21 CFR Part 11 traceability
- GitOps Runbook
- Promote a Recipe — the user-facing how-to that exercises the enforcement you just enabled.