Kubernetes Security Cheatsheet
A production-grade quick reference for securing Kubernetes clusters. Covers RBAC, PodSecurity standards, NetworkPolicies, runtime detection, secrets, image security, and audit or forensic commands with security context for every entry.
Kubernetes Authorization Chain
Every API request goes through this chain. Most security misconfigurations are failures at the authentication, authorization, or admission layer.
RBAC: Audit & Least Privilege
6 commandskubectl auth can-i --listList every action your current credentials can perform.
Production note: Run this as the ServiceAccount your workload uses (kubectl --as=system:serviceaccount:NS:NAME) to verify least-privilege before deploy.
kubectl auth can-i create pods --as=alice@example.com -n paymentsCheck whether a specific user/SA can perform an action in a namespace.
kubectl get clusterrolebinding -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .metadata.name'Find every binding to cluster-admin.
Warning: A surprising number of clusters have human users bound to cluster-admin. Audit these aggressively — cluster-admin is Kubernetes "root."
kubectl get rolebinding,clusterrolebinding -A -o wideList every RBAC binding cluster-wide.
kubectl create role NAME --verb=get,list --resource=pods --dry-run=client -o yamlGenerate a least-privilege Role YAML to commit.
Production note: Always start with the smallest verb set that works. Re-audit on every promotion (staging → prod).
kubectl describe clusterrole adminInspect what a built-in ClusterRole grants.
PodSecurity & SecurityContext
5 commandskubectl label namespace NS pod-security.kubernetes.io/enforce=restrictedEnforce the strictest PodSecurity profile on a namespace.
Production note: Use enforce / audit / warn modes together: enforce blocks, audit logs, warn shows a deprecation-style message in kubectl output.
kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].securityContext.privileged==true) | "\(.metadata.namespace)/\(.metadata.name)"'Find every privileged container in the cluster.
Warning: Privileged containers can break out to the host. They should be rare and tightly scoped (e.g. kube-system DaemonSets only).
kubectl get pods -A -o json | jq '.items[] | select(.spec.hostPID==true or .spec.hostNetwork==true) | .metadata.name'Find pods sharing host PID/network namespaces.
kubectl run debug --image=alpine --rm -it --overrides='{"spec":{"securityContext":{"runAsNonRoot":true,"runAsUser":1000,"seccompProfile":{"type":"RuntimeDefault"}}}}' -- shSpawn a debug pod with a hardened securityContext.
kubectl get psa -AList PodSecurity admission labels per namespace (if PSA enabled).
NetworkPolicy: Default-Deny & Egress
4 commandskubectl get networkpolicy -AList all NetworkPolicies cluster-wide.
Warning: A namespace with NO NetworkPolicy is allow-all. Default-deny + explicit allows is the production baseline.
kubectl describe networkpolicy NAME -n NSSee the resolved rules (which podSelectors match, which CIDRs are allowed).
kubectl exec -n NS POD -- nc -vz TARGET 443Verify a NetworkPolicy from inside a pod (open or blocked).
Production note: After applying a default-deny, run this from each microservice to confirm only the intended targets are reachable. Catches typos in label selectors.
cilium connectivity testRun Cilium's built-in policy connectivity test (if Cilium CNI).
Secrets & Certificate Management
5 commandskubectl get secrets -A -o json | jq '.items[] | select(.type=="Opaque") | "\(.metadata.namespace)/\(.metadata.name)"'List all custom (non-system) secrets cluster-wide.
kubectl get secret NAME -n NS -o jsonpath="{.data.password}" | base64 -dDecode a single secret value.
Warning: Anyone with secrets/get RBAC can do this. Audit RBAC for secret access aggressively. Treat it as a credential-extraction action.
kubectl get csrList CertificateSigningRequests (kubelet, custom certs).
kubectl auth can-i get secrets -n NS --as=system:serviceaccount:NS:SAVerify a ServiceAccount cannot read secrets it does not own.
Production note: Most workloads should NOT be able to read other workloads' secrets. Default RBAC is too permissive here.
kubectl rollout restart deployment/NAME -n NSForce pods to re-mount rotated secrets / certs.
Production note: Kubernetes does not auto-restart pods when a Secret changes. Use the secret-CSI driver, External Secrets Operator, or restart on rotation.
Audit, Forensics & Incident Response
5 commandskubectl get events -A --sort-by=.lastTimestampRecent cluster events (failed pulls, OOMKilled, evictions).
kubectl logs -n kube-system pod/kube-apiserver-NODE | grep "audit"Tail audit log entries from the API server (if audit logging enabled).
Production note: If you do not have audit logging on, you have no forensic trail. Configure --audit-policy-file with at least Metadata level on every prod cluster.
kubectl debug -it POD --image=busybox --target=CONTAINERAttach an ephemeral debug container to a running pod.
Warning: Ephemeral containers run in the same pod as the target. Restrict via RBAC; treat as a privileged action in production.
kubectl get pods -A -o json | jq '.items[] | select(.status.containerStatuses[]?.restartCount > 5) | .metadata.name'Find pods with high restart counts (often crash-loop / abuse).
kubectl top pods -A --sort-by=cpuPods by CPU — finds runaway processes (cryptominers, infinite loops).
Image Security & Admission
5 commandskubectl get pods -A -o json | jq '.items[].spec.containers[].image' | sort -uList every distinct image running in the cluster.
Production note: Pipe through `grep -E ":(latest|main|stable)"` to find mutable-tag images that bypass cryptographic provenance.
cosign verify --key cosign.pub IMAGEVerify a container image signature with Sigstore cosign.
syft IMAGE -o spdx-jsonGenerate an SBOM for an image (Syft).
trivy image --severity CRITICAL,HIGH IMAGEScan an image for known CVEs.
Production note: Wire this into CI and gate merges. Re-scan on a schedule for already-deployed images — new CVEs are published daily.
kubectl get validatingwebhookconfigurationsList admission webhooks (Kyverno, OPA Gatekeeper, Connaisseur, etc.).
Default-Deny NetworkPolicy
Drop this into a namespace to block all ingress and egress, then add explicit allow rules for the traffic that should exist.
network-policy-default-deny.yaml
Blast-radius controlapiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-payments-to-rds
namespace: payments
spec:
podSelector:
matchLabels:
app: payments-api
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 10.20.0.0/16
ports:
- protocol: TCP
port: 5432Common misconfigurations
The unsafe pattern, the replacement, and the reason the two are not equivalent in production.
Risky
# ClusterRoleBinding to cluster-admin
roleRef:
kind: ClusterRole
name: cluster-adminHardened
# Namespaced Role with only the verbs you need
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get","list","update","patch"]Why it matters: Bind humans and ServiceAccounts to scoped, namespaced Roles — never to cluster-admin. cluster-admin is "root" of Kubernetes; one compromised credential becomes total cluster takeover.
Risky
# Pod with no securityContext
spec:
containers:
- name: app
image: registry/app:latestHardened
spec:
containers:
- name: app
image: registry/app@sha256:abc...
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefaultWhy it matters: PodSecurity "restricted" requires non-root, read-only FS, dropped capabilities, and the default seccomp profile. Pin to a digest, not a tag, so the image cannot be silently rewritten.
Risky
# No NetworkPolicy in the namespace
# (Kubernetes default: any pod can talk to any pod)Hardened
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
spec:
podSelector: {}
policyTypes: [Ingress, Egress]Why it matters: Kubernetes networking is allow-all by default. A default-deny in every namespace, plus explicit allows for known traffic, dramatically shrinks lateral-movement blast radius after a single pod compromise.
Risky
# Secret as plaintext env in Deployment
env:
- name: DB_PASSWORD
value: "Hunter2-Prod-2024!"Hardened
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: payments-db
key: password
# ...and ideally projected via the secrets-store CSI driver
# pulling from Vault / AWS Secrets Manager.Why it matters: Plaintext secrets in manifests end up in Git history forever. Once leaked, the only fix is rotation — not a revert. Use external secret managers with the secrets-store CSI driver so the credential never touches source control.
Related learning paths
Cloud Native Security Engineering
A free course covering the primitives on this cheatsheet from beginner foundations to production practice.
ContinueKubernetes Security Simulator
Practice the decisions from this reference against realistic production security scenarios.
ContinueCloud Native Glossary
Definitions for SPIFFE, SVID, OPA, Falco, mTLS, and other cloud-native security vocabulary.
Continue