Kubernetes security fundamentals
Securing Kubernetes is mostly a set of layers, each of which assumes the one above it has already failed. These are the layers to check first.
The 4 Cs
Cloud native security is described in four nested layers: Cloud (or data centre), Cluster, Container and Code. Each layer is only as safe as the one outside it. A perfect container image does not help if the cluster's API is open to the internet, and a locked-down cluster does not help if the cloud account is compromised.
Containers are just isolated processes
A container is not a small virtual machine. It is a normal Linux process with restricted views, built from two kernel features:
- Namespaces limit what a process can see: its own PIDs, mounts, network devices and user IDs.
- cgroups limit what it can use: CPU, memory and so on.
All containers on a node share one kernel, so a kernel bug can cross the boundary. Most of what follows is about shrinking that risk.
Benchmarks and supply chain
- Run kube-bench against nodes to check them against the CIS Kubernetes Benchmark. It reports pass, fail and a remediation hint for each control.
- Verify the checksum of any Kubernetes binary before you install it.
- Scan images for known vulnerabilities (for example with Trivy), build from minimal base images, and pin versions by digest.
Locking down the API
Everything goes through the API server, so it gets three gates in order: authentication (who are you), authorization (are you allowed, usually RBAC) and admission (is this request acceptable).
- Disable anonymous access and never expose the API server publicly if you can avoid it.
- Give people and workloads the least privilege they need. Prefer namespaced Roles over ClusterRoles.
- Pods get the
defaultservice account unless told otherwise. SetautomountServiceAccountToken: falseon pods that never call the API. - Protect the kubelet too: turn off anonymous access and use Webhook authorization so only the API server can use it.
- Do not leave the Kubernetes Dashboard,
kubectl proxyor broad port-forwards open as a back door. - All components talk over TLS. Know which certificates exist, who signs them and when they expire.
Secrets
A Kubernetes Secret is base64-encoded, not encrypted. Anyone who can read it, or read etcd, can decode it.
- Restrict who can
getorlistsecrets with RBAC. Listing is as dangerous as reading. - Turn on encryption at rest with an
EncryptionConfigurationfor the API server, so secrets are encrypted before they reach etcd. Prefer a KMS provider over a static key. - For stronger control, keep secrets in an external system such as HashiCorp Vault and sync them in with an operator.
Runtime isolation and detection
Because containers share a kernel, you can add a stronger boundary for risky workloads using a sandboxed runtime:
- gVisor intercepts system calls in a user-space kernel, so the application never talks to the real kernel directly.
- Kata Containers runs each pod in a lightweight virtual machine.
You select one per workload with a RuntimeClass:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata: { name: gvisor }
handler: runsc
---
spec:
runtimeClassName: gvisor # in the pod spec
To see what a process is actually doing, trace its system calls with strace, or use Falco, which watches syscalls cluster-wide and alerts on behaviour such as a shell starting inside a container or a sensitive file being read.
Hardening the pod
A securityContext removes most of what an attacker would use after getting in:
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
seccompProfile: { type: RuntimeDefault }
- A read-only root filesystem makes the pod effectively immutable: nothing can be dropped on disk and executed. Give the app a writable
emptyDironly where it truly needs one. - Never run
privileged: trueor mount the host's filesystem or container socket unless you must. - Use Pod Security Admission (the
restrictedlevel) to enforce these settings per namespace.
Network policy
By default every pod can talk to every other pod. Start from deny-all and then open only what is needed:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: prod }
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
Remember to allow DNS egress afterwards. On cloud platforms also block pods from reaching the node metadata endpoint (169.254.169.254), which can hand out the node's cloud credentials. Between services, mutual TLS authenticates both sides, where ordinary one-way TLS only authenticates the server.
Hardening the node
- Limit who can log in. Use SSH keys only, disable root login and password authentication.
- Remove packages and services you do not need, close ports you do not use and restrict the rest with a firewall such as
ufw. - Restrict loadable kernel modules.
- Use AppArmor profiles to confine what a program can access, and seccomp to limit which system calls it can make.
- Drop Linux capabilities the workload does not need, instead of running as full root.
Finally, add admission policy so the rules above are enforced instead of remembered. A cluster that relies on people to follow the checklist will drift.