Even a clean image is dangerous if it runs as root with a writable filesystem and every Linux capability. Lock down the runtime with a securityContext: non-root, no privilege escalation, dropped capabilities, read-only root, seccomp.
Kubernetes lets you constrain a container with a securityContext, and a handful of settings block the majority of escape and privilege paths. Run as a non-root user, forbid privilege escalation, drop all Linux capabilities, and make the root filesystem read-only. This is the single most impactful manifest change you can make to a workload.
apiVersion: v1
kind: Pod
metadata: { name: app }
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
seccompProfile: { type: RuntimeDefault } # filter dangerous syscalls
containers:
- name: app
image: registry.example.com/app@sha256:abc...
securityContext:
allowPrivilegeEscalation: false # can't gain more than it starts with
readOnlyRootFilesystem: true # can't write malware to disk
capabilities:
drop: ["ALL"] # start with zero Linux capabilities
resources:
limits: { cpu: "500m", memory: "256Mi" } # cgroups: cap the blast radiusLinux splits root's power into ~40 capabilities; containers get a default subset they almost never use. Drop ALL, then add back only the specific capability a workload genuinely needs (often none). This turns "this container can do root-ish things" into "this container can do exactly one thing," shrinking what any exploit can achieve.
Default containers keep caps like NET_RAW, CHOWN, SETUID... mostly unused.
drop: ["ALL"] # zero out everything
add: ["NET_BIND_SERVICE"] # ONLY if you must bind a port < 1024
Dangerous caps to never grant a normal app:
CAP_SYS_ADMIN "the new root" — mount, namespaces, common escape vector
CAP_SYS_PTRACE inspect/modify other processes
CAP_NET_ADMIN reconfigure networking
Most web apps run perfectly with drop:ALL and nothing added. Start there.A read-only root filesystem stops an attacker from writing tools or modifying your binaries — a huge reduction in what a foothold can do. Apps that need to write get an explicit, non-executable emptyDir mounted only where writes are required (a temp dir, a cache), so you allow exactly the writes needed and nothing else.
spec:
containers:
- name: app
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- { name: tmp, mountPath: /tmp } # only writable path
- { name: cache, mountPath: /app/cache }
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
# everything except /tmp and /app/cache is immutable. An attacker can't
# drop a binary in /usr/bin or overwrite your app. Grant writes narrowly.The shared kernel is the container boundary, and system calls are how a process reaches it. A seccomp profile filters which syscalls a container may make, so a kernel exploit relying on an exotic syscall is blocked outright. RuntimeDefault covers most apps; a custom profile locks it down further. AppArmor/SELinux (LSMs) add mandatory access controls on top.
The kernel is the boundary. seccomp filters the syscalls that reach it.
seccompProfile: { type: RuntimeDefault } # blocks ~44 dangerous syscalls,
# allows the common ones. Use this.
For high-value workloads, a custom profile allowlists ONLY the syscalls
your app makes -> a kernel exploit needing an unlisted syscall just fails.
AppArmor / SELinux (Linux Security Modules) add mandatory access control:
even root-in-container is confined to a policy. Defense in depth on the
one boundary that actually matters — the kernel.