Add the per-pod and per-container settings that tell the kernel what a container may do: run as non-root, drop capabilities, block privilege escalation, and make the root filesystem read-only. A security context is a block in the pod or container spec that tells the Linux kernel what the container is allowed to do (it does not modify the container image). The platform sets none for you; you add them to your own workloads. Coming from the workload side, this is the per-pod step in the [workload-hardening ladder](/docs/hetzner/apalla/workloads/secure/set-security-contexts). > [!WARNING] > Platform components need elevated host access to function. Never apply restrictive Pod Security Standards to platform namespaces. ## Pod-level and container-level settings You can place a `securityContext` block in two places in a pod spec: **Pod level** (`spec.securityContext`): applies to all containers in the pod. Use this for settings that must be the same across all containers, such as the user and group IDs or the seccomp profile. **Container level** (`spec.containers[*].securityContext`): applies to one container only and overrides the pod-level field for the same setting. Use this for per-container settings: privilege escalation, capabilities, and the read-only filesystem flag. Some fields exist at only one level. `allowPrivilegeEscalation`, `capabilities`, and `readOnlyRootFilesystem` are container-only. `seccompProfile` works at either level; set it at the pod level to cover all containers. ## Run as a non-root user Most images default to root (UID 0). Running as root gives a compromised container full control of the container filesystem. `runAsNonRoot: true` tells Kubernetes to reject the container at startup if the image would run as root. Set it at the pod level: ```yaml spec: securityContext: runAsNonRoot: true ``` Use `runAsUser` and `runAsGroup` to set a specific numeric user ID and group ID. This is useful when the image does not declare its own user, or when a mounted volume requires a specific owner: ```yaml spec: securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 ``` > [!TIP] > If an image must run as root and you cannot change it, use a user namespace (`hostUsers: false`) instead. A user namespace maps container root to an unprivileged host user, so a container escape does not land as root on the node. See [Run a workload in a user namespace](/docs/hetzner/apalla/workloads/secure/run-in-a-user-namespace). ## Block privilege escalation Privilege escalation lets a process inside a container gain more permissions after it starts, for example by calling a `setuid` binary. Almost no application needs this. Set `allowPrivilegeEscalation: false` at the container level: ```yaml securityContext: allowPrivilegeEscalation: false ``` ## Linux capabilities Linux capabilities are fine-grained kernel privileges that control specific operations, such as binding a port below 1024, loading a kernel module, or changing file ownership. A container can run as non-root and still hold dangerous capabilities, or run as root with nearly all of them removed. Drop all capabilities, then add back only what a specific container needs: ```yaml securityContext: capabilities: drop: - ALL add: - NET_BIND_SERVICE # only if this container must bind a port below 1024 ``` The containerd configuration lets containers bind low ports (below 1024) without extra privileges. So most containers do not need `NET_BIND_SERVICE`. > [!TIP] > Start by dropping `ALL` with nothing in `add`. Run the container and check what fails. Add capabilities back one at a time when a specific error confirms they are needed. ## Read-only root filesystem `readOnlyRootFilesystem: true` mounts the container's own filesystem as read-only. The container can still write to volumes you mount explicitly. It cannot write to directories inside the image itself. ```yaml securityContext: readOnlyRootFilesystem: true ``` This stops a compromised container from modifying image files at runtime. It also makes all writable paths explicit in the pod spec, which is easier to audit. Some images write to paths like `/tmp` on startup. Mount an `emptyDir` volume there: ```yaml volumes: - name: tmp emptyDir: {} containers: - name: app volumeMounts: - name: tmp mountPath: /tmp securityContext: readOnlyRootFilesystem: true ``` ## Seccomp Seccomp (secure computing mode) is a Linux kernel feature that limits which system calls a container can make. A system call is a request from a process to the kernel, for example to open a file or create a network socket. Without a seccomp profile, a container can call any system call the kernel supports. Every container runs under the `RuntimeDefault` seccomp profile by default. The kubelet sets `seccompDefault: true`, so a pod that names no profile gets the runtime's profile (it blocks around 44 dangerous syscalls) instead of running unconfined. Declaring it makes the node default explicit and portable: ```yaml spec: securityContext: seccompProfile: type: RuntimeDefault ``` Set this at the pod level so all containers in the pod get it. The `restricted` Pod Security Standard requires it. A workload that needs a blocked syscall opts out with `seccompProfile.type: Unconfined`, which `baseline` and `restricted` reject. ## AppArmor AppArmor is mandatory access control: the kernel enforces a per-program policy that even a root process cannot bypass. Like the default seccomp profile, it is already on. Every non-privileged container runs under the runtime's enforcing profile without any field set. The `appArmorProfile` field in the security context controls it, GA since Kubernetes 1.31. Its types: `RuntimeDefault` (the runtime's profile, same as leaving the field unset on this platform), `Localhost` (a custom profile loaded on the node), and `Unconfined` (no confinement). ```yaml spec: securityContext: appArmorProfile: type: RuntimeDefault ``` See [Use AppArmor](/docs/hetzner/apalla/security/use-apparmor) for what the default profile blocks and how to load custom profiles. ## A complete example This Deployment satisfies the `restricted` Pod Security Standard: ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: secure-app spec: replicas: 1 selector: matchLabels: app: secure-app template: metadata: labels: app: secure-app spec: securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: app image: your/image:tag securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: - ALL ``` Common problems when applying this template: - The image runs as root and has no non-root user configured. Add a user in the Dockerfile, or use a user namespace. - The container writes to a directory inside the image at startup. Mount an `emptyDir` volume at that path. - The container needs a specific capability. Add it to `capabilities.add` and document the reason. ## Enforce these across a namespace Setting these fields on every pod by hand does not scale, and a single manifest that forgets them passes without notice. Pod Security Standards move the check to the namespace: label it once, and Kubernetes rejects any pod that does not meet the level. The `restricted` level requires exactly the context this page builds (non-root, dropped capabilities, seccomp `RuntimeDefault`). See [Enforce Pod Security Standards](/docs/hetzner/apalla/security/enforce-pod-security-standards) for the levels and an audit-first rollout that will not break running workloads. ## Add a user namespace A security context tells the kernel what a container may do. A user namespace changes who the container is on the host: set `hostUsers: false` and container root (UID 0) maps to an unprivileged host user, so an escape from a root container lands as "nobody" instead of host root. This is the answer when an image must run as root and you cannot change it. Pair it with the context above for the strongest per-pod isolation you can get without a VM. How user namespaces work is [Workload runtimes and isolation](/docs/hetzner/apalla/security/workload-runtimes-and-isolation); the how-to is [Run a workload in a user namespace](/docs/hetzner/apalla/workloads/secure/run-in-a-user-namespace). ## What the platform sets Two things are on already: the runtime applies its enforcing [AppArmor profile](/docs/hetzner/apalla/security/use-apparmor) to every non-privileged container, and the kubelet defaults every container to the `RuntimeDefault` seccomp profile. Everything else here is your responsibility. Every namespace runs at `privileged` until you label it. Platform components need privileges that the `restricted` level forbids. Applying a cluster-wide default that blocked privileged pods would break those platform components. You apply Pod Security Standards to your own namespaces, and you set security contexts on your own pods. ## Related - [Set security contexts on a workload](/docs/hetzner/apalla/workloads/secure/set-security-contexts) - [Enforce Pod Security Standards](/docs/hetzner/apalla/security/enforce-pod-security-standards) - [Workload runtimes and isolation](/docs/hetzner/apalla/security/workload-runtimes-and-isolation) - [Run a workload in a user namespace](/docs/hetzner/apalla/workloads/secure/run-in-a-user-namespace) - [Run a secure workload](/docs/hetzner/apalla/workloads/secure/run-in-the-secure-runtime) - [Security architecture](/docs/hetzner/apalla/security/security-architecture)