Docker AppArmor and seccomp: the minimum hardening that matters

Was this helpful?

You don’t need to “do security” like a compliance theater troupe. You need containers that behave when they’re compromised—because eventually one will be. The question isn’t whether your app has bugs. The question is whether a bug becomes a boring incident ticket or a weekend-long hostage situation with a kernel exploit and a very smug attacker.

AppArmor and seccomp are two of the few container-hardening controls that routinely change the blast radius in production. They’re not shiny, and they won’t impress your procurement team. They do, however, stop real techniques: strange syscalls, weird filesystem access, and “why does this API container need to mount filesystems?” moments.

What “minimum hardening” actually means

In container security, “minimum hardening” is not “turn on every feature and pray.” It’s a small set of controls that:

  • Apply broadly across heterogeneous apps.
  • Fail loudly (you can detect breakage) and fail safely (they don’t quietly remove protections).
  • Reduce kernel attack surface—because the kernel is the shared resource you cannot patch by redeploying a container.
  • Are operationally diagnosable under pressure at 02:00.

AppArmor and seccomp fit that bill. They’re not a substitute for running as non-root, dropping Linux capabilities, read-only filesystems, and proper secrets management. But if you asked me to pick two controls that change outcomes after a compromise, I’d pick these.

The minimum hardening that matters for most Docker estates looks like this:

  • Use Docker’s default seccomp profile (don’t disable it).
  • Use an enforcing AppArmor profile (don’t run unconfined).
  • Avoid –privileged and avoid blanket seccomp=unconfined.
  • When an app genuinely needs exceptions, make them explicit, documented, and tested.

There’s a reason these two controls are called out in post-mortems: they’re often the difference between “the attacker got a shell inside a container” and “the attacker got root on the host.”

Facts and history that matter (short, concrete)

Security controls make more sense when you know why they exist. A few anchor points:

  1. AppArmor shipped in the mid-2000s and became a mainstream default in Ubuntu long before containers were cool; it was built for confining daemons by pathname-based rules.
  2. SELinux vs AppArmor is largely a policy model and tooling difference; Docker integrated with both, but AppArmor is often operationally simpler in Debian/Ubuntu-heavy fleets.
  3. seccomp started as “secure computing mode” around Linux 2.6.12, initially very restrictive; seccomp-bpf later made it practical by allowing BPF-based syscall filters.
  4. Docker enabled seccomp by default years ago; many teams still disable it during “just get it working” phases and forget to re-enable it.
  5. Container isolation is not a VM boundary; containers share the host kernel. That’s why syscall filtering is disproportionately valuable.
  6. Namespacing and cgroups aren’t security policies; they’re isolation mechanisms. AppArmor and seccomp are policies that can say “no.”
  7. Most container escapes are kernel-oriented; if you can reduce the syscall surface and block weird kernel interfaces, you improve your odds.
  8. AppArmor is “path-based”, which is both its strength (readable) and a trap (rename/mount tricks if you’re sloppy about mounts and mediation).
  9. seccomp filters are per-process; if a process is started without the filter (or you set unconfined), you don’t get a second chance later.

Threat model: what these controls stop (and what they don’t)

What AppArmor is good at

AppArmor is Mandatory Access Control. It restricts what a process can do even if that process thinks it is root. In container land, that’s useful when a container breakout starts with “get root inside container” (which is often trivial) and continues with “touch host resources.” AppArmor can block file reads/writes, mounts, ptrace, signals, and interactions with specific kernel interfaces.

It’s also a good “policy tripwire.” When you see a denial, it usually correlates with behavior you should at least understand.

What seccomp is good at

seccomp filters syscalls: it can allow, deny, or trap system calls. This is not about stopping “rm -rf.” This is about stopping syscalls that are common in exploitation chains: namespace creation, keyrings, raw packet manipulation, perf events, mounting filesystems, and so on. Docker’s default seccomp profile blocks a bunch of high-risk syscalls that typical apps don’t need.

What they don’t do

  • They don’t fix your app vulnerabilities. They contain them.
  • They don’t replace least privilege. If you give the container host mounts or privileged mode, you can bypass a lot of the value.
  • They don’t stop data exfiltration over allowed channels (HTTP out, DNS out, etc.). That’s network policy and egress controls.
  • They don’t stop logic abuse like “export all customer data” if your API allows it.

Paraphrased idea (attributed): Werner Vogels is known for pushing the idea that “everything fails, all the time,” so you design for containment and recovery. That mindset applies here: assume compromise, design blast radius.

AppArmor in Docker: profiles, modes, and sane defaults

How Docker uses AppArmor

On AppArmor-enabled hosts, Docker can apply an AppArmor profile to each container. If you do nothing, you often get a default profile like docker-default (depending on distro and Docker packaging). If AppArmor is disabled, or Docker can’t load profiles, containers may run unconfined.

“Unconfined” doesn’t mean “fine.” It means you’ve removed one of the few guardrails that works even after the process is root.

Enforce vs complain

AppArmor has two operationally relevant modes:

  • enforce: deny violations. This is what you want in production.
  • complain: allow but log. This is how you gather data to write a profile without breaking workloads.

If you’re trying to roll out AppArmor, start with complain mode on a staging environment that gets real traffic. Then promote to enforce with a change window and a rollback plan.

What a “minimum useful” profile looks like

For Docker, minimum useful usually means: keep the Docker default, and only loosen where you have strong evidence. Custom profiles are a capability; they are also a maintenance obligation.

If you must write your own, keep it tight on mounts, raw devices, ptrace, and dangerous file paths. Be explicit about writable directories. Containers should not be able to write to arbitrary parts of the filesystem; if they can, you will eventually learn the hard way.

Operational signs AppArmor is not actually protecting you

  • docker info shows AppArmor disabled.
  • Containers show AppArmorProfile: "" or unconfined.
  • No denials ever appear anywhere, even during known-bad tests (this is suspicious, not comforting).

Joke #1: AppArmor in complain mode is like a security guard who only writes strongly worded diary entries. Great for research, not for stopping anyone.

seccomp in Docker: syscall filtering without self-harm

Docker’s default seccomp profile

Docker ships a default seccomp profile that blocks syscalls that are historically risky or rarely needed by typical container workloads. It’s not perfect and it’s not minimal; it’s practical. The default profile tends to break only specialized workloads (some tracing, some unusual runtimes, certain database tuning tools, nested container tricks) and even then you can usually solve it without going fully unconfined.

How seccomp failures show up

When seccomp blocks something, you typically see one of these:

  • The process gets EPERM or EACCES and logs an error like “operation not permitted.”
  • The process gets killed with SIGSYS (“bad system call”). This tends to happen when the filter is set to kill.
  • Audit logs show seccomp events if auditing is configured.

The debugging trick is to correlate app errors with kernel/audit logs and container configuration. If you start by randomly disabling seccomp, you’ll “fix” the symptom while creating a security regression that no one remembers six weeks later.

When you should customize seccomp

Customize seccomp only when you have a stable workload and a reason you can articulate in one sentence: “This container requires syscall X because feature Y uses it.” If you can’t explain it, you’re guessing. Guessing is how you end up with unconfined everywhere.

Joke #2: Running containers with seccomp=unconfined is like removing seatbelts because they wrinkle your shirt.

Practical tasks: commands, outputs, and decisions (12+)

These are the bread-and-butter tasks you run on real hosts. Each includes: a command, representative output, what it means, and what decision you make next.

Task 1 — Check if Docker thinks AppArmor and seccomp are enabled

cr0x@server:~$ docker info --format 'apparmor={{.SecurityOptions}}'
apparmor=[name=apparmor] seccomp=[name=seccomp,profile=default] cgroupns

What it means: Docker sees AppArmor and seccomp, and seccomp is using the default profile.

Decision: Good baseline. If AppArmor or seccomp is missing here, stop and fix host configuration before “hardening containers.”

Task 2 — Confirm AppArmor kernel support and status

cr0x@server:~$ sudo aa-status
apparmor module is loaded.
55 profiles are loaded.
52 profiles are in enforce mode.
3 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

What it means: The kernel module is loaded and most profiles are enforced.

Decision: If the module is not loaded or profiles are not enforced, fix that first. “Docker flags” won’t help if AppArmor isn’t active.

Task 3 — Check if any containers are unconfined (AppArmor)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} AppArmor={{.AppArmorProfile}}'
/api AppArmor=docker-default
/worker AppArmor=docker-default
/metrics AppArmor=unconfined

What it means: One container is running without AppArmor confinement.

Decision: Treat as a defect. Find who set --security-opt apparmor=unconfined (or why Docker couldn’t apply a profile) and remediate.

Task 4 — Check seccomp mode per container

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Seccomp={{.HostConfig.SecurityOpt}}'
/api Seccomp=[]
/worker Seccomp=[]
/metrics Seccomp=[seccomp=unconfined]

What it means: Two containers use defaults; one has seccomp explicitly unconfined.

Decision: Kill the exception unless you can justify it. If you must keep it, replace with a custom seccomp profile that adds only required syscalls.

Task 5 — Find how the container was started (to track the change source)

cr0x@server:~$ docker inspect metrics --format 'Image={{.Config.Image}} SecurityOpt={{.HostConfig.SecurityOpt}} Privileged={{.HostConfig.Privileged}}'
Image=corp/metrics-exporter:2.7.1 SecurityOpt=[seccomp=unconfined apparmor=unconfined] Privileged=false

What it means: Someone explicitly disabled both mechanisms.

Decision: Treat this like disabling TLS verification. File an issue against the deployment manifest, and require justification plus owner.

Task 6 — Validate container capabilities weren’t “helpfully” broadened

cr0x@server:~$ docker inspect api --format 'CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}}'
CapAdd=[] CapDrop=[ALL]

What it means: This container drops all capabilities (good). It may still have some defaults depending on how it’s run, but this is an explicit posture.

Decision: When you run tight on capabilities, AppArmor/seccomp become even more effective. Keep this pattern.

Task 7 — Detect privileged containers (because they punch holes in everything)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Privileged={{.HostConfig.Privileged}}'
/api Privileged=false
/worker Privileged=false
/buildkit Privileged=true

What it means: One container is privileged. That may be intentional (e.g., build tooling) but it’s high-risk.

Decision: If it’s a build system, isolate it: dedicated nodes, no production secrets, aggressive monitoring, and ideally use rootless or a safer build approach.

Task 8 — Check kernel logs for AppArmor denials

cr0x@server:~$ sudo dmesg -T | grep -i apparmor | tail -n 5
[Sat Jan  3 10:11:14 2026] audit: type=1400 audit(1704276674.123:781): apparmor="DENIED" operation="open" profile="docker-default" name="/proc/kcore" pid=29144 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

What it means: A process inside a docker-default confined container tried to read /proc/kcore and was denied.

Decision: This is usually good news: it indicates confinement is active. Investigate if the access was expected (rare) or suspicious (often).

Task 9 — Check audit logs for seccomp violations

cr0x@server:~$ sudo ausearch -m SECCOMP -ts recent | tail -n 3
time->Sat Jan  3 10:12:01 2026
type=SECCOMP msg=audit(1704276721.443:812): auid=4294967295 uid=0 gid=0 ses=4294967295 subj==unconfined pid=29210 comm="node" exe="/usr/local/bin/node" sig=31 arch=c000003e syscall=319 compat=0 ip=0x7f2c8e2f1a9d code=0x0

What it means: A process triggered a seccomp event on syscall 319 (example). You’ll map syscalls to names if needed, but the event is the clue.

Decision: Correlate with container and deployment. If it’s your app crashing, you need a targeted seccomp adjustment, not a blanket disable.

Task 10 — Check what seccomp profile Docker is using by default (daemon level)

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "journald",
  "live-restore": true
}

What it means: No override; Docker will use its built-in default seccomp unless a container requests otherwise.

Decision: Good. If you see "seccomp-profile": "unconfined" or similar patterns (depending on tooling), treat it as an emergency rollback of security posture.

Task 11 — Run a canary container to verify enforcement isn’t silently broken

cr0x@server:~$ docker run --rm --name aa-canary alpine:3.19 sh -c 'cat /proc/kcore; echo ok'
cat: can't open '/proc/kcore': Permission denied
ok

What it means: The denial is expected under typical Docker/AppArmor defaults; the container continues running.

Decision: Keep this as a quick test during node bootstrap or after kernel upgrades. If it suddenly succeeds, you likely lost AppArmor confinement.

Task 12 — Prove to yourself that seccomp is active with a syscall that’s usually blocked

cr0x@server:~$ docker run --rm alpine:3.19 sh -c 'apk add --no-cache strace >/dev/null; strace -e trace=bpf true'
strace: syscall bpf denied

What it means: The bpf syscall is commonly blocked by the default seccomp profile; strace reports denial.

Decision: If this succeeds unexpectedly, someone disabled seccomp or changed the profile. If your legitimate app needs bpf, you’re in niche territory—document it and isolate it.

Task 13 — Identify the AppArmor profile applied to a running container process

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' api); sudo cat /proc/$pid/attr/current
docker-default (enforce)

What it means: The kernel shows the process is confined under docker-default in enforce mode.

Decision: This is a strong verification step when Docker metadata is misleading or you suspect runtime differences.

Task 14 — Detect containers with risky mounts that undermine policy

cr0x@server:~$ docker inspect api --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/api/config","Destination":"/app/config","Mode":"ro","RW":false,"Propagation":"rprivate"}]

What it means: A read-only bind mount is used for config. That’s sane.

Decision: If you see binds to /, /var/run/docker.sock, /proc, or writable host paths with broad scope, fix those first; no amount of profile tuning will make that safe.

Task 15 — Spot the “docker.sock inside container” foot-gun

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} {{range .Mounts}}{{.Destination}} {{end}}' | grep -F '/var/run/docker.sock'
/ci-runner /var/run/docker.sock

What it means: A container has access to the Docker socket, which is effectively root on the host in most configurations.

Decision: Treat as a privileged workload. Isolate it, restrict who can deploy it, and avoid mixing it with production data paths.

Fast diagnosis playbook

When something breaks after enabling AppArmor/seccomp, your goal is to find the bottleneck in minutes, not hours. Here’s the order that saves time.

First: confirm it’s actually AppArmor/seccomp (not networking, not DNS, not permissions)

  • Look for “Operation not permitted” or “Bad system call” in container logs.
  • Check the container security opts and profiles (docker inspect).
  • Check kernel/audit logs for denials (dmesg, ausearch).

Second: identify what action is blocked

  • AppArmor denials usually mention an operation and a path (e.g., operation="open" name="/proc/kcore").
  • seccomp events mention a syscall number (and sometimes name depending on tooling) and often SIGSYS.

Third: decide the right fix category

  • Fix the app if it’s doing something unnecessary (common).
  • Fix the container config (mounts, capabilities, user) if the app behavior is normal but the container is over-tightened in the wrong place.
  • Fix the profile only if it’s a stable, justified need.
  • Isolate the workload if it needs dangerous powers (build systems, eBPF tooling, nested runtimes).

Fourth: test with the smallest exception possible

Never jump from default to unconfined. For seccomp, allow one syscall. For AppArmor, allow one path or capability. Then redeploy and verify the denial disappears and nothing else regresses.

Three corporate mini-stories from the trenches

1) Incident caused by a wrong assumption: “It’s inside a container, so it can’t touch the host”

The company ran a set of “internal-only” services in Docker on a few beefy Linux hosts. One of those services was a PDF conversion API. You can already guess the rest: user-supplied documents, a parser with a history, and a threat model that had been described as “not internet-facing.” It was, in fact, reachable from multiple places because internal networks are just the internet with nicer coffee.

The team’s assumption was that containerization itself was enough. AppArmor was installed on the distro but not actually enforced for containers; seccomp had been disabled months earlier because a different service had tripped over it during a rush migration. No one wrote down the reason. The disable became the default.

An attacker used a parsing bug to get code execution inside the container. From there they enumerated mounts and found a writable bind mount into a host directory used for shared uploads. That directory contained a stray SSH key left behind by an old automation script. The key belonged to an account that, while not root, was in the group that could talk to the Docker socket on some machines.

From “RCE in a container,” they moved to “control the Docker daemon,” and from there it was game over: start a privileged container, mount the host filesystem, drop a backdoor. The post-incident analysis kept circling one line: “We assumed containers were a boundary.”

Afterward, the fix wasn’t heroic. It was dull: re-enable default seccomp, enforce AppArmor, remove docker.sock mounts, and push a policy that any exception requires an owner and an expiration date. The next attempted exploit still got a shell in a container, but it couldn’t do the second hop. That’s the win.

2) Optimization that backfired: “Our custom seccomp profile is faster”

A performance-focused group decided Docker’s default seccomp profile was “too big” and therefore “must be slow.” They wrote a custom profile intended to allow only the syscalls their Go services used. The initial tests looked fine: latency benchmarks were slightly better on a synthetic workload, and nobody saw errors in staging.

Then production happened. The first symptom wasn’t security related; it was availability. A subset of containers started crashing during periods of higher load, but only on nodes with a newer kernel. The crash signature was a mix of “bad system call” and cryptic runtime errors. The on-call engineer did what on-call engineers do under stress: they disabled seccomp for the crashing services to restore uptime. Incidents stopped. The “fix” stuck.

The root cause was boring: the Go runtime and libc made syscalls that didn’t show up in their narrow test, including calls used for thread management and newer kernel interfaces. Their profile had been “optimized” against a snapshot of behavior, not the real behavior across kernel versions, libc changes, and operational modes like DNS and TLS variations.

The long-term lesson was not “never customize.” It was “don’t customize unless you can maintain it.” They reverted to Docker’s default seccomp profile for general workloads and kept a carefully maintained custom profile for one specialized service, owned by the team that needed it and tested in CI against multiple kernels. The slight benchmark gain was not worth the operational fragility.

3) Boring but correct practice that saved the day: canary tests and enforced defaults

Another organization ran dozens of Kubernetes clusters but also had a small fleet of Docker-only hosts for legacy batch jobs. Those hosts were frequently updated—kernel patches, Docker updates, the usual churn. The SRE team had a simple habit: every node bootstrap ran a canary container that tried a handful of “should be denied” actions and confirmed the expected denials appeared in logs.

One week, after a seemingly routine OS upgrade, the canary started passing actions that should have been blocked. No alarms had fired yet, because production jobs were still running. But the canary check failed the node admission step, so new workloads didn’t schedule there. The host was quarantined automatically.

Turns out AppArmor had been disabled at boot due to a kernel parameter mismatch during the upgrade. Docker still ran, containers still ran, and everyone would have assumed “it’s fine.” But “fine” was only true until the first compromise. The canary caught a quiet regression that would have lasted months.

The team fixed the boot configuration, re-enabled AppArmor, and unquarantined the node. No incident report, no customer impact. Just one of those invisible wins that only exist because someone insisted on a dull control loop and refused to negotiate with “it probably works.”

Common mistakes: symptoms → root cause → fix

1) Symptom: container exits immediately with “Bad system call”

Root cause: seccomp blocked a syscall and the action is configured to kill or the process doesn’t handle EPERM gracefully.

Fix: Confirm seccomp is active and inspect audit logs (ausearch -m SECCOMP). Add the specific syscall to a custom seccomp profile for that workload, or update the app/runtime. Don’t set seccomp=unconfined as a “temporary” fix unless you also schedule its removal.

2) Symptom: app can’t read a file it used to read (but file permissions look correct)

Root cause: AppArmor denied a path-based access even though Unix permissions allow it.

Fix: Check dmesg for AppArmor denials, identify the profile, then either adjust the profile or move the file into an allowed path (often better). Verify mounts: a different path inside the container can cause a policy mismatch.

3) Symptom: “mount: permission denied” inside container

Root cause: AppArmor and/or seccomp blocked mount-related operations; also likely missing capabilities like CAP_SYS_ADMIN.

Fix: Ask why the container is mounting anything. Most should not. For legitimate cases (rare), isolate the workload and explicitly grant what it needs with tight scoping.

4) Symptom: debugging tools don’t work (strace, perf, ptrace)

Root cause: This is often the point. AppArmor and seccomp commonly restrict tracing interfaces because they’re powerful in exploits.

Fix: Use dedicated debug images in isolated environments, or run debug tooling on the host with proper access. If you must enable tracing in production, do it for a short-lived debug pod/container with audited access and a clear teardown plan.

5) Symptom: “works on one node, fails on another” after hardening

Root cause: Kernel versions differ, Docker versions differ, or AppArmor profile sets differ. Custom seccomp profiles are especially sensitive to runtime variance.

Fix: Standardize node images, pin and test profiles, and add canary checks. Treat host drift as a security and reliability issue, not just a hygiene issue.

6) Symptom: everything runs, but you never see a single denial anywhere

Root cause: Logging/auditing isn’t enabled or AppArmor/seccomp isn’t actually applied (containers unconfined).

Fix: Verify via /proc/<pid>/attr/current, docker info, and canary tests. Silence is not proof of safety.

7) Symptom: someone “fixes” an incident by adding –privileged

Root cause: Lack of operational discipline and missing escalation path for profile tuning. Privileged mode becomes the escape hatch.

Fix: Make privileged deployments require explicit approval, isolate privileged workloads, and build a fast path for targeted profile changes with tests.

Checklists / step-by-step plan

Step-by-step: minimum hardening rollout that won’t ruin your week

  1. Inventory current posture. Identify containers with apparmor=unconfined, seccomp=unconfined, privileged mode, and docker.sock mounts.
  2. Fix the worst first. Remove docker.sock mounts from non-build workloads. Eliminate privileged containers in production data planes.
  3. Enable host controls. Ensure AppArmor is loaded and enforcing. Ensure Docker reports seccomp default enabled.
  4. Canary test on every node. Use quick denial checks (like /proc/kcore and a blocked syscall) during bootstrap and after upgrades.
  5. Roll out seccomp defaults broadly. Default seccomp should be on everywhere unless there is a documented exception.
  6. Roll out AppArmor enforcement. Ensure containers are not unconfined. Prefer docker-default unless you have a strong reason.
  7. Handle exceptions with ownership. Every unconfined or custom profile needs an owner, a reason, and a periodic review.
  8. Automate verification. CI checks for deployment manifests: fail if they set unconfined without an allowlist.
  9. Operationalize debugging. Teach on-call where to find denials and seccomp events; write a runbook that starts with logs, not guesswork.

Checklist: container launch options worth caring about

  • Do not use --privileged unless the node is dedicated and the workload is explicitly high-trust.
  • Do not use --security-opt seccomp=unconfined except as a temporary mitigation with a ticket and expiration.
  • Do not use --security-opt apparmor=unconfined in production. Fix the profile instead.
  • Prefer --read-only with explicit writable mounts for state.
  • Drop capabilities by default; add back only what you can explain.
  • Avoid host PID/network namespaces unless you’re building node agents and you understand the implications.

Checklist: what to capture in an exception request

  • Exact container image and version.
  • Exact denial logs (AppArmor) or syscall numbers/events (seccomp).
  • Business justification for the behavior.
  • Proposed smallest exception (one path, one syscall, one capability).
  • Isolation plan (dedicated nodes, reduced secrets, restricted network).
  • Test plan and rollback plan.

FAQ

1) Do I need both AppArmor and seccomp? Isn’t one enough?

Use both. They cover different layers: AppArmor is about access control (files, capabilities, operations), seccomp is about syscall surface. Overlap is fine; redundancy is the point.

2) If my container runs as non-root, do I still need these?

Yes. Non-root helps a lot, but kernel exploitation and container escapes don’t require your process to start as root. Defense-in-depth matters because attackers chain weaknesses.

3) Why not just run everything in privileged mode and rely on network controls?

Because network controls don’t stop local privilege escalation, kernel bugs, or accidental host damage. Privileged containers are basically “processes with a marketing department.”

4) What’s the safest default seccomp posture in Docker?

Use Docker’s default seccomp profile and only deviate for specific workloads with documented needs. The default is a pragmatic balance across many workloads.

5) How do I tell whether a container is actually confined (not just “configured”)?

Check the process attribute: /proc/<pid>/attr/current for AppArmor. For seccomp, look for SECCOMP audit events during a known blocked action, or inspect the runtime config and test with a canary.

6) My app needs ptrace/strace in production. What now?

First question: does it really? Usually you need better observability, not ptrace. If you truly need it, isolate the workload, scope it tightly, and accept the increased risk. Don’t casually widen security for the whole fleet.

7) Will enabling these controls hurt performance?

In most real workloads, the overhead is negligible compared to network, disk, and application logic. The operational cost comes from exceptions and debugging, not CPU cycles.

8) Why do denials show up only after a library update?

Because runtimes evolve. Newer libc, JVMs, Go versions, or node versions may use different syscalls or access different kernel interfaces. That’s why custom “minimal” seccomp profiles require maintenance and testing across kernels.

9) Is AppArmor enough if I’m on Ubuntu, and SELinux if I’m on RHEL?

Use what your platform supports well. AppArmor is common on Ubuntu/Debian; SELinux is common on RHEL-derived systems. The key is enforcement and operational maturity, not ideology.

10) What’s the quickest win if I can only fix one thing this week?

Stop running unconfined and stop using privileged mode. Enforce default seccomp everywhere. Those changes close a lot of “easy escape” doors.

Next steps that actually reduce risk

If you want minimum hardening that matters, don’t start by writing a 500-line policy. Start by making sure the defaults are real and enforced:

  • Verify AppArmor is loaded and enforcing on every node.
  • Verify Docker is using default seccomp and containers aren’t unconfined.
  • Remove privileged containers and docker.sock mounts from production data-plane workloads.
  • Add a simple canary test to your node bootstrap that proves denials happen.
  • Operationalize diagnosis: teach on-call to read denials and seccomp events before they reach for unconfined.

Do those five things and you’ll convert “container security” from a slide deck into a control loop. That’s the kind of boring that keeps systems—and weekends—intact.

← Previous
ZFS Special Small Blocks: How to Turbocharge Small-File Workloads
Next →
ZFS Capacity Planning: Designing for Growth Without Rebuilding

Leave a comment