Tech Expert & Vibe Coder

With 14+ years of experience, I specialize in self-hosting, AI automation, and Vibe Coding – building applications using AI-powered tools like Google Antigravity, Dyad, and Cline. From homelabs to enterprise solutions.

Configuring Docker Compose with Seccomp and AppArmor Profiles to Block Netlink Socket Access for Rootkit Prevention

Why I Worked on This

I run several containerized services on my Proxmox host, and I've always been uneasy about the default security posture of Docker containers. When I learned about netlink sockets being a common vector for container escapes and rootkit activity, I wanted to lock down my setup further. The problem: Docker's default seccomp and AppArmor profiles are permissive by design, and most guides either skip these entirely or assume you're running Kubernetes with policy engines I don't use.

I needed a way to block specific system calls—especially those tied to netlink sockets—without breaking my services. This meant learning how seccomp and AppArmor actually work in Docker Compose, not just copying profiles from the internet.

My Real Setup

I tested this on:

  • Proxmox LXC container running Ubuntu 22.04
  • Docker 24.0.7 with Docker Compose v2
  • A simple nginx container as the test case
  • AppArmor already enabled on the host (checked with aa-status)

I built a basic Dockerfile with nginx, then used docker-slim to generate initial seccomp and AppArmor profiles. These profiles became my starting point, not my final config.

What Worked (and Why)

Generating Base Profiles

I used docker-slim to observe which syscalls my nginx container actually needed:

docker-slim build --include-path /usr/share/nginx/html --expose 80 --http-probe-cmd / nginx-test

This created a seccomp profile in /tmp/docker-slim-state/.docker-slim-state/images/[image-id]/artifacts. The profile was massive and overly permissive, but it showed me what nginx legitimately uses during startup and runtime.

Blocking Netlink Sockets

Netlink sockets use the socket syscall with specific arguments. I modified the generated seccomp profile to explicitly deny socket calls with AF_NETLINK (value 16):

{
  "names": ["socket"],
  "action": "SCMP_ACT_ERRNO",
  "args": [
    {
      "index": 0,
      "value": 16,
      "op": "SCMP_CMP_EQ"
    }
  ]
}

This blocks netlink socket creation without breaking normal TCP/UDP operations nginx needs. I tested by running the container with the modified profile:

docker run -d -p 8080:80 \
  --security-opt seccomp=./custom-seccomp.json \
  --name nginx-test nginx-test

Nginx started normally, served pages, but any attempt to create netlink sockets failed. I verified this by running a test binary inside the container that tried to open a netlink socket—it failed with permission denied.

Adding AppArmor

Seccomp blocks syscalls, but AppArmor controls file access and capabilities. I created a custom AppArmor profile based on Docker's default template but removed network-related capabilities:

profile nginx-restricted flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  
  network inet tcp,
  network inet udp,
  
  deny network netlink raw,
  deny network packet raw,
  
  capability setuid,
  capability setgid,
  capability chown,
  deny capability sys_admin,
  deny capability sys_module,
  
  /usr/sbin/nginx ix,
  /etc/nginx/** r,
  /var/log/nginx/* w,
  /usr/share/nginx/html/** r,
}

I loaded this profile on the host:

sudo apparmor_parser -r -W ./nginx-restricted

Then ran the container with both protections:

docker run -d -p 8080:80 \
  --security-opt seccomp=./custom-seccomp.json \
  --security-opt apparmor=nginx-restricted \
  --name nginx-test nginx-test

This worked. Nginx functioned normally, but attempts to access kernel interfaces or load modules were blocked at two layers.

Using Docker Compose

I translated this into a compose file for my actual services:

version: '3.8'
services:
  nginx:
    image: nginx-test
    ports:
      - "8080:80"
    security_opt:
      - seccomp=./custom-seccomp.json
      - apparmor=nginx-restricted
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
      - SETUID
      - SETGID
      - NET_BIND_SERVICE

The cap_drop/cap_add lines further reduce what the container can do even if seccomp or AppArmor fail.

What Didn't Work

Audit Mode Noise

I tried running seccomp in audit mode first (with SCMP_ACT_LOG) to see what was being blocked. The logs were overwhelming and full of harmless noise from Docker's internal operations. I had to manually trace nginx's actual behavior instead of relying on audit logs.

Signal Handling

With the AppArmor profile active, docker stop sometimes hung because the container couldn't handle SIGTERM properly. I had to either remove the profile first or use docker kill. I never fully resolved this—it's a known limitation when AppArmor restricts signal-related syscalls too aggressively.

Profile Conflicts

If I forgot to reload the AppArmor profile after editing it, Docker would silently fall back to the old version. This caused confusion when testing changes. I learned to always run sudo apparmor_parser -r -W and verify with aa-status before restarting containers.

Over-Blocking

My first attempt blocked too many syscalls and broke nginx's worker process initialization. I had to add back fstatfs, getdents64, and newfstatat after tracing the startup sequence with strace.

Key Takeaways

  • Seccomp and AppArmor are complementary, not redundant. Use both.
  • Generated profiles are starting points, not solutions. You must test and refine them.
  • Blocking netlink sockets is straightforward once you understand seccomp's argument filtering.
  • Docker Compose supports security options cleanly—no need for custom scripts.
  • Always test profile changes in a non-production environment first. Breaking a container's startup is easy.
  • AppArmor profiles must be reloaded manually on the host. Docker won't warn you if they're stale.

This setup now runs on my Proxmox host for services I consider high-risk. It's not perfect—no security measure is—but it closes a specific attack vector I was concerned about without adding operational complexity.