Benefits of a Kubernetes Security Context for AI Pods
Your inference pod pulls weights from outside, loads them through a stack of native libraries, and unless someone said otherwise, runs as whatever user the image picked, often root. A Kubernetes security context is the part of the pod spec that narrows what that process can do on the node. Below are five benefits, each with the YAML and the attack it blunts, then the cases where it bites. The reference is the Kubernetes security context docs.
Five benefits of a Kubernetes security context on model pods
The realistic threats are dull: a poisoned model file (pickle-based checkpoints can run code when loaded), a vulnerable dependency in the server, or an agent talked into a tool call it shouldn't make. A security context doesn't stop the break-in. It decides how much the intruder gets.
Run as non root so a hijacked process isn't root
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
Code execution now lands as UID 10001, so it can't modify root-owned files baked into the image. With runAsNonRoot: true the kubelet refuses to start a container that would run as UID 0, so a lazy base image fails loudly. Use a numeric runAsUser, because a username in the image's USER line can't be verified as non-root. fsGroup makes mounted volumes group-owned by that GID, so the model cache stays writable.
readOnlyRootFilesystem keeps payloads off the disk
containers:
- name: inference
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /cache
- name: dshm
mountPath: /dev/shm
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir:
sizeLimit: 50Gi
- name: dshm
emptyDir:
medium: Memory
A malicious checkpoint or injected tool call can't drop a script into the image or patch your installed Python packages. Anything that legitimately writes gets a small, explicit emptyDir. Point HF_HOME, the Hugging Face cache location, at /cache/hf. An emptyDir cache is wiped when the pod reschedules, so big models want a persistent volume or weights baked into the image.
allowPrivilegeEscalation false closes the setuid door
containers:
- name: inference
securityContext:
allowPrivilegeEscalation: false
This sets the Linux no_new_privs flag, so a setuid binary in the image can't hand the process more privileges than its parent had. Kubernetes defaults the field to true when you omit it, so you have to say it.
Drop capabilities on the pod to shed NET_RAW and friends
containers:
- name: inference
securityContext:
capabilities:
drop:
- ALL
Container runtimes grant a default set of Linux capabilities, and that set includes NET_RAW, the ability to craft raw packets. A model server that listens on port 8000 needs none of them. If something breaks, add back only the named one, for example add: [NET_BIND_SERVICE] for ports below 1024. Omit the CAP_ prefix in the manifest.
Seccomp RuntimeDefault trims the kernel attack surface
spec:
securityContext:
seccompProfile:
type: RuntimeDefault
Pods run Unconfined, with no syscall filtering, unless the node's kubelet runs with --seccomp-default (stable since v1.27, enabled per node). Setting RuntimeDefault yourself applies the container runtime's default filter, so a container escape has fewer kernel entry points to try.
Combine them in one Deployment and enforce restricted
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference
spec:
replicas: 1
selector:
matchLabels:
app: inference
template:
metadata:
labels:
app: inference
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: inference
image: <your-registry>/inference-server:<tag>
env:
- name: HF_HOME
value: /cache/hf
resources:
limits:
nvidia.com/gpu: 1
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /cache
- name: dshm
mountPath: /dev/shm
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir:
sizeLimit: 50Gi
- name: dshm
emptyDir:
medium: Memory
The same fields (minus readOnlyRootFilesystem, which is extra) are what the Restricted Pod Security Standard asks for. Turn on enforcement for the namespace and check the result:
kubectl label namespace <your-namespace> pod-security.kubernetes.io/enforce=restricted
kubectl exec deploy/inference -- id
kubectl exec deploy/inference -- touch /usr/local/bin/probe
id should report UID 10001, and touch should fail with Read-only file system.
When a Kubernetes security context is not worth it
- Writable paths you didn't know about. A read-only root breaks servers that write compiled kernels or temp files in odd places. Run realistic traffic in staging, watch for
Read-only file systemerrors, and mount anemptyDirper path. vLLM's Kubernetes deployment docs (v0.7.3) mount a memory-backed/dev/shm, so expect that one. - GPUs. The NVIDIA device plugin README requests GPUs through
resources.limitswith nosecurityContext. It mentions elevated privileges only for deploying the plugin itself in certain modes. Don't addprivileged: trueto your serving pod to make a GPU appear. I found no authoritative word on every driver and runtime combination, so test non-root on your GPU nodes first. - Workloads that need privilege.
allowPrivilegeEscalation: falsecan't coexist withprivilegedorCAP_SYS_ADMIN. Coding-agent sandboxes that mount filesystems or nest containers won't fit, and running untrusted code deserves a sandboxed runtime, not only a security context. - It limits damage on the node, not data leaving it. Pair it with network policies.
A few lines in the pod spec turn a compromised model server from root on your node into a locked-down process with nowhere to write, which is the gap that matters when the weights come from outside your walls.