Kubernetes Security & Hardening›10 · Pod Security & admission control

Lesson 10 of 15 · Workload & Supply Chain

Pod Security & admission control

Control what kind of workloads may run: Pod Security Standards enforced by built-in admission, a hardened securityContext, and custom rules with ValidatingAdmissionPolicy (CEL), Kyverno or Gatekeeper.

Advanced
Key wordsPod Security StandardsPod Security AdmissionsecurityContextValidatingAdmissionPolicyCELKyvernoGatekeepermutating

RBAC says who; admission says what

RBAC decides who may create a pod. Admission control decides what that pod may look like: can it run as root, mount the host filesystem, use the latest tag, skip resource limits? Admission runs after authentication and authorization, before the object is stored.

RBAC is the rule "only club members may bring a pet to school". Admission control is the vet at the gate who checks each pet anyway: no wild animals, must be on a lead, must have a name tag. Being allowed to bring a pet doesn't mean you can bring a tiger.

Pod Security Standards and Admission

Kubernetes defines three levels, enforced per namespace by the built-in Pod Security Admission:

Level Allows Use for
privileged Everything System namespaces that truly need host access (CNI, storage, monitoring agents)
baseline Blocks known privilege escalations (privileged, host namespaces, hostPath, extra capabilities…) Minimum for all workloads
restricted Baseline + must run as non-root, drop ALL capabilities, no privilege escalation, a seccomp profile Default target for applications

Each namespace can set three modes independently: enforce (reject), warn (tell the client), audit (record in the audit log).

$ kubectl label --dry-run=server --overwrite ns shop pod-security.kubernetes.io/enforce=restricted
Warning: existing pods in namespace "shop" violate the new PodSecurity enforce level "restricted:latest"
Warning: web-7f9c5d6b8d-4hx9t: allowPrivilegeEscalation != false, unrestricted capabilities, runAsNonRoot != true, seccompProfile
namespace/shop labeled (server dry run)

A restricted-compliant pod

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: shop
spec:
  replicas: 2
  selector:
    matchLabels: { app: api }
  template:
    metadata:
      labels: { app: api }
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: api
          image: registry.example.com/shop/api:1.4.2
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - { name: tmp, mountPath: /tmp }     # writable scratch space
      volumes:
        - { name: tmp, emptyDir: {} }

Many public images run as root by default and fail under restricted. Prefer images built to run as a non-root user (e.g. nginxinc/nginx-unprivileged instead of nginx).

Custom rules: three options

1. ValidatingAdmissionPolicy (built in, CEL). No extra software; runs inside the API server:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-team-label
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["deployments"]
  validations:
    - expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
      message: "Deployments must have a 'team' label"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-team-label
spec:
  policyName: require-team-label
  validationActions: ["Deny"]              # or ["Warn", "Audit"] while rolling out
  matchResources:
    namespaceSelector:
      matchLabels:
        policy: enforced

2. Kyverno. Policies as YAML patterns; can also mutate (add defaults), generate (create objects) and verify image signatures:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce         # newer Kyverno versions set this per rule
  rules:
    - name: require-pinned-tag
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Use a specific image tag or digest, not ':latest'."
        pattern:
          spec:
            containers:
              - image: "!*:latest"

3. OPA Gatekeeper. Policies in Rego via ConstraintTemplates; strong when you already use OPA elsewhere.

ValidatingAdmissionPolicy Kyverno Gatekeeper
Runs In the API server Webhook Webhook
Language CEL YAML patterns (+ CEL) Rego
Mutation / generation Mutating policies are newer and still maturing ✅ Mutation ✅
Image verification ❌ ✅ via add-ons

Webhooks are in the critical path

A policy webhook that's down can block all pod creation (with failurePolicy: Fail), including during an incident. Run webhook engines highly available, exclude kube-system and the engine's own namespace, and monitor webhook latency and errors.

Try it: enforce and fix

  1. Label a namespace warn=restricted, deploy nginx:1.27, and read the warnings.
  2. Switch to enforce=restricted: the Deployment's pods are rejected (see kubectl get rs and its events).
  3. Fix it: use nginxinc/nginx-unprivileged (listens on 8080) with the securityContext above, plus an emptyDir for any paths nginx writes to.
  4. Apply the require-team-label ValidatingAdmissionPolicy in Warn mode, create a Deployment without the label, then switch to Deny.
  5. (Optional) Install Kyverno and the disallow-latest-tag policy; test with image: busybox:latest.

Going deeper: policy as a platform product

  • Keep policies in Git, test them in CI (Kyverno CLI, gator for Gatekeeper, or kubectl dry-run with CEL), and roll them out warn → audit → enforce with a date communicated to teams.
  • Pair each policy with a fix guide and a compliant example in your golden path (see Kubernetes Administration, lesson 30).
  • Use mutation sparingly: silently changing workloads surprises teams. Prefer validation with clear messages, plus good defaults in templates.
  • Exceptions should be explicit objects (e.g. Kyverno PolicyExceptions) with owners and expiry dates.

Recap

  • Pod Security Admission: namespace labels with levels privileged / baseline / restricted and modes enforce / warn / audit.
  • A hardened pod: non-root, no privilege escalation, drop ALL, RuntimeDefault seccomp, read-only root filesystem.
  • Custom rules: ValidatingAdmissionPolicy (CEL, in-process), Kyverno or Gatekeeper (webhooks).
  • Roll out warn/audit first; run webhooks HA and keep system namespaces safe.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.