Governance Basics
This section covers the concrete artifacts of container governance: the Dockerfile rules every team follows, the manifest fields every workload sets, and the automation that checks both.
Governance here means encoded, testable standards - not a wiki page nobody reads.
Prerequisites
- Kubernetes 1.36.2 cluster access (supported minors are 1.34-1.36; 1.33 is EOL).
- Docker Engine 29.6.1 with BuildKit as the default build backend.
- Helm 3 and Kustomize for packaging.
- A policy engine in the cluster (Kyverno or OPA Gatekeeper) and a scanner (Trivy or Grype).
kubectl,kubeconform, andhadolintavailable on CI runners.
Quick install of the local checkers:
brew install hadolint kubeconform
docker run --rm aquasec/trivy:latest --versionBasic Examples
1. A baseline Dockerfile every team starts from
Pin the base by digest, build as a non-root user, and keep the runtime stage minimal.
# syntax=docker/dockerfile:1
FROM node:22-bookworm-slim@sha256:aaaa1111bbbb2222cccc3333dddd4444eeee5555ffff6666aaaa7777bbbb8888 AS build
WORKDIR /src
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
FROM node:22-bookworm-slim@sha256:aaaa1111bbbb2222cccc3333dddd4444eeee5555ffff6666aaaa7777bbbb8888
WORKDIR /app
COPY --from=build /src /app
USER 10001
CMD ["node", "server.js"]- The
# syntaxline opts into the current Dockerfile frontend so BuildKit features stay available. - The digest pin makes the build reproducible; a floating tag makes it a lottery.
USER 10001sets a numeric UID, which lets Kubernetes enforcerunAsNonRootwithout resolving a username.- Multi-stage keeps build tooling out of the shipped image.
2. Lint the Dockerfile in CI
Fail the build on rule violations rather than reviewing them by hand.
hadolint --failure-threshold error Dockerfilehadolintcatches unpinnedapt-get install, a missingUSER, andADDwhereCOPYbelongs.--failure-threshold errorlets style warnings pass while real defects block.- Commit one
.hadolint.yamlper org so every repo inherits the same ignore list. - Run it before the image build so failures come in seconds, not minutes.
3. The manifest fields every workload sets
Requests, limits, and probes are the minimum contract for a scheduled workload.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout
spec:
replicas: 3
selector:
matchLabels:
app: checkout
template:
metadata:
labels:
app: checkout
spec:
containers:
- name: app
image: registry.example.com/checkout@sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
readinessProbe:
httpGet:
path: /readyz
port: 8080
livenessProbe:
httpGet:
path: /healthz
port: 8080- CPU requests drive scheduling; with no request the pod is BestEffort and first to be evicted.
- A memory limit bounds the node; a CPU limit is optional and often causes needless throttling.
- Readiness gates traffic and liveness restarts a wedged process, so they should not share an endpoint.
- Referencing the image by digest means the tag cannot be moved under you.
4. Apply the restricted Pod Security Standard by namespace
Pod Security admission is built in and needs only labels.
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.36
pod-security.kubernetes.io/warn: restrictedenforcerejects violating pods at admission;warnonly prints a message to the client.- Pinning
enforce-versionstops a cluster upgrade from silently tightening the rules. - Roll out with
warnfirst, read what it reports, then flip toenforce. - The restricted profile requires
runAsNonRoot, dropped capabilities, and a seccomp profile.
5. Meet restricted with an explicit securityContext
The namespace label sets the bar; the pod has to clear it.
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]allowPrivilegeEscalation: falseblocks setuid binaries from gaining privileges.- Dropping
ALLcapabilities is required by restricted; add back only what a workload proves it needs. readOnlyRootFilesystemis not required by restricted but is cheap hardening.- Put these fields in a shared Helm library chart so no team hand-writes them.
6. Validate manifests before they reach the cluster
Schema validation catches typos that a policy engine would otherwise catch too late.
kubeconform -strict -kubernetes-version 1.36.2 -summary manifests/
kubectl apply --dry-run=server -f manifests/-strictrejects unknown fields, which is how most silent misconfigurations start.- Server-side dry-run also runs admission webhooks, so policy failures surface in CI.
- Pin the Kubernetes version so CI validates against what production actually runs.
- Run both: the schema check is offline and fast, dry-run needs cluster credentials.
7. Standardize labels for ownership
Governance depends on knowing who owns each object.
metadata:
labels:
app.kubernetes.io/name: checkout
app.kubernetes.io/part-of: payments
app.kubernetes.io/managed-by: Helm
annotations:
owner-team: payments-platform
slack-channel: "#payments-oncall"- The
app.kubernetes.io/*keys are the recommended common labels and existing tools already read them. - Ownership metadata is what turns a scanner report into an actionable ticket.
- Keep free-form contact data in annotations, not labels, since labels are indexed and length-limited.
- Enforce presence with a policy rule, starting in non-production.
Intermediate Examples
8. Enforce a standard with Kyverno instead of a review comment
A policy applies the rule to code nobody reviewed.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-owner-annotation
spec:
validationFailureAction: Enforce
rules:
- name: check-owner
match:
any:
- resources:
kinds: ["Deployment", "StatefulSet"]
validate:
message: "owner-team annotation is required"
pattern:
metadata:
annotations:
owner-team: "?*"validationFailureAction: Enforceblocks the request;Auditrecords a policy report instead.- Start every new policy in
Audit, read the reports for a week, then enforce. ?*means "any non-empty value" in Kyverno's pattern matching.- Scope with namespace selectors so a policy can land on one team before all of them.
9. Ship the standards as a Helm library chart
Make the compliant path the shortest path.
# charts/org-base/templates/_security.tpl - consumers include this named template
{{- define "org-base.podSecurity" -}}
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
{{- end -}}- A library chart exports named templates and is never installed on its own.
- Teams get new defaults by bumping a dependency, not by editing YAML in twelve repos.
- Version the chart and publish a changelog, because standards need release notes like any API.
- Pair the chart with the policy: the chart makes compliance easy, the policy makes it mandatory.
10. Gate the image, not just the manifest
Scan and sign before anything reaches a registry teams can deploy from.
trivy image --exit-code 1 --severity HIGH,CRITICAL registry.example.com/checkout:build-123
cosign sign --yes registry.example.com/checkout@sha256:1111222233334444555566667777888899990000aaaabbbbccccddddeeeeffff- Scan by digest so the artifact you scanned is the artifact you sign.
--exit-code 1turns a report into a gate; without it the scan is decoration.- Verify signatures at admission so an unsigned image cannot be scheduled.
- Remember the runtime split: Docker builds this image, containerd runs it on nodes via CRI.
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).