Seccomp and AppArmor Profiles in Kubernetes
A complete, plain-English guide to the two Linux security layers that decide which system calls and which files your containers may touch — from how they work inside the kernel to how to roll them out safely across a real cluster.
Picture a huge apartment building. Every family has its own locked apartment, and from the inside it feels like a private home. But behind the walls there is one single building manager who holds the master keys, runs the elevators, controls the water and decides who gets what. If a sneaky visitor ever tricked that manager, every apartment in the building would be in trouble.
Containers in Kubernetes live in exactly this kind of building. Each container feels like its own little computer, yet all of them share one Linux kernel — the building manager. Seccomp and AppArmor are the two rule books that tell that manager, “this tenant may ask for these few things only, and may walk into these few rooms only.” In this guide you will learn what each rule book is, how Kubernetes hands them to your pods, how to write and test your own, and how to avoid the mistakes that make teams give up on them too early.
1Why Containers Need More Than Walls
A container is a regular Linux process wearing a blindfold — and the blindfold is thinner than most people think.
A container is not a tiny virtual machine
A virtual machine carries its own kernel, so an attacker who breaks into it still faces a second wall before reaching the host. A container does not have that luxury. It is simply a process that the kernel has been told to show a limited view of the world: its own list of processes, its own network, its own file tree. Underneath, the same kernel serves every container on the node.
That shared kernel is the heart of the matter. Every time a program wants to do something real — open a file, send a network packet, start another program, allocate memory — it must ask the kernel through a special request called a system call. The Linux kernel offers well over three hundred of these on a typical 64-bit Intel or AMD machine, and each one is a door into the most powerful code on the server.
Think of a hotel where every guest has a private room, but all rooms share one control room in the basement. Guests never need most of the buttons in that control room. Yet if the control room had hundreds of buttons within reach and just one of them was faulty, a curious guest could press it and affect the entire hotel. Seccomp removes the buttons you never need. AppArmor puts a “staff only” sign on the corridors you should never walk down.
Most programs use a tiny slice of what the kernel offers
A typical web service needs a few dozen system calls to read configuration, listen on a port, talk to a database, write logs and exit politely. Even a big application rarely uses more than around a hundred. The rest — calls that load kernel modules, reboot the machine, swap the root file system, or trace other processes — sit unused, waiting for someone with bad intentions to find a bug in them.
Security people call this idea reducing the attack surface. If your container cannot even ask for a dangerous operation, then a flaw hiding behind that operation can never be reached from inside your container. You do not have to know about the flaw in advance. You simply closed the door.
“My container runs as a normal user, so it is safe.” Running as a non-root user is valuable, but it does not shrink the list of system calls the process can make. A bug in a kernel feature that an ordinary user can reach is still reachable. Profiles add a separate layer that does not depend on who the user is.
The story of a compromised web app
Scenario: an image-resizing service gets hacked
An attacker finds a flaw in a library your image service uses and runs commands inside the container. Without profiles, they can try to create new namespaces, poke at kernel features through the bpf call, mount file systems, or read sensitive files under the process information area. With a tight seccomp profile and an AppArmor profile, most of those attempts simply fail with “operation not permitted,” and your logs record the attempts. The break-in still happened, but it stays small and noisy instead of becoming a cluster-wide disaster.
2The Linux Building Blocks Behind Container Isolation
Seccomp and AppArmor are two layers in a stack. You need to know the other layers to understand why you still need these two.
When a container runtime such as containerd starts your container, it does not call one magic “make safe” function. Instead it stacks several independent Linux features on top of each other. Each one answers a different question, and each one can fail or be misconfigured without the others noticing. Knowing the full stack helps you decide where a profile fits and what it cannot do.
Namespaces
Control what the process can see: its own process list, network interfaces, mounts, hostname and user IDs. They hide things; they do not forbid actions.
Control groups (cgroups)
Control how much the process can use: CPU time, memory, number of processes, disk throughput. They stop one container from starving its neighbours.
Capabilities
Split the all-powerful root user into about forty smaller powers, such as binding low ports or changing file ownership. Containers usually keep only a small default set.
Seccomp
Filters which system calls the process may make, and optionally which argument values are acceptable. It works at the kernel’s front door.
AppArmor or SELinux
Linux Security Modules that decide which files, paths, network operations and other resources a program may touch, regardless of normal permissions.
No-new-privileges and read-only root
Small switches that stop a process from gaining more power through special programs, and stop it from changing its own file system.
flowchart TD
A["Your application process"] --> B["Namespaces: limited view"]
B --> C["Cgroups: limited resources"]
C --> D["Capabilities: limited powers"]
D --> E["Seccomp: limited system calls"]
E --> F["AppArmor or SELinux: limited resources by name"]
F --> G["Shared Linux kernel"]
Read the diagram from top to bottom. A request from your application passes every layer on its way down. Any layer can say no. That is why security folks speak of defence in depth: you assume one layer will eventually have a hole and make sure the next layer catches what slipped through.
Think of getting into a secure office. Namespaces are like being given a map that only shows your floor. Cgroups are the limit on how many photocopies you may make. Capabilities are the keycard that opens only some doors. Seccomp is the receptionist who checks what kind of request you are making. AppArmor is the guard who checks which rooms you are heading to. Any one of them can stop you.
What each layer controls
| Layer | Question it answers | Typical failure it prevents |
|---|---|---|
| Namespaces | What can I see? | Reading host processes or host network details |
| Cgroups | How much can I use? | Fork bombs and memory hogs that crash the node |
| Capabilities | Which special powers do I hold? | Loading drivers or changing system time |
| Seccomp | Which kernel requests may I make? | Reaching rarely used, bug-prone kernel features |
| AppArmor | Which files and resources may I touch? | Writing to sensitive system paths or mounting file systems |
Capabilities and seccomp overlap but are not the same. Dropping a capability makes a privileged operation fail even when the system call is allowed. Blocking the system call makes the whole request fail before capabilities are even checked. Using both is safer than trusting either alone.
3Seccomp From the Ground Up
Seccomp stands for “secure computing mode,” and it is the kernel’s built-in bouncer for system calls.
First, what is a system call?
Programs run in a restricted area called user space. They cannot directly touch the disk, the network card or the memory manager. To do anything real, they raise their hand and ask the kernel by number: “please do request number 257 with these details.” That request is a system call, and the kernel answers it in a privileged area called kernel space. Everyday things like reading a file, creating a socket or starting a thread are all system calls underneath.
Because every meaningful action passes through this single narrow gateway, it is the perfect place to put a checkpoint. That is exactly what seccomp does: it inspects each request at the gateway and decides what to do with it before the kernel acts.
Imagine a restaurant kitchen with a service window. Customers cannot walk into the kitchen; they hand an order slip through the window. Seccomp is a manager standing at the window with a list of allowed orders. If your slip says “burger,” it goes through. If it says “open the safe,” the manager refuses and may even call security.
How seccomp grew up
Strict mode (2005)
The first version allowed only four calls: read, write, exit and return from a signal. Anything else killed the process. Great for crunching untrusted numbers, useless for real applications.
Filter mode with BPF (Linux 3.5, 2012)
Developers could now attach a small program written in the Berkeley Packet Filter language that examines each call and returns a verdict. This is the version containers use today.
Richer actions and logging
Later kernels added verdicts to log a call without blocking it, kill the whole process instead of one thread, and return custom error codes.
User notification (Linux 5.0)
A filter can pause a call and ask a supervising program in user space to decide. This powers advanced sandboxes but is rarely used directly in Kubernetes profiles.
How a filter works inside the kernel
A seccomp filter is attached to a thread. Every child process or thread created afterwards inherits it, and once attached it can never be removed — only made stricter by stacking another filter on top. When several filters exist, the kernel runs them all and obeys the most restrictive verdict. This one-way design is crucial: even if an attacker takes over the process, they cannot loosen the rules.
For each system call the filter sees the call number, the processor architecture and the raw values of up to six arguments. It compares these against its rules and returns a verdict. Because the filter looks at plain numbers, it is very fast, and it cannot be fooled by clever naming.
flowchart LR
A["Process makes a system call"] --> B{"Seccomp filter checks number, architecture and arguments"}
B -->|"Allowed"| C["Kernel runs the call"]
B -->|"Return an error"| D["Process gets a permission error and keeps running"]
B -->|"Kill"| E["Process is terminated with a bad-system-call signal"]
B -->|"Log"| F["Call runs and an audit record is written"]
The common verdicts, in plain words
| Verdict | What the program experiences | When to use it |
|---|---|---|
| Allow | The call works normally | Calls the application truly needs |
| Errno | The call fails with an error code, usually “operation not permitted” | Safe default for blocked calls; most programs cope gracefully |
| Kill process | The whole process ends immediately | Calls that should never happen, where continuing is dangerous |
| Trap | A signal is delivered that a debugger can catch | Debugging and special sandboxes |
| Log | The call works, and the kernel records it | Learning phase while building a profile |
| Notify | The call pauses until a supervisor decides | Advanced container sandboxes |
Seccomp can read argument values but cannot follow a pointer to see what a string says. That means it cannot tell whether a program is opening a harmless file or a secret one, because the file name lives behind a pointer. Naming files and paths is the job of AppArmor.
4Anatomy of a Seccomp Profile
A profile is just a structured list that tells the runtime how to build the kernel filter for you.
Nobody writes BPF by hand for containers. Instead, container runtimes follow a standard JSON profile format originally popularised by Docker and now shared across the container ecosystem. The runtime reads the file, translates it into a BPF program and installs it just before your application starts. You only need to understand the pieces of the file.
Default action
What happens to any call that no rule mentions. This single setting decides whether the profile is an allow-list or a deny-list.
Architectures
Which processor families the profile covers, such as 64-bit Intel, 64-bit ARM, and the older 32-bit variants some binaries still use.
Syscall rules
Groups of call names paired with an action. One group might allow a hundred calls; another might force a specific error on two others.
Argument conditions
Optional tests on argument values, letting you allow a call only when it is used in a harmless way, such as only for certain flag combinations.
Capability conditions
Some runtime profiles allow extra calls only when the container actually holds a related capability, keeping the profile compact and sensible.
Flags
Options such as asking the kernel to log every non-allowed call, which is handy while you are still learning what an application needs.
Allow-list or deny-list?
There are two ways to think about a profile. In an allow-list profile, the default action is to block, and you name only the calls you permit. This is the strongest posture because anything you forgot to think about is blocked automatically. In a deny-list profile, the default is to allow and you name specific dangerous calls to refuse. It is easier to write, but it fails open: a call added to a future kernel is allowed until someone notices.
An allow-list is a party guest list: only people whose names are on it get in. A deny-list is a “banned visitors” list: everyone is welcome unless you remembered to ban them. Guest lists are safer, but you have to put in the effort to write every name down.
Choosing the right error for blocked calls
For blocked calls, the usual choice is to return an error rather than kill the process. Well-written programs expect that some operations may be refused and have a plan B. Killing the process, by contrast, gives the user a cryptic crash with a bad-system-call signal. Reserve the kill verdict for calls that should never appear at all in your application, where silent continuation would be worse than a crash.
There is one special case worth knowing. When new versions of the standard C library start using a newer system call, they first try it and, if the kernel replies “function not implemented,” they quietly fall back to the older call. If a profile instead returns “operation not permitted” for that unknown newer call, the library does not fall back — it reports a failure. Good profiles return the “not implemented” answer for calls that did not exist when the profile was written. This exact trap broke many containers when a newer thread-creation call appeared, and it is a lesson in why default errors deserve thought.
System call names and numbers differ between processor families. A profile recorded on an Intel server can be wrong on an ARM node, because ARM never had some old calls that Intel still carries. If your cluster mixes architectures, test the profile on each one and make sure the architecture list is complete.
5AppArmor From the Ground Up
AppArmor is a Linux Security Module that gives each program a written rule book about the files and resources it may use.
What is a Linux Security Module?
Normal Linux permissions work on users and groups: if a file is readable by user “web,” then any program running as “web” can read it. This is called discretionary access control because the file’s owner decides. The trouble is that a hacked program inherits every permission of its user, even though it only ever needed a few files.
Linux Security Modules add mandatory access control. The administrator writes rules that apply to a specific program, and even the owner of a file cannot override them. The kernel checks these rules in addition to normal permissions. AppArmor, SELinux and a few others are different implementations of this idea.
Normal permissions are like giving a worker a master keycard because they are an employee. AppArmor is like adding a rule on top: “this worker, while doing this job, may only enter the supply room and the loading dock, no matter what the keycard says.”
How AppArmor thinks
AppArmor is path-based. A profile describes access in terms of file paths and patterns, such as “may read anything under the application folder” or “may never write to the system configuration area.” Profiles are attached to programs, and when that program runs, the kernel enforces its profile on it and on anything it starts if the rules say so.
A profile can cover many kinds of access: reading, writing, appending, memory-mapping files, executing other programs, using specific network families and protocols, holding particular capabilities, sending signals, tracing other processes and mounting file systems. Rules can allow or explicitly deny. A deny rule always wins over an allow rule, which makes it easy to carve out forbidden spots inside otherwise permitted areas.
Enforce
Rules are applied. Anything not allowed is blocked and recorded in the audit log.
Complain
Rules are not enforced; violations are only logged. Perfect for learning what an application really does.
Abstractions
Reusable bundles of common rules, such as “base” access every program needs, so profiles stay short and readable.
Child profiles
Rules for programs that the main program launches, which lets each helper get tighter permissions than its parent.
Everyday tools you will meet
Check status
The status utility lists which profiles are loaded and whether each is in enforce or complain mode. On a Kubernetes node this is the quickest way to confirm a profile exists.
Load or replace profiles
The parser tool compiles a text profile and loads it into the kernel. It can also replace an already loaded version without a reboot.
Generate and refine
Helper tools watch a program in complain mode and propose rules from the logged behaviour, then let you accept or reject each suggestion.
Where AppArmor lives
AppArmor is built into the kernels of Ubuntu, Debian and SUSE-family distributions and is typically enabled out of the box there. Red Hat-family systems prefer SELinux instead. This matters a lot in Kubernetes because the feature only works on nodes whose operating system has AppArmor switched on. Always check the node image your cloud provider or platform team uses before planning around it.
AppArmor versus SELinux in one glance
| Aspect | AppArmor | SELinux |
|---|---|---|
| Model | Path-based rules per program | Label-based rules on every file and process |
| Learning curve | Gentler, readable text profiles | Steeper, powerful but more complex |
| Common distributions | Ubuntu, Debian, SUSE | Red Hat family, Fedora |
| Kubernetes integration | Profile field on pods and containers | SELinux options in the security context |
This guide focuses on AppArmor because it is the most common choice on Ubuntu-based nodes. If your nodes run a Red Hat-family system, the same defence-in-depth thinking applies, but you would use SELinux there instead of AppArmor. Seccomp works the same on both.
6Seccomp and AppArmor: Different Questions, Better Together
People often ask which one to choose. The honest answer is that they guard different doors.
Seccomp judges the type of request. AppArmor judges the target of the request. A bank offers a nice parallel: one rule says “customers may deposit and withdraw but never open the vault,” and another says “customers may enter the lobby and the teller area but not the staff corridor.” You want both rules because each catches things the other cannot.
| Question | Seccomp | AppArmor |
|---|---|---|
| What does it look at? | System call number and raw arguments | File paths, network types, capabilities, signals, mounts |
| Can it block by file name? | No, it cannot read strings behind pointers | Yes, this is its main strength |
| Can it block a whole class of kernel features? | Yes, by refusing the call entirely | Only for those features it understands |
| Where is it defined? | A JSON profile translated into a kernel filter | A text profile loaded into the kernel by the parser |
| Kernel requirement | Seccomp support, present in virtually all modern kernels | AppArmor enabled on the node’s operating system |
| Typical sweet spot | Removing dangerous or unused kernel doors | Protecting sensitive paths and limiting file access |
Seccomp is the bouncer who checks the type of ticket you hold: “dance floor,” “VIP” or “backstage.” AppArmor is the guard inside who knows the floor plan and keeps you out of certain rooms even when you hold a valid ticket. A crowd that gets past one of them still has to deal with the other.
Two attacks, two different catches
Attack A: loading a kernel program through a rarely used call
An attacker tries to use a powerful kernel feature that your application never touches. Seccomp blocks the system call itself, so the attempt never reaches the vulnerable code. AppArmor would have a hard time describing this attack in terms of file paths, so seccomp is the right tool.
Attack B: writing to a sensitive kernel setting file
An attacker uses ordinary open and write calls — which your application legitimately uses — but aims them at a protected system settings file. Seccomp cannot tell this write from a harmless one because they use the same calls. AppArmor refuses it because the path is forbidden.
Use the runtime-default versions of both as your baseline everywhere. Then add custom profiles only for workloads that are high-value, internet-facing, or handle untrusted input. Do not try to hand-tune every pod on day one.
7How Kubernetes Delivers a Profile to the Kernel
Kubernetes itself never talks to seccomp or AppArmor. It passes a request down a chain of helpers until the kernel finally applies it.
It helps to remember that Kubernetes is a manager, not a worker. When you declare that a pod should use a security profile, the control plane simply stores your wish. The real work happens on the node, where several programs hand the wish along like a relay race. Understanding each runner tells you where to look when something goes wrong.
sequenceDiagram
participant U as Engineer
participant A as API server
participant S as Scheduler
participant K as Kubelet on node
participant C as Container runtime
participant R as Low-level runtime
participant L as Linux kernel
U->>A: Submit pod with security settings
A->>S: New unscheduled pod
S->>A: Chosen node
A->>K: Pod assigned to this node
K->>K: Check profile is available and supported
K->>C: Create container with profile settings
C->>R: Prepare container specification
R->>L: Install seccomp filter and AppArmor profile
R->>L: Start the application process
What each runner does
The API server stores your intent
It validates that the profile type and field names are legal for the cluster version and saves the pod. It does not know whether any node actually has the profile file or the AppArmor module.
The scheduler picks a node
By default the scheduler is blind to profiles. If only some nodes have your custom profile, you must steer the pod with labels and affinity rules, or make sure every node carries the profile.
The kubelet checks and translates
The kubelet is the node agent. It verifies that the node supports the requested feature, resolves where a custom profile should be found, and tells the container runtime what to do through a standard interface.
The container runtime builds the specification
Runtimes such as containerd or CRI-O turn the request into a standard description of the container, including the seccomp rules and the AppArmor profile name.
The low-level runtime talks to the kernel
A tool like runc compiles the profile into a kernel filter, attaches it, applies the AppArmor profile, drops capabilities and finally starts your program. From that instant the rules are in force and cannot be loosened.
Think of a school trip permission slip. The parent writes it (you), the school office files it (API server), the teacher on the bus checks it (kubelet), the tour guide reads it (runtime), and the security guard at the museum gate actually enforces it (kernel). The guard never saw the original form, so if a step in the chain loses a detail, the guard enforces something different from what the parent wanted.
Problems before the kubelet step appear when you submit the pod. Problems at the kubelet or runtime step appear as pod events such as a failed container creation. Problems inside the application at the kernel step appear as strange permission errors in the application’s own logs or in the node’s audit log. Knowing which phase you are in narrows your search quickly.
8Seccomp Profiles in Kubernetes
Kubernetes gives you three seccomp choices and two places to set them. Simple on paper, subtle in practice.
The three profile types
RuntimeDefault
Uses the profile that ships with the container runtime. It blocks a few dozen risky calls while keeping nearly all normal applications working. This is the recommended baseline for almost every workload.
Localhost
Uses a profile file you provide, stored on the node under the kubelet’s seccomp folder and referenced by a relative path. This is how custom, tighter profiles are used.
Unconfined
No seccomp filtering at all. Every system call is allowed. Sometimes needed for special system tools, but it should be a visible, justified exception.
Pod level and container level
You can set the profile in the pod’s security settings, where it applies to every container in the pod including init containers, or in a single container’s security settings. A setting on the container wins over the setting on the pod. This lets you give a whole pod the safe default while loosening or tightening one container that has special needs, such as a log shipper or a network helper.
Pod-level settings are like the house rules for a family: “everyone is home by nine.” Container-level settings are a note for one child: “your cousin may stay out until ten tonight.” The specific note beats the general rule, but only for that child.
Making RuntimeDefault the cluster-wide default
Historically, if a pod did not mention seccomp, Kubernetes left the container unconfined, which meant no filtering. That surprised many teams, because Docker had applied a default profile for years. Modern Kubernetes lets a node administrator switch the kubelet so that any pod without an explicit setting receives the runtime default automatically. This feature went through alpha and beta stages and became generally available in Kubernetes 1.27.
Turning it on is one of the highest-value, lowest-effort security moves available. It raises the floor for every workload, including ones nobody remembers to configure. The cost is that a small number of older or unusual applications may break, so test it in a staging cluster first and keep a short list of workloads that need an explicit exception.
A short history, so old blog posts make sense
| Era | How seccomp was set | Status today |
|---|---|---|
| Early versions | Annotations on the pod with special names | Deprecated, then removed from Kubernetes in version 1.27 |
| Kubernetes 1.19 onward | Official field in the security settings of the pod or container | Current and supported way |
| Kubernetes 1.22 to 1.27 | Kubelet option to default every pod to the runtime profile | Generally available from 1.27 |
If you copy a manifest from an older article that uses the annotation style, it may be silently ignored on a modern cluster, leaving your pod unprotected while you believe it is safe. Always use the field-based style and verify the effective setting on the running pod.
How the profile type interacts with other settings
Two other settings change how seccomp behaves. A container marked as privileged effectively runs without the seccomp filter, so asking for a profile on a privileged container gives a false sense of security. And the setting that forbids privilege escalation turns on a kernel flag that stops the process from gaining extra powers through special executable files, which also supports safe filter installation. A well-hardened pod combines a runtime-default or custom seccomp profile, no privilege escalation, dropped capabilities and a read-only root file system.
Example: a hardened API pod
A payments API pod runs as a non-root user, drops all capabilities, forbids privilege escalation, uses a read-only root file system and sets the seccomp type to RuntimeDefault at the pod level. This combination satisfies the strictest built-in Kubernetes policy level and blocks a long list of container-escape tricks without touching the application.
9AppArmor Profiles in Kubernetes
AppArmor in Kubernetes follows the same idea as seccomp, with one big extra requirement: the node must already know the profile.
The three profile types
RuntimeDefault
The runtime’s built-in AppArmor profile, which blocks mounting file systems, writing to sensitive kernel setting areas and similar risky actions.
Localhost
A profile you wrote and loaded on the node, referenced by its name. The name must match a profile already loaded into the kernel on that very node.
Unconfined
No AppArmor restrictions for this container. Use only with a clear reason and review.
From annotations to a real field
For many years AppArmor in Kubernetes was configured through pod annotations whose names included the container’s name. It worked but was awkward, easy to typo and hard to validate. Starting with Kubernetes 1.30, AppArmor support became generally available through a proper field in the security settings, at both pod and container level, with the same three types you saw for seccomp. The older annotations are deprecated, and you should migrate manifests to the field-based form.
Confirm the node supports AppArmor
The node’s operating system must have the module enabled in its kernel, and the container runtime must be built to use it. If not, a pod that asks for AppArmor will be refused by the kubelet.
Load the profile on every eligible node
Custom profiles exist only in the memory of the kernel they were loaded into. A reboot forgets them unless a service reloads them at startup.
Reference the profile in the pod
Choose the type and, for custom profiles, give the exact loaded name. Spelling mistakes cause the pod to fail rather than silently run unprotected, which is the behaviour you want.
Verify on the running container
Check the process’s attributes on the node, or the profile list, to be sure the intended profile is attached and in enforce mode.
Using a custom AppArmor profile is like hiring a security guard who must be trained at each office building separately. If you send a worker to a building where the guard has never heard of the instructions, the building refuses to let the worker start. That refusal is annoying, but it is much safer than letting the worker in without a guard.
What the default profile typically restricts
The runtime’s default AppArmor profile is intentionally broad but useful. It stops containers from mounting file systems, from writing to many sensitive entries in the kernel’s process and system information areas, and from tracing processes that belong to other profiles. Most ordinary applications never notice these limits. They matter only when an attacker, or a clumsy tool, tries to reach beyond the container’s own space.
A cluster with mixed node images may have AppArmor on some nodes and not on others. Managed Kubernetes services use different node operating systems, and some default images use SELinux or no mandatory access control module at all. Check the node image, and do not claim AppArmor coverage in a security review until you have confirmed it on the nodes that actually run your pods.
Controlling where pods land
If you cannot put the profile on every node, label the nodes that carry it and add node selectors or affinity rules to the pods that need it. The scheduler will then place those pods only where the profile exists. This is a practical bridge, but the long-term goal should be uniform nodes with profiles delivered automatically, so scheduling stays simple.
10The Runtime Default Profiles Explained
The default profiles are the quiet heroes of container security. Knowing what they do tells you what you get for free.
When people say “RuntimeDefault,” they mean the profile shipped by the container runtime. The seccomp version grew out of the one that Docker introduced around 2016 after studying which calls normal containers needed. It is an allow-list with a long set of permitted calls and a carefully chosen group of blocked ones. It aims for maximum compatibility with minimum risk, which is why it is safe to switch on almost everywhere.
Kinds of calls the default blocks
Changing the kernel itself
Loading kernel modules, replacing the running kernel, and rebooting the machine. No normal application needs these.
Mounting and root swaps
Attaching new file systems or changing the root directory, which are classic container-escape ingredients, are allowed only when the container holds the matching strong capability.
Kernel key management
Calls that manipulate the kernel’s key store, which is shared in ways that are awkward for containers.
Powerful kernel programs
Loading programs into the kernel and similar advanced tracing features are restricted unless explicitly permitted.
Clock and system settings
Changing the system time or host-wide settings, which would affect every container on the node.
Creating new namespaces
Making new isolation spaces is limited, because nested namespaces have repeatedly been used to reach buggy kernel code.
Why compatibility is the design goal
A default that breaks applications would be switched off by frustrated teams, giving no protection at all. So the designers chose to block calls that nearly no application uses and to allow conditionally the ones that need strong capabilities. A web server, a database or a batch job almost never trips over the defaults. Applications that do are usually system-level tools, such as debuggers, network analyzers, container-in-container builders and some security agents.
The default profile is like the standard safety rules of a public swimming pool: no diving in the shallow end, no running, no glass bottles. Almost every swimmer is happy with these rules. Only the lifeguard training team needs special permissions, and they get them explicitly.
When the default is not enough
Default is too loose
A runtime default must allow what thousands of different programs need, so it permits many calls your single application will never use. A custom allow-list built for one application can be far tighter, often cutting allowed calls to a fraction of the default.
Default is too tight
Some specialised workloads need calls the default refuses, such as performance profilers or tools that create nested containers. These need a custom profile that adds only the missing calls, or a carefully reviewed exception.
Default is changing under you
Runtimes update their default profiles over time. Some have restricted newer and historically bug-prone kernel interfaces such as asynchronous I/O rings after repeated security flaws. If an application suddenly breaks after a node upgrade, a changed default is a prime suspect.
A privileged container is, in practice, run without the default seccomp and AppArmor protections. Treat the privileged setting as a fire alarm in reviews. If you see it, the profile discussion is moot until the privileged setting is justified or removed.
11Getting Custom Profiles onto Your Nodes
Writing a good profile is half the job. Delivering it to every node, every time, is the other half.
Where the files must live
For seccomp, a Localhost profile is a file on the node’s disk inside a dedicated folder under the kubelet’s working directory. The pod refers to it by a path relative to that folder. If the file is missing, the container cannot be created, and the pod shows an error in its events. For AppArmor, the profile must be loaded into the node’s kernel under a known name, not merely stored as a file.
Because Kubernetes does not copy these files for you, you need a delivery method. Teams usually pick from the following approaches, often combining two of them.
Bake into the node image
Profiles are part of the machine image, so every node starts with them. Very reliable, but changing a profile means building and rolling out a new image.
Configuration management
Existing tools that manage node files copy the profiles and reload them. Good for fleets of long-lived machines that already use such tools.
A node-level helper pod
A privileged helper pod runs on every node, reads profiles from a shared store and places or loads them. It works without touching the machine image, though it needs careful protection because it is powerful.
The Security Profiles Operator
A Kubernetes-native project that stores profiles as cluster objects and distributes them to nodes automatically. This is the most complete answer and has its own chapter below.
Protecting the delivery path
The folder holding profiles is a sensitive spot. Anyone who can write a loose profile there, or load a permissive AppArmor profile under a trusted name, can quietly weaken your protection. Treat profile files like code: store them in version control, review changes, sign or checksum them where possible, and restrict who and what can write to the node locations.
Problem
Profiles are copied to nodes by hand or by ad-hoc scripts. New nodes created during scaling or repair do not get them, so pods fail randomly or, worse, are moved to nodes where protection differs.
Why It’s Harmful
Inconsistent nodes create intermittent failures that are hard to reproduce. Teams respond by weakening profiles to unconfined, which silently removes the protection everywhere.
Correct Approach
Deliver profiles through an automated, declarative mechanism that runs on every node, including new ones. Keep the source of truth in version control and verify coverage continuously.
Node pools that scale up and rolling upgrades create fresh machines all the time. Any delivery method that depends on a human remembering to run a script will eventually miss a node. Make profile installation part of the node’s startup path or a controller that continuously reconciles.
Versioning profiles with applications
A profile and the application it protects are a matched pair. When the application adds a feature that needs a new system call, the profile must change in the same release. Name profiles with a version or release tag, roll them out before the application update, and keep the previous version around so you can roll back quickly. This avoids the nasty situation where a new application version meets an old profile on some nodes.
12Building a Custom Profile: The Safe Workflow
The goal is a profile tight enough to matter and loose enough to never page you at three in the morning.
The biggest fear around custom profiles is breaking production. That fear is justified if you write a profile once, in a hurry, and flip it to enforcing mode. It disappears when you follow a staged workflow that learns first, tests second and enforces last.
flowchart LR
A["Record behaviour in a test environment"] --> B["Merge and review the call list"]
B --> C["Deploy in log-only mode"]
C --> D["Watch real traffic and fix gaps"]
D --> E["Switch to enforcing mode"]
E --> F["Monitor and update with each release"]
F --> A
Record
Run the application in a test environment while a recorder collects every system call it makes. Tools exist for this, including the recorder in the Security Profiles Operator, tracing tools and node-level gadgets. Drive the application through startup, normal traffic, errors, health checks, shutdown and rarely used features.
Review
Read the resulting list like an auditor. Question every surprising entry. Some calls are used by the language runtime, some by libraries, and some by startup scripts you forgot about. Remove calls that are clearly incidental, such as ones from debugging tools left in the test image.
Log-only rollout
Deploy the profile in a mode where non-allowed calls are recorded instead of blocked. Run it on a canary slice of real traffic for long enough to include daily, weekly and month-end behaviour.
Fix the gaps
Every logged violation is either a missing legitimate call, which you add, or a suspicious call, which you investigate. Repeat until the log is quiet for a full business cycle.
Enforce
Switch blocked calls to return an error. Keep the previous log-only version ready for fast rollback, and tell the on-call team what changed.
Maintain
Rebuild the profile whenever the application, its dependencies, its base image or the node kernel changes in a meaningful way. Automate this in your delivery pipeline so it does not depend on memory.
Building a profile is like writing a guest list for a wedding. You first watch who actually comes to similar events, then draft the list, then run a rehearsal dinner where uninvited guests are only noted, and only on the big day do you really turn people away at the door. Skipping the rehearsal is how a grandmother gets turned away.
Common reasons recordings miss things
| Blind spot | Why it happens | How to cover it |
|---|---|---|
| Rare code paths | Tests never exercise error handling or special features | Include failure injection and full feature tests |
| Startup and shutdown | Recorders attached after startup miss early calls | Start the recorder before the container begins |
| Different processor type | Recorded on one architecture, run on another | Record and test on every architecture you deploy |
| Library upgrades | New library versions use newer system calls | Re-record after each dependency or base image change |
| Environment differences | Test cluster lacks the production storage or network plugins | Use a production-like staging environment |
Begin with a profile that starts from the runtime default and only removes a handful of calls you are sure your application never uses. You still gain a measurable reduction in attack surface, with far less risk of breakage. You can tighten further over time as your confidence and tooling grow.
13The Security Profiles Operator
A Kubernetes add-on that treats security profiles as first-class cluster objects instead of loose files.
The Security Profiles Operator, often shortened to SPO, is a project in the Kubernetes special interest group ecosystem. An operator is a program that runs inside the cluster and keeps reality matching a declared description. SPO takes the painful parts of profile management — recording, storing, distributing and binding — and turns them into ordinary Kubernetes resources that you can create, review and delete with the same tools you use for everything else.
flowchart TD
A["Engineer applies a profile object"] --> B["Operator controller in the cluster"]
B --> C["Node agent on every node"]
C --> D["Profile file or loaded profile on the node"]
E["Pod refers to the profile"] --> F["Kubelet and runtime"]
D --> F
G["Recording request for a workload"] --> B
B --> H["Generated profile object from observed behaviour"]
What the operator gives you
Profile objects
Seccomp profiles, and in newer releases other kinds, become cluster resources. You manage them with normal declarative workflows and version control.
Automatic distribution
A node agent installs each profile on every node, so new and replaced nodes receive them without manual steps.
Recording
You ask the operator to observe selected pods and it generates a profile from what they really did, using either logs or an eBPF-based recorder.
Binding
A binding links a profile to workloads that use a given container image, so teams do not have to edit every manifest to apply protection.
Log enrichment
Raw audit lines name only process numbers. The enricher adds the pod, namespace and container, making violations understandable to humans.
Status reporting
Each profile object reports whether it is installed, partially installed or in error, so you can see coverage across the fleet.
Without the operator, managing profiles is like a company that hands out paper rule books and hopes every office receives the new edition. With the operator, it is like a company intranet: you publish a page once, every office sees it automatically, and the page shows who has read it.
Recording with the operator
A recording request selects workloads by label and asks the operator to watch them. When the workloads finish or are deleted, the operator produces a profile object containing the calls it saw. You then review that object like any other change and attach it to the workload. This shortens the learn step of the workflow from days of scripting to a few declarative objects.
The operator runs components with high privileges on every node, because it must write profile files and load kernel rules. Install it from trusted sources, pin its version, limit who may create profile objects, and monitor its own health. A compromised operator could weaken protection cluster-wide.
The project is actively developed, and the list of supported profile types, installation methods and cluster platforms changes between releases. Read the project’s release notes for the exact versions you plan to run before depending on a specific feature.
14Enforcing Profiles with Cluster Policy
A profile that engineers may forget to request protects nobody. Policy makes the safe choice the default and the unsafe choice visible.
Pod Security Standards
Kubernetes defines three built-in levels of pod hardening, called the Pod Security Standards. They are privileged, baseline and restricted. Seccomp is part of this scheme. The baseline level forbids explicitly setting seccomp to unconfined. The restricted level goes further and requires the seccomp type to be either the runtime default or a custom Localhost profile. These standards give you a shared vocabulary and a ready-made checklist.
| Level | Seccomp requirement | Who it suits |
|---|---|---|
| Privileged | No restriction | Trusted system components such as storage drivers and network plugins |
| Baseline | Must not be explicitly unconfined | Ordinary applications; a sensible minimum for most namespaces |
| Restricted | Must be runtime default or Localhost | Security-sensitive and internet-facing workloads |
Pod Security Admission modes
The built-in admission controller applies a chosen level to a namespace using labels. It has three modes that you can combine. Enforce rejects pods that break the rule. Audit lets them run but writes an annotation in the audit log. Warn lets them run but prints a warning to the person who submitted the pod. A smart rollout uses warn and audit first, collects the list of offenders, fixes them, and only then turns on enforce.
Warn
Friendly messages to developers right when they apply a manifest. Zero risk to running workloads.
Audit
Silent record of violations for security teams to review in bulk and track over time.
Enforce
Hard stop. Violating pods are refused. Turn this on only after warn and audit are clean.
Going beyond the built-in levels
The built-in standards do not know about your custom profile names, nor can they insist that a particular AppArmor profile be used. For that, teams use policy engines such as Kyverno or OPA Gatekeeper, or the built-in validating admission policy feature. These can check that every pod in a namespace has a seccomp type set, that Localhost profiles come only from an approved list, that privileged containers are not used, and can even add the setting automatically through mutation when a pod forgot it.
Policy is the building’s front desk. A visitor who forgets a badge is not simply waved in. The desk either gives them a correct badge on the spot, mutates the request, or politely refuses entry, validates and denies. Either way, nobody wanders the halls without a badge.
A safe rollout plan
Inventory
List current workloads that have no seccomp setting, use unconfined, or run privileged. This is your starting map.
Warn and audit
Switch on warn and audit modes at the baseline level across namespaces. Share the output with application teams.
Default the node setting
Enable the kubelet option that applies the runtime default to pods without a setting, one node pool at a time, starting with non-critical ones.
Enforce baseline, then restricted for sensitive namespaces
Move to enforcement gradually. Grant narrow, documented exceptions for system components that truly need more.
Add custom profiles for crown jewels
Only after the baseline is solid, build tailored profiles for the few workloads where the extra effort pays off.
15Observability: Logs, Metrics and Detection
A blocked call that nobody sees is either a hidden outage or a hidden attack. You need to see both.
Where seccomp violations show up
When a filter blocks a call with an error, the application usually just sees a failure and carries on, so the kernel’s own log is the best witness. Kernels write seccomp audit records when a process is killed by the filter or when the log verdict is used. These appear in the node’s audit system or kernel messages as records of a specific type, containing the process number, the system call number and the architecture. Translating a call number back to a name needs a lookup table, which is why the operator’s log enricher is so handy.
Where AppArmor denials show up
AppArmor writes readable lines that include the word “DENIED,” the operation attempted, the profile name, and the file path or resource that was refused. In complain mode the same lines appear with the word “ALLOWED” so you can see what would have been blocked. Because the path is included, these lines are far easier to understand than raw seccomp records.
Seccomp’s log is like a security camera that records “someone pressed button 435 at 2:14.” AppArmor’s log is like a camera with a caption: “someone tried to open the server-room door at 2:14.” Both are useful, but one needs translating.
Signals worth watching
| Signal | Where to find it | What it usually means |
|---|---|---|
| Pod events about failed container creation | Pod description and cluster events | Missing profile file, unsupported AppArmor, wrong profile name |
| Application errors saying “operation not permitted” | Application logs | A needed call or file is blocked by a profile |
| Bad system call signal and sudden exits | Container exit status and restart counts | A call with the kill verdict was attempted |
| Seccomp audit records | Node audit log or kernel messages | Log-mode hits or kills; shows exact call numbers |
| AppArmor denied lines | Node audit log or kernel messages | Refused file, network or mount operation with its path |
| Profile status objects not fully installed | Operator status | Some nodes lack the profile; coverage gap |
Turning logs into alerts
Collect the node audit logs into your central logging system. Create dashboards counting denials by namespace, workload and profile. Alert on sudden jumps, which often follow a deployment that changed behaviour, and on any denial involving especially dangerous calls, which may indicate an intrusion attempt. Pair this with a runtime detection tool such as Falco, which watches system calls and process behaviour and can flag things like a shell starting inside a container that never runs shells.
During the log-only phase, expect plenty of entries from harmless startup behaviour. Group by call or path, remove the duplicates, and decide on each unique item once. A short, reviewed list beats a flood nobody reads.
Example: an alert that caught a real problem
A team noticed a spike of AppArmor denials from a reporting service at night. The denials showed attempts to write into a system settings area. Investigation found a vulnerable dependency being exploited by a scanner. The profile had already blocked the writes, and the alert gave the team a day to patch before any damage.
16Troubleshooting Playbook
When a profile breaks something, a calm, ordered approach finds the cause faster than guessing.
Identify the phase
Did the pod fail to start, or did it start and then misbehave? Failure to start points to node setup. Misbehaviour after start points to a blocked call or file.
Read the events and exit status
Events state when a profile is missing or unsupported. The exit status tells you whether the process was killed by a bad-system-call signal.
Check the node logs
Look at audit records or kernel messages on the node where the pod ran, around the time of failure. Match the process, the call number or the denied path.
Reproduce in log-only mode
Switch the specific workload to log-only temporarily in a safe environment. Learn exactly which calls or paths are needed.
Make the smallest fix
Add only the single call or path you proved is required. Resist the urge to switch to unconfined, which fixes the symptom by removing the whole defence.
Record the lesson
Write down why the call was needed and add a test that exercises it, so the next profile rebuild never forgets it.
Symptom to cause lookup
| Symptom | Likely cause | First thing to try |
|---|---|---|
| Container will not be created, message mentions a profile file | Seccomp Localhost file missing on that node | Confirm the file exists in the node’s seccomp folder with the exact relative path |
| Pod rejected on a node, message mentions AppArmor not enabled | Node operating system lacks AppArmor | Schedule onto supported nodes or use the matching module for that OS |
| Pod fails after node reboot only | AppArmor profile was loaded in memory and not reloaded | Add a startup step that reloads profiles |
| Application works in staging but fails in production | Different architecture, kernel or runtime default | Compare nodes and re-record on production-like machines |
| New application version suddenly fails | New library uses a newer system call | Re-record and allow the call, returning a not-implemented error for unknown future calls |
| Random crashes with bad-system-call signal | Kill verdict hit by a rarely used path | Switch to error verdict temporarily and log |
When production is on fire, switching a pod to unconfined feels like the quickest fix. It is, and it quietly turns off every protection for that workload. If you must do it, treat it as a temporary incident measure with an owner and an expiry date, and follow up with a real fix.
17Performance and Compatibility Considerations
Teams worry that security filters will slow everything down. In practice the cost is small, and the compatibility questions matter more.
Is there a speed penalty?
Seccomp filters run for every system call, so a very large or poorly structured filter adds a little time to each one. Kernel developers have worked to keep this tiny: modern kernels can cache the decision for simple allowed calls so that repeat calls skip most of the work. For typical web services and databases, the difference is hard to measure. Only programs that make millions of tiny system calls per second might notice, and even then the effect is usually a small percentage.
AppArmor adds checks whenever a program touches files or other controlled resources. Rules are compiled into an efficient form when loaded, and the overhead is generally low. Extremely heavy file workloads with huge numbers of path lookups can see more impact than average, which is a good reason to benchmark in your own environment before and after.
A quick ticket check at a stadium gate takes a second per person and hardly changes how long the game takes to start. A badly organised gate with a hundred different forms would cause queues. The same is true for filters: keep the rules simple and structured, and the cost stays invisible.
Compatibility questions that deserve attention
Kernel versions
Newer system calls and actions require newer kernels. A profile written for a modern kernel might name calls an older node does not know.
Processor architectures
Calls differ between Intel and ARM machines. Mixed clusters need profiles tested on both.
Language runtimes
Managed runtimes such as Java, Go and Node.js use many background threads and memory tricks. Their needs change between versions.
Sidecars and agents
Service meshes, log shippers and security agents have their own call patterns and may need separate profiles from the main container.
Base image libraries
Switching the base image can change which system calls your program uses, even when your own code did not change.
Container runtime versions
Runtimes ship different default profiles. A runtime upgrade can tighten or loosen the baseline underneath you.
Make profile testing part of every node image, runtime and kernel upgrade. A short suite that starts each key workload, runs a smoke test and checks for denials in the logs catches most surprises before they reach production.
18What Profiles Stop, and What They Cannot
Honest security means knowing where your protection ends. Profiles are strong, but they are not a force field.
Attacks they commonly blunt
Container escape through mounting tricks
Several well-known escapes rely on a container creating or mounting special file systems. The default seccomp profile refuses mounting without strong capabilities and the default AppArmor profile refuses mount operations. Researchers analysing a widely reported control-group escape in 2022 noted that containers running with these default protections were not exploitable in the usual way, which is a strong real-world argument for turning defaults on everywhere.
Exploiting rarely used kernel features
Many kernel vulnerabilities live in niche features reachable only through unusual system calls or flag combinations. When your profile never allows those calls, an entire family of future bugs becomes unreachable from your container, even bugs nobody has found yet.
Post-exploitation tooling
After breaking into an application, attackers like to trace processes, dump memory, load tools and write to configuration areas. Tight profiles break many of these steps and raise noisy alarms.
What they cannot do
What they handle well
- Shrinking the number of kernel doors reachable from a container
- Preventing writes and reads on protected system paths
- Limiting damage from unknown future kernel bugs in blocked features
- Giving clear audit trails of suspicious attempts
- Costing very little at run time
What they cannot do
- Fix bugs in system calls your application legitimately uses
- Stop a flaw in your own code from leaking data your app may read
- Replace patching the kernel, runtime and images
- Provide a separate kernel for strongly hostile tenants
- Stay correct automatically as applications change
When you need a stronger wall
If you run code written by strangers, such as a platform that executes customer programs, a shared kernel with filters is not enough. Stronger isolation approaches put a barrier between the workload and the host kernel. Some intercept system calls in a user-space kernel written in a memory-safe language. Others run each pod inside a lightweight virtual machine with its own kernel. Both cost some performance and compatibility but raise the bar substantially.
| Approach | Isolation strength | Overhead | Best for |
|---|---|---|---|
| Runtime-default profiles | Moderate; shared kernel | Very low | All ordinary workloads as a baseline |
| Custom seccomp and AppArmor | Higher; shared kernel with a narrow surface | Low | Internet-facing and high-value services |
| User-space kernel sandbox | High; most calls never reach the host kernel | Moderate, with some compatibility limits | Running semi-trusted or customer code |
| Lightweight virtual machines per pod | Very high; separate kernel | Moderate, with higher start-up cost | Multi-tenant platforms and hostile code |
19Design Patterns and Anti-Patterns
Teams that succeed with profiles tend to repeat the same good habits. Teams that struggle tend to repeat the same four mistakes.
Patterns that work
Secure by default
Turn on the runtime default at the node or policy level so protection exists even when engineers forget. Exceptions must be requested, not assumed.
Learn, log, then enforce
Always move through recording and log-only phases before blocking. It turns profile rollout into a routine, low-drama activity.
Profile as code
Store profiles in version control, review changes, test them in the pipeline and ship them with the application release.
Tiered protection
Use defaults for most workloads, custom profiles for crown jewels, and sandboxes for untrusted code. Match effort to risk.
Anti-patterns to avoid
Problem
The team fixes every profile-related error by setting the workload to unconfined, and never goes back.
Why It’s Harmful
Over months, the cluster fills with unprotected workloads while dashboards still say “profiles in use.” The real security posture is much worse than the paperwork suggests.
Correct Approach
Find the specific blocked call or path and allow just that. Track unconfined exceptions in a register with owners and review dates.
Problem
A custom seccomp profile uses an allow-everything default and lists a few blocked calls, then is described as “hardened.”
Why It’s Harmful
It fails open. New kernel features, or calls nobody remembered to list, are permitted by default, so the profile gives much less protection than it appears to.
Correct Approach
Use an allow-list with a blocking default, built from recorded behaviour and reviewed by humans.
Problem
A profile is recorded once during launch and never updated, while the application and its libraries change every sprint.
Why It’s Harmful
The profile drifts away from reality. Either legitimate calls start failing and teams loosen the profile in panic, or the profile permits calls that no longer matter.
Correct Approach
Re-record and retest automatically on every meaningful change to the application, dependencies, base image or runtime.
Problem
Profiles are applied to privileged containers or to pods running as root with every capability, on the belief that the profile will contain the damage.
Why It’s Harmful
Privileged containers largely bypass these filters, and powerful capabilities can undo the intended limits. The profile is decoration.
Correct Approach
Remove privilege first, drop capabilities, run as non-root, and only then layer profiles on top for defence in depth.
20Choosing Your Protection Level and Pairing It with Other Controls
Profiles work best when they sit inside a wider hardening plan. This chapter helps you decide how far to go for each workload.
A simple decision path
Not every pod deserves a handcrafted profile. Spending a week tuning an allow-list for a small internal tool wastes effort that could protect your payment service. A short decision path keeps the work proportional to the risk. Ask how exposed the workload is, how sensitive its data is, and how much you trust the code it runs.
flowchart TD
A["New workload"] --> B{"Runs code you do not trust?"}
B -->|"Yes"| C["Use a sandboxed runtime plus profiles"]
B -->|"No"| D{"Internet facing or handles sensitive data?"}
D -->|"Yes"| E["Build a custom seccomp profile and an AppArmor profile"]
D -->|"No"| F["Use RuntimeDefault for both layers"]
E --> G["Add policy checks and monitoring"]
F --> G
C --> G
The diagram is deliberately simple. Everything flows into the same final step, which is policy and monitoring, because even the lowest-risk workload should be covered by a baseline rule and visible in your logs. The difference between paths is only how much custom work sits in the middle.
Think of how a city protects buildings. Every building gets a smoke detector and a fire exit, which is the default. A bank gets cameras and a vault door, which is the custom profile. A prison gets thick walls and guards, which is the sandboxed runtime. Nobody puts a vault door on a garden shed, and nobody leaves a bank with only a smoke detector.
Controls that should travel with every profile
Seccomp and AppArmor are strongest when the process holding them has little power to begin with. Several simple settings in the pod’s security section shrink that power and cost almost nothing to apply.
Run as a non-root user
Ensures the application process does not hold the all-powerful user identity inside the container, which removes a large group of easy attacks.
Drop all capabilities
Start from nothing and add back only the single power the application proves it needs, such as binding to a low network port.
Forbid privilege escalation
Stops a process from gaining extra rights through special executable files, and supports safe use of seccomp filters.
Read-only root file system
Prevents the application from altering its own program files, so attackers cannot plant tools or modify scripts. Use small writable scratch areas for what truly needs writing.
User namespaces for pods
A newer Kubernetes feature that maps the container’s root to an ordinary user on the host, so even an escape lands with little power. Check your version and runtime for support.
No host sharing
Avoid sharing the host’s network, process list or file system with pods unless absolutely required, because each share weakens the walls profiles depend on.
Controls around the pod
Profiles protect a running container, but attackers have other ways in. Reduce the chance of an initial break-in by scanning images for known flaws, keeping base images small, and patching often. Restrict the network paths a compromised pod could use with network policies, so a hacked service cannot freely reach databases or the metadata service of the cloud provider. Limit what the pod’s service account token can do with careful role-based access rules, since stolen tokens are a favourite target. Finally, patch the node operating system and the container runtime, because a filter cannot protect against a bug in a call you must allow.
Putting it together for a public checkout service
The service runs as a non-root user with all capabilities dropped, a read-only root file system and no privilege escalation. It uses a custom seccomp profile built from recordings, an AppArmor profile that permits only its application folder and one temporary folder, and a network policy allowing traffic only to the payment database. Admission policy refuses any version of the deployment that omits these settings, and denials flow to a dashboard that the on-call engineer watches.
Putting it together for an internal reporting job
The nightly job uses the runtime default for both layers, runs as a non-root user, and has a narrow network policy. It gets no custom profile, because the added effort would not meaningfully change the risk. A baseline admission rule keeps it from drifting into an unsafe configuration.
Non-root, dropped capabilities, read-only file systems and runtime defaults cost minutes per workload and prevent a surprising share of real attacks. Do these everywhere before investing in custom profiles anywhere.
A perfectly hardened pod running on an unpatched node, pulling unscanned images, with an over-powered service account is still a risk. Treat profiles as one brick in a wall, and check regularly that the other bricks are in place.
Who owns what
Profiles fail most often because nobody is sure whose job they are. A clear split of responsibilities avoids this. The platform team owns the cluster-level pieces: node images, the kubelet defaults, the profile delivery mechanism, admission policy and the central log pipeline. Application teams own the behaviour of their own workloads: they provide the test suites that exercise every feature, review recorded call lists, and fix their code or libraries when a call looks suspicious. The security team sets the minimum standards, approves exceptions, and reviews the dashboards for anything unusual.
When these three groups meet regularly, even for thirty minutes a month, profile work stops being a surprise that lands on one tired engineer. Exceptions get reviewed before they become permanent. Upgrades are planned with profile testing in mind. And when a real incident happens, everybody already knows where the logs are and who can change what.
Platform team
Delivers profiles to nodes, sets cluster defaults, runs admission policy and keeps the operator and node images healthy.
Application teams
Provide realistic tests, review recorded behaviour, and own the fixes when an application needs a new call or file.
Security team
Defines the standards, approves and expires exceptions, and watches denial trends for signs of attacks.
Measuring progress
Choose a few simple measures and track them monthly. The percentage of pods with an explicit non-unconfined seccomp setting shows coverage. The number of unconfined or privileged exceptions shows how much risk you are knowingly carrying. The number of custom profiles with a current recording shows freshness. The count of denials investigated within an agreed time shows whether monitoring is real. Report these to security leadership in plain language, with trend arrows, so that progress is visible and funding for the work is easier to justify.
21Best Practices and Common Mistakes
If you remember only one chapter for your next architecture review, make it this one.
Do This
- Turn on the runtime default for every workload through node settings or admission policy
- Use the field-based syntax and verify the effective profile on running pods
- Drop capabilities, run as a non-root user and forbid privilege escalation alongside profiles
- Build custom profiles from recordings, review them, and roll out in log-only mode first
- Keep profiles in version control and ship them with the application release
- Deliver profiles to nodes automatically, including new and replaced nodes
- Alert on denials and on unusual spikes after deployments
Avoid This
- Switching to unconfined as a first reaction to any error
- Copying a giant profile from the internet without understanding it
- Using allow-by-default custom profiles and calling them hardened
- Applying profiles to privileged containers and trusting them
- Assuming AppArmor exists on every node of every managed cluster
- Recording on one architecture and deploying on another
- Letting profiles go stale while applications change
A review checklist you can reuse
Baseline coverage
Is every namespace at least at the baseline policy level, and does the node-level default apply runtime protection to pods that omit it?
Exception register
Is there a list of every workload that is unconfined or privileged, with an owner, a reason and a review date?
Node consistency
Do all nodes share the same operating system image, kernel feature set and profile delivery mechanism?
Custom profile hygiene
For each custom profile, is there a source repository, a recording method, a test suite and a rollback plan?
Visibility
Are denials collected centrally, labelled with pod and namespace, and tied to alerts that someone actually reads?
Upgrade testing
Does every kernel, runtime and base image upgrade run a profile compatibility test before reaching production?
A dashboard saying “98 percent of pods have a seccomp profile” is a good start. It says nothing about whether those profiles are tight, current or actually enforced. Measure quality too: how many allowed calls, how many exceptions, how fresh the last recording is.
Give each custom profile an owner, a backlog and a release process, just like any piece of software. Profiles with no owner are the ones that end up unconfined during the first incident.
22Real-World and Industry Examples
Seccomp and AppArmor are not niche research toys. They quietly protect software you probably used today.
Web browsers
Major browsers such as Chrome and Firefox use seccomp filters on the sandboxed processes that render web pages. The reasoning is the same as in Kubernetes: the page-rendering process handles untrusted content from the internet, so it is stripped of every kernel door it does not need.
Docker and container runtimes
Docker introduced a default seccomp profile for all containers in 2016 and Kubernetes-focused runtimes inherited the idea. That single decision gave millions of containers a baseline defence without asking users to do anything.
Lightweight virtual machine monitors
Firecracker, the technology behind several serverless platforms, applies tight seccomp filters to its own threads. Even if a guest breaks out of its virtual machine, the monitor process that it lands in is already boxed in.
System service managers and remote access tools
The systemd service manager offers a system-call filter option for every service it runs, and the OpenSSH server uses a sandbox for its pre-authentication process. These show that the same approach protects traditional servers, not just containers.
Regulated industries
Banks, healthcare providers and government agencies often require hardened container baselines as part of their compliance frameworks. Industry benchmarks for Kubernetes hardening recommend setting a seccomp profile on all workloads, and many audit teams now check for it explicitly.
Managed Kubernetes services
Cloud providers publish hardening guidance that recommends runtime-default seccomp and, where the node image supports it, AppArmor or SELinux. Some also offer sandboxed runtimes for stronger isolation of untrusted pods. Always read your provider’s current documentation for the node images and features available in your region and version.
23Frequently Asked Questions
They answer different questions, so using both gives better protection than either alone. If your nodes do not support AppArmor, seccomp plus dropped capabilities plus a non-root user is still a strong baseline, and on SELinux-based systems SELinux plays AppArmor’s role.
Make the runtime default seccomp profile apply to every workload, either through the kubelet default setting or through admission policy. It costs little, protects everything, and breaks very few applications.
Most ordinary applications run fine. Problems appear mostly with system-level tools such as profilers, network analyzers and programs that build containers inside containers. Test in staging, watch the logs and keep a small exception list.
The container cannot be created, and the pod’s events explain that the profile could not be loaded. This is intentional: failing loudly is safer than running unprotected.
No. Seccomp sees system call numbers and raw argument values but cannot read the file name stored behind a pointer. Use AppArmor, SELinux or file permissions for path-based control.
In practice a privileged container runs without the usual seccomp and AppArmor restrictions, so naming a profile on it provides a false sense of safety. Remove the privileged setting before relying on profiles.
The seccomp annotations were removed from Kubernetes in version 1.27, and the AppArmor annotations are deprecated since the field-based form became generally available in 1.30. Always move to the field-based settings and verify the result on a running pod.
Record it. Use the recorder in the Security Profiles Operator, tracing tools, or node-level gadgets while running a thorough test suite. Then review the list manually before using it as an allow-list.
An error is the safer default for most calls, because well-written software handles refusals gracefully. Use the kill verdict only for calls that must never occur, where continuing would be dangerous. For calls that do not yet exist in your profile’s era, return a not-implemented error so libraries can fall back.
Yes for seccomp, which is a kernel feature available on virtually all node images. For AppArmor, check whether your provider’s node image has it enabled. For custom profiles you need a delivery method that works without logging into nodes, such as the Security Profiles Operator.
Usually the overhead is too small to notice. Heavy workloads that make enormous numbers of tiny system calls or file operations may see a small cost, so measure in your own environment.
Not for most workloads. Custom profiles are worth the effort for internet-facing services, applications that process untrusted input, and systems that handle especially sensitive data.
The built-in standards check that seccomp is not unconfined at the baseline level and that it is set to the runtime default or a Localhost profile at the restricted level. They do not check the contents of your custom profile or AppArmor names, which is where policy engines come in.
No. A shared kernel with filters reduces risk but does not give a hard boundary. For hostile tenants, combine profiles with a sandboxed runtime that provides a separate kernel layer or user-space kernel.
Whenever the application, its dependencies, its base image, the container runtime or the node kernel changes in a meaningful way. Automating re-recording and testing in your pipeline makes this routine instead of heroic.
24Summary and Key Takeaways
Containers share one kernel, and that shared kernel is both their strength and their biggest risk. Seccomp narrows which requests a container may make to that kernel, and AppArmor narrows which files and resources it may touch. Kubernetes lets you apply both through simple settings, but the real craft lies in delivering profiles to every node, building them from observed behaviour, rolling them out gradually, and watching the results. Start with the runtime defaults everywhere, enforce them with policy, and add custom profiles only where the risk justifies the effort. Done this way, profiles stop being a scary source of outages and become a quiet, reliable layer of defence that keeps small mistakes from turning into big incidents.
Key Takeaways
- Containers share a kernel — filters on system calls and resource access reduce what an attacker can reach after breaking into an application.
- Seccomp and AppArmor are complementary — seccomp judges the type of kernel request, while AppArmor judges the file, network or resource being targeted.
- RuntimeDefault is the best first step — set it cluster-wide through the kubelet default or admission policy, and avoid unconfined except as a documented exception.
- Use modern, field-based settings — seccomp became a first-class field in 1.19 and AppArmor in 1.30, and old annotation-based manifests may be ignored or deprecated.
- Deliver profiles automatically — custom profiles must exist on every node; use image baking, a node-level helper or the Security Profiles Operator rather than manual copying.
- Learn, log, then enforce — record behaviour, review it, run in log-only mode on real traffic, and only then block, keeping a fast rollback ready.
- Profiles are one layer, not the whole wall — combine them with non-root users, dropped capabilities, patching and, for hostile code, a sandboxed runtime.