Kubernetes SecurityContext Generator

Build a pod and container securityContext that applies, starts, and passes the namespace profile you are actually running. It emits the whole Deployment, puts each field at the level it belongs to, and mounts the emptyDir a read-only root filesystem needs.

Workload

A Deployment is emitted so the result is something you can apply and we can check. Only the security fields below are parameterised; the rest is the minimum that makes the object valid.

Pod-level securityContext

spec.template.spec.securityContext. fsGroup, supplementalGroups and seccompProfile exist only here. Written on a container instead, they are unknown fields.

Makes the kubelet refuse to start the container if the image's user resolves to uid 0. It does not choose a user, and it cannot resolve a USER written as a name.

Container-level securityContext

spec.containers[].securityContext. These four exist only here. Where a field exists at both levels, the container value wins for that container.

Sets no_new_privs, so a setuid binary in the image cannot gain privileges the process did not start with.

Most images write a temp file within the first second of running, so this needs the writable paths below to be right.

Removes the set every container starts with, including NET_RAW, which on its own permits ARP spoofing of other pods.

Every capability, every host device, and no seccomp or AppArmor confinement. This is a root shell on the node with extra steps.

Namespace and Pod Security Admission

The baseline and restricted profiles are enforced by labels on the namespace. Nothing inside a pod opts into one, and an unlabelled namespace enforces nothing at all.

Turn it off if the namespace already exists. The pod is still checked against the profile you pick below.

pod-security.yaml

updates as you type

    Common mistakes

    These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

    1. Setting runAsNonRoot at the pod level and a root user at the container level

      The container-level securityContext wins for fields both define, so the container setting silently overrides the pod's intent.

      Instead:Know which fields exist at each level and that the container level takes precedence.

    2. Setting runAsNonRoot without runAsUser

      The kubelet then refuses to start any image whose USER is numeric-unknown, because it cannot verify the id is non-zero.

      Instead:Set runAsUser explicitly alongside it.

    3. Dropping all capabilities on a container that binds a low port

      Binding below 1024 needs NET_BIND_SERVICE, and dropping ALL removes it.

      Instead:Drop ALL then add back only what is needed, or bind a high port and map it in the Service.

    What securityContext actually controls, level by level

    securityContext is two different schemas that share a name. One sits on the pod and one sits on each container, they overlap in three fields, and the container wins where both apply. Almost every mistake on this object is a field written at the wrong level, or a field that does less than its name suggests. What follows is the set that bites people, with what the failure looks like when it arrives.

    Two schemas, one name, and no warning when you mix them

    fsGroup, fsGroupChangePolicy, supplementalGroups and seccompProfile exist only on the pod. allowPrivilegeEscalation, readOnlyRootFilesystem, capabilities and privileged exist only on the container. runAsUser, runAsGroup and runAsNonRoot exist in both, and the container value overrides the pod value for that container only. Put a container field at pod level and modern kubectl refuses the file; an older client, or one told not to validate, drops the field and applies an object that looks hardened and is not.

    spec:
      template:
        spec:
          securityContext:            # pod level
            runAsNonRoot: true
            runAsUser: 10001
            fsGroup: 10001            # pod only
            seccompProfile:           # pod only
              type: RuntimeDefault
          containers:
            - name: api
              securityContext:        # container level
                allowPrivilegeEscalation: false   # container only
                readOnlyRootFilesystem: true      # container only
                capabilities:                     # container only
                  drop: [ALL]
    
    # readOnlyRootFilesystem written at pod level:
    error: error validating data: strict decoding error:
      unknown field "spec.template.spec.securityContext.
      readOnlyRootFilesystem"

    runAsNonRoot does not pick a user

    It is an admission check, not an assignment. The kubelet resolves the user the image will run as and refuses to start the container if that is uid 0. If the image's USER is a number, the check passes. If it is a name, the kubelet has no way to resolve it against the image's /etc/passwd before the container exists, so it gives up and the pod never starts. The failure is CreateContainerConfigError, which reads like a broken config rather than a working image with the wrong USER line. Pair it with an explicit runAsUser and the answer no longer depends on how the image was written.

    # Dockerfile: works
    USER 10001
    
    # Dockerfile: fails at pod start
    RUN adduser -D app
    USER app
    
    kubectl get pod
    NAME        READY   STATUS                       RESTARTS
    api-7d9f    0/1     CreateContainerConfigError   0
    
    kubectl describe pod api-7d9f
      Error: container has runAsNonRoot and image
      has non-numeric user (app), cannot verify
      user is non-root

    A read-only root filesystem breaks on the first temp file

    readOnlyRootFilesystem: true is the highest-value line on the container and the one most likely to be reverted an hour later, because nearly every image writes something early: a temp file, a pid file, a unix socket, an unpacked template cache. The write fails with EROFS from inside the application, so the error comes out in the application's own words rather than as anything Kubernetes reports. An emptyDir at each writable path is the fix, and the generator emits one per path so the manifest is not correct-but-broken.

    securityContext:
      readOnlyRootFilesystem: true
    volumeMounts:
      - name: tmp
        mountPath: /tmp
    volumes:
      - name: tmp
        emptyDir:
          sizeLimit: 64Mi        # not a default: without it a
                                 # runaway file fills the node
    
    # Common extra paths, per stack:
    #   /var/run, /run          pid files and unix sockets
    #   /var/cache/nginx        nginx temp paths
    #   /home/app/.cache        anything that uses XDG paths

    Drop ALL, then add back as little as possible

    Containers start with a capability set nobody chose, including CHOWN, SETUID, SETGID and NET_RAW. NET_RAW on its own lets a compromised container craft raw packets and ARP spoof its neighbours on the node. Dropping ALL and adding back only what fails is the reviewable version of that decision. NET_BIND_SERVICE is the one people genuinely need, and only to bind below port 1024, which is usually a config change away from not being needed at all. allowPrivilegeEscalation: false belongs next to it: it sets no_new_privs so a setuid binary cannot climb out, and dropping ALL plus running non-root implies it without recording it anywhere.

    capabilities:
      drop: [ALL]
      add: [NET_BIND_SERVICE]   # only for ports < 1024
    
    # Better: listen on 8080 and let the Service map it,
    # then the add list is empty.
    ports:
      - port: 80
        targetPort: 8080
    
    # privileged: true is not "one more capability":
    #   every capability, every host device under /dev,
    #   no seccomp or AppArmor confinement.
    #   It is a root shell on the node with extra steps.

    seccomp: RuntimeDefault, and the annotation that stopped working

    RuntimeDefault applies the container runtime's own syscall filter, which blocks the obscure calls container escapes tend to be built on. It lives on the pod, at securityContext.seccompProfile.type. The trap is the field it replaced: seccomp.security.alpha.kubernetes.io/pod was an annotation, annotations do not fail validation, and it stopped being honoured in 1.25. A pod that carries only the annotation applies cleanly, reports nothing, and runs with no seccomp profile at all. The kubelet flag that would have covered it, --seccomp-default=RuntimeDefault, is off by default on managed distributions.

    # Honoured. Pod level.
    securityContext:
      seccompProfile:
        type: RuntimeDefault
    
    # Ignored since 1.25, and silently:
    metadata:
      annotations:
        seccomp.security.alpha.kubernetes.io/pod: runtime/default
    
    # Check what a running pod actually has:
    kubectl get pod api-7d9f \
      -o jsonpath='{.spec.securityContext.seccompProfile}'

    fsGroup rewrites the whole volume, every start

    fsGroup sets group ownership on mounted volumes so a non-root process can write to them, and the default way it does that is a recursive chown of the entire volume on every pod start. On a volume with a lot of files that is minutes of ContainerCreating on every restart, which usually gets diagnosed as a slow image pull. fsGroupChangePolicy: OnRootMismatch checks the top-level directory first and skips the walk when the group already matches. It is also not universal: hostPath is unaffected, and CSI drivers opt in or out through fsGroupPolicy, so fsGroup is not a guarantee that a volume ends up writable.

    securityContext:
      fsGroup: 10001
      fsGroupChangePolicy: OnRootMismatch
    
    # Always (the default):
    #   chown -R on every file, every start
    # OnRootMismatch:
    #   check the volume root's gid, skip if it matches
    
    # An emptyDir needs none of this: the kubelet
    # creates it world-writable, so any uid can write.

    Pod Security Admission is namespace labels, not pod fields

    baseline and restricted are enforced by labels on the namespace. Nothing inside a pod opts into a profile, which means the same manifest is enforced in one namespace and unenforced in the next, and a carefully hardened pod in an unlabelled namespace is checked by nobody. When it does reject something, it rejects the pod rather than the Deployment, so kubectl apply succeeds and the workload sits at 0 ready with the real message on the ReplicaSet. restricted requires runAsNonRoot, allowPrivilegeEscalation: false, capabilities dropping ALL, and a seccompProfile. It does not require readOnlyRootFilesystem, which is worth setting anyway.

    apiVersion: v1
    kind: Namespace
    metadata:
      name: apps
      labels:
        pod-security.kubernetes.io/enforce: restricted
        pod-security.kubernetes.io/enforce-version: latest
        pod-security.kubernetes.io/warn: restricted
    
    # What a rejection looks like from the outside:
    kubectl get deploy api
    NAME   READY   UP-TO-DATE   AVAILABLE
    api    0/2     0            0
    
    kubectl describe replicaset api-5c8d
      Error creating: pods "api-5c8d-" is forbidden:
      violates PodSecurity "restricted:latest":
      allowPrivilegeEscalation != false

    More kubernetes tools

    Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes QoS Class Calculator And where you sit in the eviction order Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes Label & Annotation Validator Why your image tag is not a legal label Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes ServiceAccount Token Decoder Bound or legacy, and when it dies Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Helm Chart Generator Chart.yaml, values, and working templates Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes HPA Generator Why it says unknown and never scales Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Ingress to Gateway API Which annotations silently stop working Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

    Elsewhere on the site