ЁЯПл The SchoolтА║ЁЯЫбя╕П SecurityтА║ЁЯЫВ рдзрдбрд╛ 12 тАФ Admission рдЖрдгрд┐ pod security: pod рдЪрд╛рд▓рдгреНрдпрд╛рдЖрдзреА рджрд░рд╡рд╛рдЬрд╛рд╡рд░рдЪреА рддрдкрд╛рд╕рдгреА
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯЫВ рдзрдбрд╛ 12 тАФ Admission рдЖрдгрд┐ pod security: pod рдЪрд╛рд▓рдгреНрдпрд╛рдЖрдзреА рджрд░рд╡рд╛рдЬрд╛рд╡рд░рдЪреА рддрдкрд╛рд╕рдгреА

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 16 рдкреИрдХреА рдзрдбрд╛ 12 ┬╖ рдорд╛рдЧреАрд▓: lesson-11-k8s-secrets-network ┬╖ рдкреБрдвреАрд▓: lesson-13-dependencies


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ11, рдЖрдгрд┐ Kubernetes рдЪреЗ рд╢реЗрд╡рдЯрдЪреЗ рджрд╛рд░: admission. "рд╣рд╛ user pods рддрдпрд╛рд░ рдХрд░реВ рд╢рдХрддреЛ" рдЕрд╕реЗ RBAC рдореНрд╣рдЯрд▓реНрдпрд╛рдирдВрддрд░, admission controllers pod рдЪреНрдпрд╛ рдЖрдд рдкрд╛рд╣рддрд╛рдд рдЖрдгрд┐ рддреЛ рдЪрд╛рд▓реВ рд╢рдХрддреЛ рдХрд╛ рддреЗ рдард░рд╡рддрд╛рдд. рддреБрдореНрд╣реА рддреАрди Pod Security Standards (privileged, baseline, restricted), namespace labels рд╕рд╣ Pod Security Admission, рдЖрдгрд┐ standards рдордзреНрдпреЗ рди рдпреЗрдгрд╛рд▒реНрдпрд╛ рдирд┐рдпрдорд╛рдВрд╕рд╛рдареА policy engines (Kyverno, OPA Gatekeeper) рд╢рд┐рдХрддрд╛ тАФ рдЬрд╕реЗ pinned images рдЖрдгрд┐ resource limits.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдХрддрд░рд┐рдирд╛рдХрдбреЗ рдПрдХ keycard (RBAC) рдЖрд╣реЗ рдЬреНрдпрд╛рдореБрд│реЗ рддреА рдкрд░рд┐рд╕рд░рд╛рдд рдПрдХрд╛ рдкрд╛рд╣реБрдгреНрдпрд╛рд▓рд╛ рдЖрдгреВ рд╢рдХрддреЗ. рдкрдг рджрд░рд╡рд╛рдЬрд╛рд╡рд░ рджреАрдкрд┐рдХрд╛рдЪреА team рддрд░реАрд╣реА рдкрд╛рд╣реБрдгреНрдпрд╛рд▓рд╛ рддрдкрд╛рд╕рддреЗ:

рдЖрдЬрдЪрд╛ рдкрд╣рд┐рд▓рд╛ рдкрд╛рд╣реБрдгрд╛ рд╕рд╣рд╛рд╣реА рдирд┐рдпрдо рдореЛрдбрддреЛ. рджрд░рд╡рд╛рдЬрд╛ рдирд╛рд╣реА рдореНрд╣рдгрддреЛ, рдЖрдгрд┐ рдпрд╛рджреА рджреЗрддреЛ. рдпрд╛рджреАрддрд▓реЗ рджреБрд░реБрд╕реНрдд рдХрд░рд╛, рдкрд░рдд рдпрд╛, рдЖрдгрд┐ рджрд░рд╡рд╛рдЬрд╛ рдЙрдШрдбрддреЛ.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    k["ЁЯСй kubectl apply / CI"] --> a["ЁЯФР authentication"]
    a --> r["тШ╕я╕П RBAC<br/>may create pods? (L10)"]
    r --> m["тЬПя╕П mutating admission"]
    m --> v{"ЁЯЫВ validating admission<br/>Pod Security Admission: restricted<br/>Kyverno / Gatekeeper / VAP"}
    v -->|"тЭМ privileged ┬╖ root ┬╖ escalation<br/>hostPath ┬╖ :latest ┬╖ no limits"| no["403: rejected, with the reasons"]
    v -->|"тЬЕ all rules pass"| etcd["ЁЯУТ stored тЖТ scheduled тЖТ runs"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l12

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг RBAC "рдХрддрд░рд┐рдирд╛ pods рддрдпрд╛рд░ рдХрд░реВ рд╢рдХрддреЗ рдХрд╛?" рдпрд╛рдЪреЗ рдЙрддреНрддрд░ рджреЗрддреЗ, "рддрд┐рдЪреНрдпрд╛ pod рдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ?" рдпрд╛рдЪреЗ рдирд╛рд╣реА. рдПрдЦрд╛рджреНрдпрд╛ namespace рдордзреНрдпреЗ pods рддрдпрд╛рд░ рдХрд░реВ рд╢рдХрдгрд╛рд░рд╛ рдХреЛрдгреАрд╣реА тАФ developer, CI pipeline, рдЪреЛрд░рд▓реЗрд▓рд╛ token тАФ рдирд╛рд╣реАрддрд░ node рдЪреА disk mount рдХреЗрд▓реЗрд▓рд╛ privileged pod рдЪрд╛рд▓рд╡реВрди node рддрд╛рдмреНрдпрд╛рдд рдШреЗрдК рд╢рдХрддреЛ, рдЖрдгрд┐ рддрд┐рдереВрди рддреНрдпрд╛рд╡рд░рдЪрд╛ рдкреНрд░рддреНрдпреЗрдХ pod. Admission рд╣реА рдЕрд╢реА рдПрдХрдореЗрд╡ рдЬрд╛рдЧрд╛ рдЖрд╣реЗ рдЬрд┐рдереВрди рдкреНрд░рддреНрдпреЗрдХ pod рдЬрд╛рддреЛ, рддреЛ рдХреЛрдгреАрд╣реА рдЖрдгрд┐ рдХрд╕рд╛рд╣реА рддрдпрд╛рд░ рдХреЗрд▓реЗрд▓рд╛ рдЕрд╕реЛ. рддрд┐рдерд▓рд╛ рдирд┐рдпрдо cluster рдЪреЗ рдЪреБрдХрд╛рдВрдкрд╛рд╕реВрди рдЖрдгрд┐ attackers рдкрд╛рд╕реВрди рд╕рд╛рд░рдЦреНрдпрд╛рдЪ рдкреНрд░рдХрд╛рд░реЗ рд╕рдВрд░рдХреНрд╖рдг рдХрд░рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

sec/cloud.py рдордзрд▓реЗ admit(pod) pod["container"] рдЖрдгрд┐ pod.get("hostPath") рдкрд╛рд╣рддреЗ рдЖрдгрд┐ рдирд╛рдХрд╛рд░рдгреНрдпрд╛рдЪреА рдХрд╛рд░рдгреЗ рдЧреЛрд│рд╛ рдХрд░рддреЗ:

рддреЗ (True, []) рдХрд┐рдВрд╡рд╛ (False, [reasons]) рдкрд░рдд рдХрд░рддреЗ. рдкрд╣рд┐рд▓реНрдпрд╛ рдЪрд╛рд░ Pod Security Standards рдЪреНрдпрд╛ рдХрд▓реНрдкрдирд╛ рдЖрд╣реЗрдд; рд╢реЗрд╡рдЯрдЪреЗ рджреЛрди policy engine рдирд┐рдпрдо рдЖрд╣реЗрдд тАФ model рддреНрдпрд╛рдВрдирд╛ рдПрдХрддреНрд░ рдареЗрд╡рддреЗ, рдПрдХрдЪ рджрд╛рд░ рдореНрд╣рдгреВрди. sec/demo.py рдордзрд▓реЗ admission() рдПрдХ рд╡рд╛рдИрдЯ pod рдЖрдгрд┐ рдПрдХ рджреБрд░реБрд╕реНрдд pod рд╡рд╛рдкрд░реВрди рдкрд╛рд╣рддреЗ. Snippet рд╡рд╛рдИрдЯ pod рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ рдирд┐рдпрдо рдЕрд╕рд╛ рджреБрд░реБрд╕реНрдд рдХрд░рддреЛ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 sec/demo.py admission
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from cloud import admit
c = dict(image="school/results", privileged=True); pod = dict(container=c, hostPath="/var/run/docker.sock")
steps = (("first try", lambda: None),
         ("privileged off", lambda: c.update(privileged=False)),
         ("run as non-root", lambda: c.update(runAsNonRoot=True)),
         ("no escalation", lambda: c.update(allowPrivilegeEscalation=False)),
         ("no hostPath", lambda: pod.pop("hostPath")),
         ("tag :latest", lambda: c.update(image="school/results:latest")),
         ("pinned digest", lambda: c.update(image="school/results:1.4.2@sha256:9f1c")),
         ("limits", lambda: c.update(limits={"cpu": "500m", "memory": "256Mi"})))
for name, fix in steps:
    fix(); ok, why = admit(pod)
    print(f"{name:<16} тЖТ {'admitted' if ok else f'{len(why)} left: ' + '; '.join(why)}")
EOF
python3 sec/test_sec.py

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

admission рд╣реЗ рдЫрд╛рдкрддреЗ:

тХРтХРтХР admission тХРтХРтХР
тФАтФА first try тЖТ rejected: privileged container; may run as root (set runAsNonRoot: true); allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
тФАтФА fixed     тЖТ admitted
   enforce with Pod Security Admission (restricted) or a policy engine (Kyverno / OPA Gatekeeper)

тЬЕ done тАФ every door checked

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

first try        тЖТ 6 left: privileged container; may run as root (set runAsNonRoot: true); allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
privileged off   тЖТ 5 left: may run as root (set runAsNonRoot: true); allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
run as non-root  тЖТ 4 left: allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
no escalation    тЖТ 3 left: mounts a hostPath volume; image has no pinned tag/digest; no resource limits
no hostPath      тЖТ 2 left: image has no pinned tag/digest; no resource limits
tag :latest      тЖТ 2 left: image has no pinned tag/digest; no resource limits
pinned digest    тЖТ 1 left: no resource limits
limits           тЖТ admitted

Tests рдордзреНрдпреЗ тЬЕ L12 admission rejects a privileged, unpinned pod рдЖрд╣реЗ рдЖрдгрд┐ рд╢реЗрд╡рдЯ 16/16 passed рдиреЗ рд╣реЛрддреЛ.

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рдкреНрд░рддреНрдпреЗрдХ рджреБрд░реБрд╕реНрддреА рдиреЗрдордХреЗ рдПрдХ рдХрд╛рд░рдг рдХрд╛рдвреВрди рдЯрд╛рдХрддреЗ, рдЖрдгрд┐ рд╕рд╣рд╛рд╣реА рдЧреЗрд▓реНрдпрд╛рд╡рд░рдЪ рджрд╛рд░ рдЙрдШрдбрддреЗ. :latest tag рдЬреЛрдбрд▓реНрдпрд╛рдиреЗ рдорджрдд рдЭрд╛рд▓реА рдирд╛рд╣реА тАФ рддреЛ рд╣рд▓рдгрд╛рд░рд╛ tag рдЖрд╣реЗ, рдореНрд╣рдгреВрди рд╕рдВрдЦреНрдпрд╛ 2 рд╡рд░рдЪ рд░рд╛рд╣рд┐рд▓реА. рдлрдХреНрдд digest рд╕рд╣ рдЦрд▒реНрдпрд╛ version рдиреЗ рддреЗ рджреБрд░реБрд╕реНрдд рдЭрд╛рд▓реЗ. Default рдиреЗ рдХрд╛рдп рдирд╡реНрд╣рддреЗ рд╣реЗ рд╕реБрджреНрдзрд╛ рд▓рдХреНрд╖рд╛рдд рдШреНрдпрд╛: escalation рд╕рд╛рдареА рдзреЛрдХрд╛рджрд╛рдпрдХ рдард░рд╛рдпрд▓рд╛ pod рдиреЗ рдХрдзреАрдЪ "privileged: yes" рдореНрд╣рдЯрд▓реЗ рдирд╡реНрд╣рддреЗ тАФ allowPrivilegeEscalation рддреБрдореНрд╣реА false рд▓рд┐рд╣реАрдкрд░реНрдпрдВрдд true рдореНрд╣рдгреВрди рдЧрдгрд▓реЗ рдЬрд╛рддреЗ. рд╕реБрд░рдХреНрд╖рд┐рдд settings рд▓рд┐рд╣рд╛рд╡реНрдпрд╛ рд▓рд╛рдЧрддрд╛рдд.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ account рд╡рд░ тАФ school namespace рд╡рд░ Pod Security Admission. рдЖрдзреА рдХрд╛рдп рддреБрдЯреЗрд▓ рддреЗ рдкрд╛рд╣рд╛, рдордЧ enforce рдХрд░рд╛:

kubectl label --dry-run=server --overwrite ns school pod-security.kubernetes.io/enforce=restricted
apiVersion: v1
kind: Namespace
metadata:
  name: school
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted

restricted (рдЖрдгрд┐ рдЦрд╛рд▓рдЪреЗ policy engine рдирд┐рдпрдо) рдкрд╛рд╕ рдХрд░рдгрд╛рд░рд╛ pod spec:

apiVersion: apps/v1
kind: Deployment
metadata: { name: results-api, namespace: school }
spec:
  replicas: 3
  selector: { matchLabels: { app: results-api } }
  template:
    metadata: { labels: { app: results-api } }
    spec:
      serviceAccountName: results-api
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: api
          image: ghcr.io/baluraut/school-results:1.4.2@sha256:<the image digest from CI>
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits:   { cpu: 500m, memory: 256Mi }

PSA рддрдкрд╛рд╕рдд рдирд╛рд╣реА рддреНрдпрд╛рд╕рд╛рдареА Kyverno тАФ :latest рдирд╛рд╣реА, рдЖрдгрд┐ CPU рд╡ memory limits рдмрдВрдзрдирдХрд╛рд░рдХ (Kyverno 1.13+ рдкреНрд░рддреНрдпреЗрдХ рдирд┐рдпрдорд╛рд╡рд░ failureAction рдареЗрд╡рддреЛ; рдЬреБрдиреНрдпрд╛ releases рдордзреНрдпреЗ spec.validationFailureAction рд╡рд╛рдкрд░рд▓реЗ рдЬрд╛рддреЗ):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: school-pod-rules }
spec:
  background: true
  rules:
    - name: no-latest-tag
      match: { any: [{ resources: { kinds: [Pod] } }] }
      validate:
        failureAction: Enforce
        message: "Use a pinned version tag or a digest, not :latest."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
    - name: require-limits
      match: { any: [{ resources: { kinds: [Pod] } }] }
      validate:
        failureAction: Enforce
        message: "CPU and memory limits are required."
        pattern:
          spec:
            containers:
              - resources:
                  limits:
                    cpu: "?*"
                    memory: "?*"

рддреЛрдЪ limits рдирд┐рдпрдо, Kubernetes рдордзреНрдпреЗрдЪ рдЕрд╕рд▓реЗрд▓реНрдпрд╛ ValidatingAdmissionPolicy (CEL) рдЖрдгрд┐ рддреНрдпрд╛рдЪреНрдпрд╛ binding рд╕рд╣:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: { name: require-limits }
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "object.spec.containers.all(c, has(c.resources) && has(c.resources.limits) && 'memory' in c.resources.limits && 'cpu' in c.resources.limits)"
      message: "every container needs CPU and memory limits"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: { name: require-limits-school }
spec:
  policyName: require-limits
  validationActions: [Deny]
  matchResources:
    namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: school } }

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рдпрд╛рдЪ policies CI рдордзреНрдпреЗ рд╕реБрджреНрдзрд╛ рдЪрд╛рд▓рд╡рд╛ (рдЙрджрд╛рд╣рд░рдгрд╛рд░реНрде rendered manifests рд╡рд┐рд░реБрджреНрдз kyverno apply), рдореНрд╣рдгрдЬреЗ developer рд▓рд╛ "rejected: no limits" pull request рдордзреНрдпреЗрдЪ рджрд┐рд╕реЗрд▓, rollout рджрд░рдореНрдпрд╛рди рд░рд╛рддреНрд░реА 2 рд╡рд╛рдЬрддрд╛ рдирд╛рд╣реА.

тПня╕П рдкреБрдвреЗ

Cluster рдЪреА рджрд╛рд░реЗ рдмрдВрдж рдЖрд╣реЗрдд: рдХреЛрдг рдХреГрддреА рдХрд░реВ рд╢рдХрддреЛ (RBAC), рдХреЛрдг connect рдХрд░реВ рд╢рдХрддреЛ (NetworkPolicy), рдХрд╛рдп рдЪрд╛рд▓реВ рд╢рдХрддреЗ (admission). рдкрдг pod рдЪреА image рдЗрддрд░рд╛рдВрдЪреНрдпрд╛ code рдиреЗ рднрд░рд▓реЗрд▓реА рдЖрд╣реЗ. рдкреБрдвреЗ: рд╡рд┐рддрд░рдг рдЦрд┐рдбрдХреА рдЙрдШрдбрддреЗ, dependencies рд╕рд╣.

git checkout lesson-13-dependencies

ЁЯЫВ Lesson 12 тАФ Admission & pod security: the check at the gate before a pod runs

ЁЯУН You are here: Lesson 12 of 16 ┬╖ Previous: lesson-11-k8s-secrets-network ┬╖ Next: lesson-13-dependencies


ЁЯУж What's in this branch

Lessons 01тАУ11, plus the last Kubernetes gate: admission. After RBAC says "this user may create pods", admission controllers look inside the pod and decide whether it may run. You learn the three Pod Security Standards (privileged, baseline, restricted), Pod Security Admission with namespace labels, and policy engines (Kyverno, OPA Gatekeeper) for rules the standards do not cover тАФ like pinned images and resource limits.

ЁЯзТ Explain like I'm 5

Katrina has a keycard (RBAC) that lets her bring a visitor onto the campus. But at the gate, Dipika's team still looks at the visitor:

The first visitor today breaks all six rules. The gate says no, and gives the list. Fix the list, come back, and the gate opens.

ЁЯЧ║я╕П Diagram

flowchart LR
    k["ЁЯСй kubectl apply / CI"] --> a["ЁЯФР authentication"]
    a --> r["тШ╕я╕П RBAC<br/>may create pods? (L10)"]
    r --> m["тЬПя╕П mutating admission"]
    m --> v{"ЁЯЫВ validating admission<br/>Pod Security Admission: restricted<br/>Kyverno / Gatekeeper / VAP"}
    v -->|"тЭМ privileged ┬╖ root ┬╖ escalation<br/>hostPath ┬╖ :latest ┬╖ no limits"| no["403: rejected, with the reasons"]
    v -->|"тЬЕ all rules pass"| etcd["ЁЯУТ stored тЖТ scheduled тЖТ runs"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/security/lesson-diagrams.html#l12

тЭУ What

ЁЯдФ Why

Because RBAC answers "may Katrina create pods?", not "what is inside her pod?". Someone who may create pods in a namespace тАФ a developer, a CI pipeline, a stolen token тАФ could otherwise run a privileged pod with the node's disk mounted and own the node, and from there every pod on it. Admission is the one place where every pod passes, whoever created it and however. A rule there protects the cluster from mistakes and from attackers in the same way.

ЁЯФз How (in this repo)

admit(pod) in sec/cloud.py looks at pod["container"] and pod.get("hostPath") and collects reasons to reject:

It returns (True, []) or (False, [reasons]). The first four are Pod Security Standards ideas; the last two are policy engine rules тАФ the model puts them together, as one gate. admission() in sec/demo.py tries a bad pod and a fixed pod. The snippet fixes the bad pod one rule at a time.

ЁЯзк Try it

python3 sec/demo.py admission
python3 - <<'EOF'
import sys; sys.path.insert(0, "sec"); from cloud import admit
c = dict(image="school/results", privileged=True); pod = dict(container=c, hostPath="/var/run/docker.sock")
steps = (("first try", lambda: None),
         ("privileged off", lambda: c.update(privileged=False)),
         ("run as non-root", lambda: c.update(runAsNonRoot=True)),
         ("no escalation", lambda: c.update(allowPrivilegeEscalation=False)),
         ("no hostPath", lambda: pod.pop("hostPath")),
         ("tag :latest", lambda: c.update(image="school/results:latest")),
         ("pinned digest", lambda: c.update(image="school/results:1.4.2@sha256:9f1c")),
         ("limits", lambda: c.update(limits={"cpu": "500m", "memory": "256Mi"})))
for name, fix in steps:
    fix(); ok, why = admit(pod)
    print(f"{name:<16} тЖТ {'admitted' if ok else f'{len(why)} left: ' + '; '.join(why)}")
EOF
python3 sec/test_sec.py

тЬЕ Verify тАФ what you should see

admission prints:

тХРтХРтХР admission тХРтХРтХР
тФАтФА first try тЖТ rejected: privileged container; may run as root (set runAsNonRoot: true); allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
тФАтФА fixed     тЖТ admitted
   enforce with Pod Security Admission (restricted) or a policy engine (Kyverno / OPA Gatekeeper)

тЬЕ done тАФ every door checked

Your snippet prints:

first try        тЖТ 6 left: privileged container; may run as root (set runAsNonRoot: true); allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
privileged off   тЖТ 5 left: may run as root (set runAsNonRoot: true); allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
run as non-root  тЖТ 4 left: allowPrivilegeEscalation not false; mounts a hostPath volume; image has no pinned tag/digest; no resource limits
no escalation    тЖТ 3 left: mounts a hostPath volume; image has no pinned tag/digest; no resource limits
no hostPath      тЖТ 2 left: image has no pinned tag/digest; no resource limits
tag :latest      тЖТ 2 left: image has no pinned tag/digest; no resource limits
pinned digest    тЖТ 1 left: no resource limits
limits           тЖТ admitted

The tests include тЬЕ L12 admission rejects a privileged, unpinned pod and end with 16/16 passed.

ЁЯПБ What you just proved

Each fix removes exactly one reason, and the gate opens only when all six are gone. Adding the tag :latest did not help тАФ it is a tag that moves, so the count stayed at 2. Only a real version with a digest fixed it. Note also what was missing by default: the pod never said "privileged: yes" to be risky for escalation тАФ allowPrivilegeEscalation counts as true until you write false. Safe settings have to be written.

тЪая╕П Common mistakes

ЁЯПн In production

On a real account тАФ Pod Security Admission on the school namespace. First see what would break, then enforce:

kubectl label --dry-run=server --overwrite ns school pod-security.kubernetes.io/enforce=restricted
apiVersion: v1
kind: Namespace
metadata:
  name: school
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
    pod-security.kubernetes.io/warn: restricted
    pod-security.kubernetes.io/audit: restricted

A pod spec that passes restricted (and the policy engine rules below):

apiVersion: apps/v1
kind: Deployment
metadata: { name: results-api, namespace: school }
spec:
  replicas: 3
  selector: { matchLabels: { app: results-api } }
  template:
    metadata: { labels: { app: results-api } }
    spec:
      serviceAccountName: results-api
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        seccompProfile: { type: RuntimeDefault }
      containers:
        - name: api
          image: ghcr.io/baluraut/school-results:1.4.2@sha256:<the image digest from CI>
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities: { drop: ["ALL"] }
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits:   { cpu: 500m, memory: 256Mi }

Kyverno for what PSA does not check тАФ no :latest, and CPU and memory limits required (Kyverno 1.13+ puts failureAction on each rule; older releases use spec.validationFailureAction):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: school-pod-rules }
spec:
  background: true
  rules:
    - name: no-latest-tag
      match: { any: [{ resources: { kinds: [Pod] } }] }
      validate:
        failureAction: Enforce
        message: "Use a pinned version tag or a digest, not :latest."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
    - name: require-limits
      match: { any: [{ resources: { kinds: [Pod] } }] }
      validate:
        failureAction: Enforce
        message: "CPU and memory limits are required."
        pattern:
          spec:
            containers:
              - resources:
                  limits:
                    cpu: "?*"
                    memory: "?*"

The same limits rule, built in, with a ValidatingAdmissionPolicy (CEL) and its binding:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata: { name: require-limits }
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "object.spec.containers.all(c, has(c.resources) && has(c.resources.limits) && 'memory' in c.resources.limits && 'cpu' in c.resources.limits)"
      message: "every container needs CPU and memory limits"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata: { name: require-limits-school }
spec:
  policyName: require-limits
  validationActions: [Deny]
  matchResources:
    namespaceSelector: { matchLabels: { kubernetes.io/metadata.name: school } }

ЁЯПн Why this matters in production: run the same policies in CI too (for example kyverno apply against the rendered manifests), so a developer sees "rejected: no limits" in the pull request, not at 2 a.m. during a rollout.

тПня╕П Next

The cluster's gates are closed: who may act (RBAC), who may connect (NetworkPolicy), what may run (admission). But the pod's image is full of other people's code. Next: the delivery desk opens, with dependencies.

git checkout lesson-13-dependencies
тЖР Previousk8s secrets networkNext тЖТdependencies

This page is the lesson's README from the lesson-12-admission branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.