Every non-privileged container on a Syself Linux node already runs under an enforcing AppArmor profile. You do not turn this on. AppArmor is a Linux kernel security module for mandatory access control (MAC). MAC means the kernel enforces a policy on a process no matter which user it runs as: even a root process cannot do what its profile forbids. That is exactly the property container isolation needs, because a process that breaks out of its container usually acts as root on the host, and normal file permissions no longer stop it. The profile is applied to every container rather than only the ones that request it: an opt-in control protects only the workloads whose authors already thought about the problem. ## AppArmor runs on every node The Syself Linux kernel runs AppArmor as its mandatory access control system, alongside landlock, lockdown, and yama in the kernel's security-module stack. SELinux is compiled out of the kernel. The two systems solve the same problem in different ways: SELinux attaches security labels to every file on disk, AppArmor attaches profiles to programs by path. On a sealed OS built from read-only dm-verity images and replaced on every update, there is no place for SELinux label management and nothing it would add, because container confinement comes from the runtime's profile either way. Shipping one MAC and removing the other also removes code that would otherwise sit unused. See [Node and OS security](/docs/hetzner/apalla/security/node-and-os-security) for the rest of the kernel hardening. For you this means AppArmor profiles work on every pod, and SELinux settings in a pod spec do nothing (details [below](#selinux-is-not-active)). ## What the default profile does containerd loads its AppArmor profile (`cri-containerd.apparmor.d`) when the node starts and applies it, in enforce mode, to every container that does not request something else. The profile blocks operations a normal application never performs but a container breakout does: - Mounting filesystems inside the container - Writing to most of `/proc` and to `/sys` - Tracing processes outside the container's own profile This is a safety net, not a full sandbox. It stops well-known breakout techniques while still working with ordinary workloads, which is why it can be on by default. The seccomp `RuntimeDefault` profile is on by default too (the kubelet sets `seccompDefault: true`), and the controls you add per workload (dropped capabilities, non-root, read-only root) narrow the attack surface much further. Two kinds of pods run without it: - **Privileged containers** (`privileged: true`) run unconfined. Privileged means full host access, so there is nothing left for a profile to protect. - **Pods that request `Unconfined`** (below). Verify it from any running pod: ```console $ kubectl exec -- cat /proc/1/attr/current # Expected: cri-containerd.apparmor.d (enforce) ``` ## Set the profile per pod The `appArmorProfile` field in the security context controls the profile. It arrived in Kubernetes 1.30 and is GA since 1.31. Set it at the pod level to cover all containers, or on one container to override the pod setting: ```yaml apiVersion: v1 kind: Pod metadata: name: hello-apparmor spec: securityContext: appArmorProfile: type: RuntimeDefault containers: - name: app image: your/image:tag ``` The types: - `RuntimeDefault`: containerd's profile, described above. Same as leaving the field unset on this platform. Set it anyway in manifests you ship to other clusters, because it turns "whatever the node does" into a declared guarantee. - `Localhost`: a custom profile already loaded on the node, named in the `localhostProfile` field. See the next section. - `Unconfined`: no AppArmor confinement. Use it only to check whether a denial comes from AppArmor. A namespace at the `baseline` or `restricted` Pod Security level rejects it. Older manifests use the `container.apparmor.security.beta.kubernetes.io/` annotation. It still works but is deprecated since 1.30. Migrate to the field; when both are present they must agree. ## Custom profiles (Localhost) A `Localhost` profile must already be loaded into the kernel of every node the pod can be scheduled on, or the container fails to start. On this platform the kubelet rejects a pod that names a missing profile, so a typo never leaves a workload silently unconfined. The node ships `apparmor_parser`, the tool that loads a profile, but the sealed OS has no writable profile directory and no package manager. Load custom profiles with a privileged DaemonSet that runs `apparmor_parser` against profiles you mount from a ConfigMap. Nodes are replaced, not patched, so anything loaded at runtime disappears with the node; the DaemonSet model handles that, because it runs on every new node before your workloads land there. Most workloads do not need a custom profile. Use one only when you must confine a specific application tighter than `RuntimeDefault` and a seccomp profile plus dropped capabilities is not enough. ## SELinux is not active SELinux is compiled out of the Syself Linux kernel. The Kubernetes API still accepts SELinux fields, so nothing breaks at apply time, but they have no effect on the node: - `securityContext.seLinuxOptions` is accepted and ignored. - SELinux-based volume relabeling does not happen. Volume access control comes from `fsGroup` and file permissions. - Helm charts written for SELinux-based platforms run unchanged; their SELinux settings are no-ops. This is a deliberate choice, not a gap. No compliance framework mandates SELinux by name: CIS asks for "SELinux or AppArmor", the Ubuntu STIG requires AppArmor, and managed Kubernetes services that use AppArmor nodes hold FedRAMP High and DoD IL5. If a requirement asks for mandatory access control, the enforced AppArmor profile satisfies it. If you relied on SELinux multi-category isolation to separate hostile tenants, use the stronger tools this platform has instead: a user namespace (`hostUsers: false`) to strip host privileges, or the `secure` RuntimeClass to move the pod into its own VM. See [Workload runtimes and isolation](/docs/hetzner/apalla/security/workload-runtimes-and-isolation). If you have a contractual need for enforcing SELinux specifically, Syself can build a SELinux-enforcing node-image variant per nodepool; it is not something you toggle in a manifest. Contact support. ## The full hardening stack AppArmor is one layer. The platform turns on the first two for you; the rest you set per workload. For each control, what it protects against: | Control | You set | Protects against | | ---------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------ | | AppArmor `RuntimeDefault` | Nothing (on by default) | Known breakout techniques: mounts, /proc and /sys writes, cross-profile ptrace | | Seccomp `RuntimeDefault` | Nothing (on by default via `seccompDefault`) | Exploits that need rare system calls; around 44 dangerous syscalls are blocked | | `restricted` Pod Security level | One label per namespace | Anyone deploying a pod without the controls below | | `runAsNonRoot` + drop `ALL` capabilities | Security context | A compromised process holding root or kernel privileges | | `readOnlyRootFilesystem` | Security context | Malware persisting inside the container image | | User namespace (`hostUsers: false`) | Pod spec | An escape landing as host root | | `secure` RuntimeClass (Kata) | Pod spec, bare metal only | An escape reaching the shared host kernel at all | The node's own hardening (sealed dm-verity layers, signed-modules-only, the hardened kernel build) protects the OS from the outside and from a compromised workload. The stack above protects your workloads from each other and shrinks what a compromised container can even attempt. Both are needed; neither replaces the other. A pod spec that passes the `restricted` level with every layer declared: ```yaml apiVersion: v1 kind: Pod metadata: name: hardened-app spec: securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 appArmorProfile: type: RuntimeDefault seccompProfile: type: RuntimeDefault containers: - name: app image: your/image:tag securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] ``` Roll the namespace label out audit-first before enforcing; see [Enforce Pod Security Standards](/docs/hetzner/apalla/security/enforce-pod-security-standards). For what each security-context field does, see [Set security contexts](/docs/hetzner/apalla/security/set-security-contexts). ## Related - [Set security contexts](/docs/hetzner/apalla/security/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) - [Node and OS security](/docs/hetzner/apalla/security/node-and-os-security) - [Security architecture](/docs/hetzner/apalla/security/security-architecture)