← Articulet Kubernetes Zero to Hero Chapter 6.2
Module 6 Security, RBAC, and Policies

Pod Security and Network Policies

In the previous chapter, you built the identity layer — RBAC controls who can access your cluster. But identity is half the story. What stops a legitimate user from deploying a pod that runs as root and compromises a no...

Chapter 15 of 22

In the previous chapter, you built the identity layer — RBAC controls who can access your cluster. But identity is half the story. What stops a legitimate user from deploying a pod that runs as root and compromises a node? What stops any breached pod from talking freely to every other pod? This chapter closes those gaps. We shift from who gets in to what workloads can do once they're running — the difference between a doorman checking IDs and the structural safety of the building itself.

Analogy: Apartment Building Security

Picture a large apartment complex. RBAC (from the last chapter) is the keycard system at the front door — it decides who can enter. But what about the buildings themselves? Pod Security Standards are the building codes — minimum safety requirements every unit must meet: fire exits, safe wiring, no master keys lying around. Security Contexts are the locks on individual apartment doors — each tenant configures their own defenses. Network Policies are the elevator keycards between floors — preventing residents from wandering where they don't belong. Secret Management is the secure mailroom — documents handled through locked channels. Defense in depth means securing every layer.


6.2.1 Pod Security Standards: From PSP to PSS

The original PodSecurityPolicy (PSP) was powerful but complex. It was deprecated in Kubernetes 1.21 and removed in 1.25. The replacement — Pod Security Standards (PSS) enforced by the Pod Security Admission (PSA) controller — takes a simpler approach: three predefined policy levels applied through namespace labels.

Think of it as a city's building inspector office. Instead of every building needing a custom inspection contract (PSP), the city publishes three standard building codes. You post a sign on each neighborhood (namespace) declaring which code applies.

Level Building Code Analogy What It Allows Use Case
Privileged No code enforced Unrestricted: host namespaces, root, all capabilities System infrastructure only (CNI, storage drivers)
Baseline Standard safety code Blocks known privilege escalations: no privileged mode, no hostPath, no hostNetwork General applications; the minimum you should accept
Restricted Maximum security code Requires runAsNonRoot, read-only root FS, drops all capabilities, enforces seccomp Production workloads handling sensitive data

Visual Description:

graph TD subgraph "Pod Security Standards [Building Codes]" direction LR P[Privileged<br/>Unrestricted] --> B[Baseline<br/>Known Risks Blocked] B --> R[Restricted<br/>Defense in Depth] end subgraph "Enforcement Modes [Inspector Actions]" WARN[Warn<br/>Flag + Allow] AUDIT[Audit<br/>Log + Allow] ENFORCE[Enforce<br/>Reject] end R --> WARN R --> AUDIT R --> ENFORCE style P fill:#ef9a9a style B fill:#ffcc80 style R fill:#a5d6a7 style ENFORCE fill:#ef5350

Apply PSS through namespace labels:

apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

enforce blocks pod creation and returns an error. audit allows the pod but writes a log entry. warn allows the pod but prints a warning. The recommended migration path: start with warn, graduate to audit, then flip on enforce once workloads comply.

⚠️ Common Misconception: PSS levels are cumulative — Restricted includes all Baseline restrictions. If you need to relax one Restricted requirement, you move the whole namespace down to Baseline; you don't "subtract" from Restricted.


6.2.2 Security Contexts: Hardening Individual Pods

While PSS sets the building code for a neighborhood, Security Contexts are the individual locks each tenant installs on their apartment door. A SecurityContext can be defined at the pod level (applies to all containers) or container level (overrides pod-level settings). When both are set, container-level settings win — the building has a standard lock, but each apartment can upgrade theirs.

Here's a hardened pod satisfying the Restricted standard:

apiVersion: v1
kind: Pod
metadata:
  name: hardened-app
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: myapp:1.2.3
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
          - ALL
    volumeMounts:
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: tmp
    emptyDir: {}
  • runAsNonRoot: true — Rejects the pod if the container image would run as root (UID 0). "No master keys allowed."
  • readOnlyRootFilesystem: true — Root filesystem becomes immutable; writable data goes to explicitly mounted emptyDir volumes.
  • capabilities: drop: [ALL] — Removes all Linux capabilities, then add back only what's needed.
  • allowPrivilegeEscalation: false — Prevents processes from gaining higher privileges via setuid binaries.
  • seccompProfile: RuntimeDefault — Applies syscall filtering, blocking dangerous kernel interfaces.

Visual Description:

graph TD subgraph "Security Context Layers [Apartment Locks]" subgraph "Pod Level [Building Standard]" P1[runAsNonRoot: true] P2[seccompProfile] end subgraph "Container Level [Tenant Overrides]" C1[allowPrivilegeEscalation: false] C2[readOnlyRootFilesystem: true] C3[capabilities: drop ALL] end subgraph "Runtime Result" R1[Non-root user] R2[Immutable root FS] R3[No extra privileges] end P1 --> R1 C1 --> R3 C2 --> R2 C3 --> R3 end

🛑 PAUSE & RECALL — 2 minutes

  1. What are the three PSS levels, and which should be the minimum for application workloads?
  2. In the analogy, what does a SecurityContext represent? What does PSS represent?
  3. What happens if a pod violates the enforce mode? What about warn?
  4. Why do we pair readOnlyRootFilesystem: true with emptyDir volumes?

Rate your confidence (0-4).


6.2.3 Network Policies for Security

By default, every pod can communicate with every other pod on any port. NetworkPolicies change this through allow-list semantics: you explicitly define what's permitted, and everything else is denied. The essential pattern is default-deny: block everything, then punch precise holes.

Default-Deny All Ingress and Egress

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}      # Empty = ALL pods in namespace
  policyTypes:
  - Ingress
  - Egress

This blocks all traffic in both directions. But here's the critical insight: this will break DNS resolution because pods can no longer reach CoreDNS on UDP port 53. Applications fail in subtle ways — they can't resolve service names or reach external APIs.

Allow DNS Egress (Essential!)

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53

Allow Application-Specific Traffic

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend       # Protects BACKEND pods
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend  # Only frontend can reach backend
    ports:
    - protocol: TCP
      port: 8080

Visual Description:

graph TD subgraph "Tiered Network Policy [Floor Access Controls]" T1["Platform: Allow DNS + monitoring"] --> T2["Namespace: App-to-app rules"] T2 --> T3["Pod: Specific port/protocol"] end subgraph "Result" DNS[DNS OK] --> APP[App Works] BLOCK[Everything else BLOCKED] end T3 --> APP T3 --> BLOCK style T1 fill:#ef9a9a style T2 fill:#ffcc80 style T3 fill:#a5d6a7 style BLOCK fill:#f44336

🤔 TRY BEFORE YOU SEE

You applied default-deny to production. You have pods: frontend (label app=frontend), backend (label app=backend, port 8080), and db (label app=database, port 5432). Only frontend reaches backend; only backend reaches db.

Write the NetworkPolicy YAML that allows backend-to-database traffic on port 5432. Which podSelector protects the destination? What goes in from?


Reveal:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-to-db
spec:
  podSelector:
    matchLabels:
      app: database      # Protects destination DB pods
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend   # Only backend can reach database
    ports:
    - protocol: TCP
      port: 5432

Key insight: the top-level podSelector always selects the destination pods being protected. The from field selects the source pods allowed to connect. Many learners reverse these — imagine installing a lock on your neighbor's door instead of your own.


6.2.4 Secret Management Security

Kubernetes Secrets are only base64-encoded — not encrypted by default. Two practices close this gap:

Never inject Secrets as environment variables. Any process can read /proc/<pid>/environ to see your secrets. Instead, mount as files:

volumeMounts:
- name: db-password
  mountPath: /secrets
  readOnly: true
volumes:
- name: db-password
  secret:
    secretName: database-credentials

Volume-mounted Secrets use tmpfs (memory-backed), so they never touch disk. When the pod dies, the memory is reclaimed. For external secret management, the Secret Manager CSI driver mounts GCP Secret Manager secrets directly as volumes without ever storing them in etcd.


🛑 PAUSE & RECALL — 2 minutes

  1. Why are volume-mounted Secrets more secure than environment variables? (Hint: /proc)
  2. What breaks when you apply default-deny but forget DNS egress?
  3. What does the top-level podSelector in a NetworkPolicy select — source or destination pods?

Rate your confidence (0-4).


GKE in Practice: Shielded GKE and GKE Sandbox

Shielded GKE provides hardware-verified protections:

  • Secure Boot: Verifies each boot component is cryptographically signed by Google.
  • Integrity Monitoring: Continuously compares boot state against a known-good baseline.
  • Confidential Nodes: Encrypts memory at the hardware level using AMD SEV.

GKE Sandbox uses gVisor — a userspace kernel reimplementation — to intercept every system call between your container and the host kernel. Even if a container escapes, it only reaches the gVisor sandbox, not the host. Like building each apartment as a reinforced bunker — a breach of one unit cannot compromise the building.

Enable Shielded nodes:

gcloud container clusters create my-cluster \
  --enable-shielded-nodes \
  --shielded-secure-boot \
  --shielded-integrity-monitoring

For NetworkPolicies, GKE Dataplane V2 replaces Calico with Cilium-based eBPF, enforcing policies more efficiently and providing network flow logging:

gcloud container clusters create my-cluster \
  --enable-dataplane-v2 \
  --enable-network-policy

Binary Authorization adds deploy-time enforcement: clusters reject any container image that hasn't been signed by an approved authority or that fails vulnerability scanning.


Lab: LAB-6.2 — Pod and Network Security (75 min)

Step 1: Create Namespace with Restricted PSS (10 min)

kubectl create namespace secure-apps
kubectl label namespace secure-apps \
  pod-security.kubernetes.io/enforce=restricted \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/warn=restricted

Step 2: Attempt Insecure Pod — Observe Rejection (10 min)

apiVersion: v1
kind: Pod
metadata:
  name: insecure-pod
  namespace: secure-apps
spec:
  containers:
  - name: nginx
    image: nginx:latest
    securityContext:
      runAsRoot: true
kubectl apply -f insecure-pod.yaml
# Expected: Error "violates PodSecurity 'restricted:latest'..."

Step 3: Deploy Hardened Pod (15 min)

apiVersion: v1
kind: Pod
metadata:
  name: secure-web
  namespace: secure-apps
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: nginx
    image: nginx:alpine
    ports:
    - containerPort: 8080
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: [ALL]
    volumeMounts:
    - name: tmp
      mountPath: /tmp
    - name: cache
      mountPath: /var/cache/nginx
    - name: run
      mountPath: /var/run
  volumes:
  - name: tmp
    emptyDir: {}
  - name: cache
    emptyDir: {}
  - name: run
    emptyDir: {}
kubectl apply -f hardened-pod.yaml
kubectl exec -it secure-web -n secure-apps -- id
# Expected: uid=1000 gid=1000

Step 4: Apply Default-Deny NetworkPolicy (5 min)

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: secure-apps
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
EOF

Step 5: Observe DNS Breakage — The Teaching Moment (10 min)

kubectl run debug --rm -it --image=busybox -n secure-apps --restart=Never -- nslookup kubernetes.default
# Expected: times out (DNS egress blocked!)

This is the critical teaching moment. Default-deny blocks ALL egress, including DNS. Fix it:

cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: secure-apps
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
  - to:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: kube-system
    ports:
    - protocol: UDP
      port: 53
    - protocol: TCP
      port: 53
EOF

Re-test — DNS now works.

Step 6: Test Selective Allow Policy (15 min)

kubectl run frontend --image=nginx:alpine -n secure-apps --labels="app=frontend"
kubectl run backend --image=nginx:alpine -n secure-apps --labels="app=backend"

Apply policy allowing only frontend-to-backend on port 80, then test from frontend (works) and from an unlabeled pod (blocked).

Cleanup

kubectl delete namespace secure-apps

Chapter Summary

Pod Security Standards provide three enforcement levels — Privileged, Baseline, and Restricted — applied via namespace labels with enforce, audit, and warn modes. Security Contexts harden individual pods through runAsNonRoot, readOnlyRootFilesystem, capabilities: drop: [ALL], and seccomp profiles. Network Policies implement defense-in-depth for pod-to-pod traffic, but default-deny must always include DNS egress to avoid breaking service discovery. Secret management improves through volume mounts (not env vars), encryption at rest, and external secret integration. On GKE, Shielded nodes provide boot integrity, gVisor provides runtime sandboxing, and Dataplane V2 enables efficient NetworkPolicy enforcement with visibility.

📇 KEY CONCEPT CARDS

  1. Q: What are the three Pod Security Standard levels, and which admission controller enforces them?
    A: Privileged (unrestricted), Baseline (blocks known risks), and Restricted (defense-in-depth). Enforced by the Pod Security Admission (PSA) controller via namespace labels.
  1. Q: Why must you always allow DNS egress when implementing default-deny NetworkPolicy?
    A: Default-deny blocks ALL egress including UDP/TCP port 53 to CoreDNS in kube-system. Without it, pods cannot resolve service names, breaking service discovery completely.
  1. Q: What is the difference between pod-level and container-level SecurityContext?
    A: Pod-level applies to all containers (e.g., runAsNonRoot, seccompProfile). Container-level overrides for that specific container (e.g., readOnlyRootFilesystem, capabilities). Container settings take precedence.
  1. Q: Why are volume-mounted Secrets more secure than environment variables, and what extra protection does GKE provide?
    A: Environment variables are visible via /proc/<pid>/environ. Volume-mounted Secrets use tmpfs (memory-only, never touches disk). GKE encrypts etcd at rest by default and supports CMEK for customer-controlled encryption.