The Exam Blueprint · D5 · Security & Policy Enforcement · 15%

Security & Policy Enforcement

A golden path is only golden if it’s safe to drive fast on. These guardrails let Dot ship in minutes without a 2am breach page: proving who every service is (mTLS and identity), limiting what each person and workload can do (RBAC and least privilege), checking every change before it lands (admission control and policy-as-code), and trusting what you run (a signed, scanned supply chain). Done right, security isn’t a gate people route around but the paved shoulder keeping them on the road.

☺ Explain it like I’m 10

Imagine a skate park where kids zoom as fast as they like — because there are helmets at the gate, padded walls, and a guard checking everyone belongs. The rules don’t slow the fun — they’re why nobody breaks an arm and the park never closes. Platform security works the same: locked doors between rides (mTLS), name badges that open only your locker (RBAC), a bag-check that turns away anything dangerous (admission control), a “where did this come from?” sticker on every part (supply-chain signing), and a lifeguard watching for trouble (runtime detection). All of it exists so Dot can go fast safely.

🐢🐦Your hosts for this topic: Timmy the Turtle & Pip the Hummingbird — Timmy is slow, careful, and builds the guardrails (RBAC, policy-as-code, supply-chain checks), while Pip is the fast connector who secures the wires between services with mutual TLS and a service mesh.

Zero trust: never trust, always verify

☺ Like you’re 10: Being inside the building doesn’t mean you can open every door — everyone shows a badge at every door, every time.

The old model was a castle: a hard wall around the cluster, everything inside trusted. That fails the moment one pod is compromised — the attacker is now “inside the wall.” Zero trust flips it: nothing is trusted for where it comes from; every call must prove its identity and be explicitly authorised, every time. On a shared multi-tenant platform that’s not optional — one tenant’s bug must never become another’s breach.

◆ Key idea

Security on a platform is defense in depth: identity, network, RBAC, admission policy, supply chain, and runtime are independent layers. None is trusted to be perfect — each assumes the one before it may have failed. An attacker must defeat all of them; a defender need get only one right at the critical moment.

mTLS with a service mesh (Pip’s wires)

☺ Like you’re 10: Two services don’t just talk — they check each other’s ID cards and whisper through a tube nobody else can hear.

Plain HTTP between services is a postcard: anyone on the path can read it or forge the sender. Mutual TLS (mTLS) fixes both — traffic is encrypted, and both ends present a certificate, so each proves its identity to the other. Per-app that’s miserable, so a service mesh does it: a sidecar (or per-node proxy in “ambient” mode) beside each pod transparently upgrades every connection to mTLS. The exam names two:

Both derive a workload’s identity from its Kubernetes ServiceAccount (a SPIFFE identity), so “who is this service?” has a cryptographic answer, not an IP you must trust. Here, Istio requires mTLS in a namespace, then lets only the frontend account call checkout:

apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: payments
spec:
  mtls:
    mode: STRICT              # reject any non-mTLS traffic in this namespace
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: checkout-allow-frontend
  namespace: payments
spec:
  selector:
    matchLabels: { app: checkout }
  action: ALLOW
  rules:
    - from:
        - source:
            # a principal is the SPIFFE ID minus its scheme, i.e.
            # spiffe://cluster.local/ns/<namespace>/sa/<serviceaccount>
            principals: ["cluster.local/ns/storefront/sa/frontend"]
      to:
        - operation:
            methods: ["POST"]
            paths: ["/api/charge"]

Read aloud: “in payments, refuse un-encrypted traffic, and only let frontend POST to /api/charge on checkout.” Every other caller is denied by default.

cert-manager: who mints the certificates

mTLS depends on a steady supply of short-lived certificates, and hand-rotated ones always expire at the worst moment. cert-manager is the Kubernetes-native certificate robot: you declare an Issuer / ClusterIssuer (backed by an internal CA, Vault, or an ACME provider like Let’s Encrypt) and a Certificate, and it obtains, stores it in a Secret, and renews before it lapses. It secures ingress TLS, and via istio-csr can even back the mesh’s workload identities — no more expired-cert outages.

NetworkPolicy: the network-layer complement

☺ Like you’re 10: Even with ID checks at every door, you brick up the hallways nobody should walk down — so a burglar can’t wander room to room.

mTLS secures traffic that is allowed; a NetworkPolicy decides which traffic may exist at all, at L3/4. It limits blast radius: if one pod is compromised, policy stops it reaching every other. The golden-path pattern is default-deny — deny everything in a namespace, then open only the flows each app needs:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}                 # an empty selector = every pod in the namespace
  policyTypes: [Ingress, Egress]  # with no rules below, all in/out traffic is denied
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-checkout
  namespace: payments
spec:
  podSelector:
    matchLabels: { app: checkout }
  policyTypes: [Ingress]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels: { team: storefront }
      ports:
        - protocol: TCP
          port: 8080
⚠ Watch out

A plain Kubernetes cluster is allow-all by default — with no NetworkPolicy, any pod can reach any other. And NetworkPolicy only covers L3/L4; it needs a CNI that enforces it (Calico, Cilium). One more trap: a default-deny that includes Egress also blocks DNS, so pair it with a rule allowing port 53 to the cluster DNS pods or every lookup in the namespace fails. Run mesh authorization (L7 identity) and network policy (L3/L4) together; they guard different layers, and you want both.

RBAC and least privilege

☺ Like you’re 10: Everyone gets a key that opens exactly the doors their job needs — and not one more.

Once a request is authenticated (“who are you?”), Kubernetes asks “are you allowed to do this?” That’s Role-Based Access Control (RBAC), governing humans and workloads through four small, composable objects:

ObjectGrantsScope
RoleA set of verbs (get, list, create, delete…) on resources (pods, secrets…)One namespace
ClusterRoleThe same, but usable cluster-wide (and for cluster-scoped resources like nodes)Whole cluster
RoleBindingAttaches a Role (or ClusterRole) to subjects in one namespaceOne namespace
ClusterRoleBindingAttaches a ClusterRole to subjects across the whole clusterWhole cluster

Subjects come in three kinds: users and groups (people, authenticated by your identity provider — Kubernetes has no built-in user database) and ServiceAccounts (the identity a pod runs as). RBAC is purely additive — no “deny” rules, so a subject can do only what a binding grants. Below, checkout gets read-only config access in its namespace, nothing else:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: checkout
  namespace: payments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: config-reader
  namespace: payments
rules:
  - apiGroups: [""]                       # "" = the core API group
    resources: ["configmaps"]
    verbs: ["get", "list", "watch"]       # read-only; no create/update/delete
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: checkout-config-reader
  namespace: payments
subjects:
  - kind: ServiceAccount
    name: checkout
    namespace: payments
roleRef:
  kind: Role
  name: config-reader
  apiGroup: rbac.authorization.k8s.io

The governing principle is least privilege: narrowest verbs, fewest resources, smallest scope, namespaced Roles over ClusterRoles. Two habits matter. Don’t let workloads use the default ServiceAccount — give each its own, and set automountServiceAccountToken: false where a pod needs no API access. And be ruthless about the top of the ladder.

⚠ Never hand out cluster-admin

The built-in cluster-admin ClusterRole is a skeleton key to everything — read every Secret, delete any namespace, disable your own guardrails. Gizmo’s favourite “fix” is a ClusterRoleBinding granting it to a whole group “so people stop filing access tickets” — one line that erases every other control on this page. cluster-admin sprawl — how a leaked laptop token becomes a full cluster takeover — is why you reserve it for a tiny break-glass group, audit who holds it, and give everyone else scoped roles.

Scoping platform vs tenant permissions

On an internal platform, RBAC draws the line between the platform team that builds the playground and the tenants who play in it. The platform team needs cluster-scoped power for controllers, CRDs, and mesh config, but even they should work through GitOps, not personal cluster-admin. Tenants get a namespace (or virtual cluster) and a curated Role: they manage their own Deployments, Services, and ConfigMaps, but cannot edit NetworkPolicies, escalate RBAC, or touch other namespaces — neither can bulldoze the other’s lane, the RBAC half of the multi-tenancy story.

The admission-control path — where policy plugs in

☺ Like you’re 10: Before anything is written into the cluster’s notebook, it passes through a hallway of checkpoints — any one can change it or turn it away.

To place policy engines, know where they sit. Every write is an API request that travels a fixed gauntlet inside the kube-apiserver before it’s saved — picture this path and half of Domain 5 clicks into place:

🦆 kubectl apply pod AuthN who are you? AuthZ · RBAC allowed? Mutating admission Validating admission etcd persisted inject sidecar · add defaults OPA/Gatekeeper · Kyverno · PSA ✗ rejected → HTTP 403 Mutating webhooks change the object first; validating webhooks then accept or reject — only then is it written.

Mutating vs validating webhooks

☺ Like you’re 10: One checkpoint can fix your outfit (tuck in your shirt); the next only says yes or no — it can’t touch you.

Two kinds of admission webhook hang off that path, order exam-critical. Mutating webhooks run first and can change the object — inject a mesh sidecar, add default labels, set a securityContext. Validating webhooks run last and only accept or reject; they never modify. So a policy validates the final object — after defaults were injected — not the user’s original. Engines register as one or both.

Pod Security Admission — the built-in baseline

☺ Like you’re 10: Kubernetes has a built-in bouncer with three dress codes — pick one per room with a label on the door.

Before any full policy engine, know the built-in floor. Pod Security Admission (PSA) is a validating controller enforcing the Pod Security Standards at three levels: privileged (no restrictions), baseline (blocks obvious escapes: host namespaces, privileged containers), and restricted (hardened: non-root, no privilege escalation, seccomp). You opt a namespace in with labels, and each label pairs one of three modesenforce (reject the pod), audit (let it through but record it in the audit log), warn (let it through and warn the applier) — with the level to apply:

apiVersion: v1
kind: Namespace
metadata:
  name: payments
  labels:
    pod-security.kubernetes.io/enforce: restricted   # reject non-conforming pods
    pod-security.kubernetes.io/warn: restricted       # and warn the applier
    pod-security.kubernetes.io/audit: restricted      # and log it for review

PSA is coarse — three fixed levels, no custom rules — so teams graduate to Gatekeeper or Kyverno for bespoke policy. But it’s free, always-on, and a sane floor for every tenant namespace.

Policy engines: policy as code

☺ Like you’re 10: Instead of a human remembering the rules, you write them as code so the computer checks every request the same way, forever.

Policy-as-code means the rules governing your cluster — “no privileged pods,” “images only from our registry” — live in version control and are enforced automatically at admission, not in a wiki nobody reads. Two CNCF projects dominate, and the exam names both.

DimensionOPA / GatekeeperKyverno
Policy languageRego — a purpose-built query languageKubernetes-native YAML (and CEL expressions)
Learning curveSteeper — you learn RegoGentle — it looks like the manifests you already write
ShapeConstraintTemplate (the logic) + Constraint (the parameters)One ClusterPolicy / Policy resource
Can dovalidate; mutate (via Assign / AssignMetadata mutators)validate · mutate · generate · verifyImages
ReachGeneral engine — the same Rego works outside Kubernetes tooKubernetes-only, batteries included

Kyverno — policies that look like manifests

Kyverno runs as an admission webhook and expresses policy as ordinary Kubernetes resources in the YAML you already write — anyone who can read a Deployment can read a policy. Beyond validate, it can mutate (add defaults), generate (a default NetworkPolicy per new namespace), and verifyImages (check signatures). Here it requires every pod to run non-root:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-non-root
spec:
  validationFailureAction: Enforce   # "Audit" to only report; "Enforce" to block
  background: true                    # also scan already-running pods
  rules:
    - name: run-as-non-root
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Running as root is not allowed — set runAsNonRoot: true."
        pattern:
          spec:
            securityContext:          # pod level — a hardened version also checks
              runAsNonRoot: true      # each container, which can override the pod

OPA / Gatekeeper — Rego and constraints

☺ Like you’re 10: You write the logic once as a reusable stencil, then stamp out specific rules by filling in the blanks.

Open Policy Agent (OPA) is a general policy engine; Gatekeeper is its Kubernetes admission integration. Policy splits in two: a ConstraintTemplate carries the reusable Rego logic and defines a new CRD; a Constraint instantiates it with parameters and match rules. This example requires a label on namespaces:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names: { kind: K8sRequiredLabels }
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels: { type: array, items: { type: string } }
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        violation[{"msg": msg}] {
          provided := {label | input.review.object.metadata.labels[label]}
          required := {label | label := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("missing required labels: %v", [missing])
        }
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: ns-must-have-owner
spec:
  enforcementAction: deny          # "dryrun" = audit only; "deny" = block; "warn"
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels: ["owner"]

Audit vs enforce — roll out safely

☺ Like you’re 10: First the guard just writes down who’s breaking the rule; only once the backlog’s fixed do they start turning people away.

Both engines share a two-speed rollout, and using it well marks a senior engineer. In audit mode (validationFailureAction: Audit in Kyverno, enforcementAction: dryrun in Gatekeeper), violations are recorded but nothing is blocked — so you switch a policy on across a live cluster, see who breaks, and fix them without an outage. Then flip to enforce and it starts rejecting — guardrails added to a moving platform, no big-bang breakage.

⚠ Watch out

The opposite failure mode: leaving a policy in audit-only forever. One that only reports non-root violations month after month gives false safety — the dashboard is red, nobody owns fixing it, and on the day of the incident it blocked nothing. Audit is a runway, not a destination: set a date to flip each policy to enforce, and track “policies still in audit” as debt.

Supply-chain security — trust what you ship

☺ Like you’re 10: Before you eat the sandwich, you want the list of ingredients, an allergy check, and a tamper-proof seal proving the kitchen actually made it.

Plenty of compromises never touch your front door — they arrive baked into a container image: a vulnerable dependency, or a tampered artifact from a poisoned build. Supply-chain security is trusting what you run: four questions, each with a tool, answered early in the pipeline (“shift left”) and re-checked at admission.

① Build image in CI ② SBOM Syft ③ Scan Trivy · Grype ④ Sign cosign Registry image + sig ⑤ Admission verify gate Kyverno CI pipeline — shift left cluster ✓ trusted pod runs cosign verify · Rekor ✗ unsigned / unscanned image → admission blocks it SBOM answers “what’s inside?”, scan “is it safe?”, signing “did we really build it?”, provenance “how?”.

SBOM, scanning, signing, provenance

QuestionArtifactTool
What’s inside this image?SBOM — a Software Bill of Materials (every package & version)Syft (SPDX / CycloneDX)
Does it contain known vulnerabilities?A CVE scan reportTrivy, Grype
Did we really build this, untampered?A cryptographic signaturecosign (Sigstore)
How was it built, and from what?A provenance attestationSLSA framework

Sigstore/cosign is exam-worthy: cosign can sign with a key, but its clever mode is keyless — it gets a short-lived certificate from Fulcio tied to your CI’s OIDC identity (e.g. GitHub Actions), signs, and records it in Rekor, a public append-only transparency log where any later tampering is detectable — no long-lived key to leak. SLSA (Supply-chain Levels for Software Artifacts) then grades build integrity around a verifiable provenance attestation of which source and builder produced the artifact.

Verify at admission — closing the loop

☺ Like you’re 10: Signing the sandwich is pointless if nobody checks the seal — so the cluster checks before letting anything in.

Signing only helps if something verifies it before the image runs — otherwise you’ve locked the door and left the key in it. Here policy engines and the supply chain meet: a Kyverno verifyImages rule (or the Sigstore policy-controller) blocks any pod whose image isn’t signed by your identity. This one demands a keyless cosign signature from your GitHub org:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-signed-by-ci
      match:
        any:
          - resources:
              kinds: ["Pod"]
      verifyImages:
        - imageReferences: ["registry.acme.io/*"]
          attestors:
            - entries:
                - keyless:                                  # Sigstore keyless
                    subject: "https://github.com/acme/*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev       # transparency log
🦆 Dot’s-eye view

“Here’s the part I love: I never see most of this. My CI template already runs Syft, Trivy, and cosign — I just git push. Pull in a critical-CVE dependency and the pipeline tells me in the PR, not a security officer three weeks later. Because the image is signed, the cluster trusts my deploy automatically. The guardrails don’t slow me down — they’re why I’m allowed to self-serve straight to prod.” The CI/CD pipeline shifts these scans left; admission is the final backstop.

Audit trails, compliance, and runtime

Prevention isn’t the whole story: you also have to prove what happened — for auditors and forensics — and catch the attack that slips past every gate. Three capabilities finish the domain.

Kubernetes audit logs

Audit logs are the API server’s chronological record of every request: who did what, to which resource, when, and whether it was allowed. Tune verbosity per rule with an audit policy — level None, Metadata, Request or RequestResponse — and ship the log off the cluster to your observability stack, which is what stops an attacker simply deleting their own tracks. When Foxy asks “who deleted the payments namespace at 03:14?”, the log answers — so enable it before the incident, not after.

Policy reports & compliance evidence

Both Kyverno and Gatekeeper continuously emit machine-readable results — Kyverno via the standard PolicyReport / ClusterPolicyReport CRDs, Gatekeeper via the audit violations recorded in each Constraint’s status, a live report card — so “are we compliant right now?” is a query, not a quarterly fire drill. Add CIS-benchmark checks (kube-bench) and compliance reports fall out as a by-product. Your SBOMs double as evidence: when a Log4Shell-style CVE drops, stored SBOMs answer “which of our 400 services ship this library?” in seconds, not weeks.

Runtime security with Falco

☺ Like you’re 10: Even with locked doors and ID checks, you still want a lifeguard watching the pool for someone who’s already drowning.

Everything so far runs at admission — before a pod starts. But a container that passed every check can still be exploited at runtime. Falco is the CNCF runtime-security watchdog: it watches the kernel’s syscalls (through an eBPF probe, or a kernel module on older setups) and alerts on behaviour a static check can’t see — a shell spawning in a container, an unexpected outbound connection, a process reading /etc/shadow. Admission asks “should this start?”; Falco asks “is something bad happening now?” — you need both. Here’s the whole domain as layers:

LayerGuards againstExample control
Identity / mTLSEavesdropping, service impersonationIstio / Linkerd mTLS, SPIFFE identity, cert-manager
NetworkLateral movement, blast radiusNetworkPolicy default-deny
RBACOver-broad human / workload permissionsLeast-privilege Roles; no cluster-admin
Admission / policyUnsafe or non-compliant manifestsKyverno, Gatekeeper, Pod Security Admission
Supply chainTampered or vulnerable imagesSBOM, Trivy scan, cosign verify, SLSA
RuntimeA breach already in progressFalco syscall detection
AuditNo record of who did whatKubernetes audit log, PolicyReports

Wiring it into the platform

The senior insight: security must be the default, built into the road so nobody has to remember to be safe. Two placements do the work. First, shift left — SBOM, scanning, and signing live in Benny’s CI/CD pipeline, so problems surface in the PR, cheap to fix. Second, admission is the final gate — policy engines re-check at the cluster boundary, so a skipped pipeline can’t sneak in an unsigned or root-running image. Belt and braces.

When Mira’s self-service templates scaffold a service, it ships already secure-by-default: its own ServiceAccount, a default-deny NetworkPolicy, a restricted Pod Security level, mesh mTLS, and a signing step. Dot gets it all by clicking “new service” — the golden path is the secure path. Being declarative, it flows through GitOps: policies, RBAC, and mesh config reconcile from Git — one audit trail, one-commit rollback. See the reference architecture for how the layers fit and best practices for the guardrails-as-defaults pattern.

🐢 Timmy’s workshop · 20 min

On a throwaway cluster (kind or minikube), install Kyverno and apply the require-non-root policy above with validationFailureAction: Audit. Deploy a root pod — it’s admitted, but a PolicyReport records the violation. Flip to Enforce, re-apply, and admission now rejects it with your message. Finally, label a namespace pod-security.kubernetes.io/enforce: restricted and try a privileged pod — two independent guardrails, one built-in, one policy-as-code, both saying no. That contrast is defense in depth.

🎬 At the Platform Guild
🦊

Foxy: Why not just trust our own services? They’re all inside the cluster — the wall keeps bad guys out, right?

🐦

Pip: Because the day a pod gets popped, “inside the wall” is where the attacker is. Zero trust: every service proves itself with mTLS on every call — I zip the certs around so nobody has to think about it.

👺

Gizmo: Ugh, so much ceremony. Just give everyone cluster-admin and turn the policies off — the access tickets are slowing me down. 🤑

🐢

Timmy: That one line undoes every control we built, Gizmo. Least privilege, scoped Roles — guardrails baked into the golden path, so nobody files a ticket or gets breached.

🦆

Dot: Honestly? I don’t want cluster-admin. I want to push code, have the platform flag anything unsafe in my PR — and then just let me ship.

Guardrails turn “move fast and break things” into “move fast and break nothing”: the platform can hand Dot the keys because the road itself keeps her safe. Next, Timmy catalogues how teams get this wrong in Anti-Patterns & Pitfalls.

🐢 Timmy’s checkpoint

1. What does mutual TLS prove that ordinary TLS doesn’t, and which mesh resource makes Istio require it? 2. In the API request path, do mutating or validating webhooks run first — and why does order matter? 3. Name one concrete difference between OPA/Gatekeeper and Kyverno. 4. The danger of leaving a policy in audit mode permanently? 5. You sign every image with cosign — what else must happen for that signature to protect the cluster? 6. Which control catches an attack that begins after a pod has started?

Check your answers
  1. Ordinary TLS proves the server’s identity and encrypts traffic; mutual TLS also makes the client present a certificate, so both ends verify each other. A PeerAuthentication with mtls.mode: STRICT makes Istio require it.
  2. Mutating webhooks run first, then validating — so policies validate the final object, after defaults or a sidecar were injected, not the version the user submitted.
  3. Gatekeeper uses Rego and splits policy into ConstraintTemplate + Constraint; Kyverno uses Kubernetes-native YAML/CEL in a single ClusterPolicy and can also mutate, generate, and verify image signatures.
  4. It gives false security: violations are reported, never blocked, so on incident day it stops nothing. Audit is a runway, not a destination — set a date to flip it to enforce.
  5. Something must verify the signature before the image runs — a Kyverno verifyImages rule (or the Sigstore policy-controller) at admission that rejects any image not signed by your trusted identity. Signing without verification protects nothing.
  6. Runtime securityFalco — which watches kernel syscalls and alerts on suspicious behaviour after the pod starts, unlike admission, which acts only before start.