Skip to content

Kubernetes Security Cheat Sheet

Remember: The "minimum viable security" for a Kubernetes pod: runAsNonRoot: true, readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"]. These four settings prevent the most common container escape vectors. Mnemonic: "NRAD" — Non-root, Read-only, no privilege Ascension, Drop caps.

Gotcha: readOnlyRootFilesystem: true breaks applications that write to /tmp, /var/run, or other directories. Mount emptyDir volumes for those paths. Failing to do this is the #1 reason teams remove this security setting instead of fixing their app.

Pod Security Context

spec:
  securityContext:               # Pod level
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    securityContext:             # Container level
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop: ["ALL"]

RBAC

# Role (namespace-scoped)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: production
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list", "watch"]
---
# Bind role to ServiceAccount
kind: RoleBinding
metadata:
  name: read-pods
subjects:
- kind: ServiceAccount
  name: monitoring
  namespace: production
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
# Check permissions
kubectl auth can-i get pods -n prod --as=system:serviceaccount:prod:mysa
kubectl auth can-i --list --as=system:serviceaccount:prod:mysa

Image Security

# Scan for vulnerabilities
trivy image myapp:latest
trivy image --severity CRITICAL --exit-code 1 myapp:latest

# Sign image
cosign sign --key cosign.key registry.example.com/myapp:v1

# Verify signature
cosign verify --key cosign.pub registry.example.com/myapp:v1

Pod Security Standards

# Namespace labels
metadata:
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted
Level Allows
privileged Everything (unrestricted)
baseline Blocks known escalations (hostNetwork, privileged)
restricted Non-root, drop caps, seccomp, no hostPath

Network Security

# Default deny all traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-all
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]

Then add explicit allow rules per service pair.

Secret Hygiene

DO:
  ✓ Encrypt secrets at rest (etcd encryption)
  ✓ Use external secret stores (Vault, AWS SM, ESO)
  ✓ Rotate credentials regularly
  ✓ Audit secret access (K8s audit logging)
  ✓ Use short-lived credentials (IRSA, Workload Identity)

DON'T:
  ✗ Commit secrets to Git
  ✗ Log secrets (mask in app logs)
  ✗ Pass secrets as CLI arguments (visible in /proc)
  ✗ Use default ServiceAccount
  ✗ Store secrets in ConfigMaps

Audit Logging

# audit-policy.yaml
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: RequestResponse
  resources:
  - group: ""
    resources: ["secrets"]
  verbs: ["get", "list"]
- level: Metadata
  resources:
  - group: ""
    resources: ["pods"]

Supply Chain Checklist

[ ] Minimal base images (distroless, alpine)
[ ] Pin image digests, not tags
[ ] Scan in CI (Trivy, Grype)
[ ] Sign images (cosign)
[ ] Verify signatures at admission (Kyverno, Gatekeeper)
[ ] Private registry with access control
[ ] No package managers in production images
[ ] SBOM generation for all images

Quick Security Audit

# Find pods running as root
kubectl get pods -A -o json | jq -r '
  .items[] | select(.spec.containers[].securityContext.runAsNonRoot != true) |
  "\(.metadata.namespace)/\(.metadata.name)"'

# Find pods with host networking
kubectl get pods -A -o json | jq -r '
  .items[] | select(.spec.hostNetwork == true) |
  "\(.metadata.namespace)/\(.metadata.name)"'

# Find pods with privileged containers
kubectl get pods -A -o json | jq -r '
  .items[] | select(.spec.containers[].securityContext.privileged == true) |
  "\(.metadata.namespace)/\(.metadata.name)"'

# Check for default SA usage
kubectl get pods -A -o json | jq -r '
  .items[] | select(.spec.serviceAccountName == "default") |
  "\(.metadata.namespace)/\(.metadata.name)"'