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:
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 mountedemptyDirvolumes.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:
🛑 PAUSE & RECALL — 2 minutes
- What are the three PSS levels, and which should be the minimum for application workloads?
- In the analogy, what does a SecurityContext represent? What does PSS represent?
- What happens if a pod violates the
enforcemode? What aboutwarn? - Why do we pair
readOnlyRootFilesystem: truewithemptyDirvolumes?
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:
🤔 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
- Why are volume-mounted Secrets more secure than environment variables? (Hint:
/proc) - What breaks when you apply default-deny but forget DNS egress?
- What does the top-level
podSelectorin 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
- 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.
- 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 inkube-system. Without it, pods cannot resolve service names, breaking service discovery completely.
- 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.
- 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.