Docker Security Hardening: Rootless Mode, Seccomp, AppArmor op een VPS
Zeven beveiligingslagen voor Docker op een VPS. Elke sectie legt de dreiging uit, toont de oplossing met CLI- en Compose-syntax en verifieert of het werkt.
De standaardconfiguratie van Docker ruilt beveiliging in voor gemak. Containers draaien als root op de host. Alle 14 Linux capabilities blijven actief. Seccomp blokkeert slechts 44 van de meer dan 300 syscalls. Inter-container verkeer stroomt vrij.
Op een VPS is dit belangrijker dan op een lokale ontwikkelmachine. Je deelt een fysieke host met andere huurders. Een container escape betekent dat een aanvaller als root op de hypervisor-facing kernel terechtkomt. Elke beveiligingslaag die je toevoegt, verkleint de impact van een inbraak.
Deze tutorial behandelt zeven beveiligingsmaatregelen. Elke sectie legt de dreiging uit die het voorkomt, toont de implementatie (zowel docker run flags als Compose-syntax) en bevat een verificatiestap. We hebben elk commando getest op Ubuntu 24.04 met Docker Engine 29.x.
Vereisten: Een VPS met Debian 12 of Ubuntu 24.04 en Docker Engine geïnstalleerd. SSH-toegang als een non-root sudo-gebruiker. Als je de host zelf nog niet hebt beveiligd, begin dan met onze Linux VPS Beveiliging: Bedreigingen, Lagen en Hardening-gids. Voor Docker firewall-problemen, zie Docker omzeilt UFW: 4 geteste oplossingen voor je VPS.
Dit artikel maakt deel uit van de Docker in productie op een VPS: wat er misgaat en hoe je het oplost serie.
Hoe stel ik rootless Docker in op Ubuntu 24.04 of Debian 12?
Rootless Docker draait de daemon en alle containers onder een gewoon gebruikersaccount in plaats van root. Als een aanvaller uit een container ontsnapt, komt hij terecht als een onbevoegde gebruiker op de host. Geen root-toegang. Van alle maatregelen in deze handleiding heeft deze de grootste impact.
Rootless Docker installeren
Installeer de benodigde pakketten. Het uidmap-pakket biedt newuidmap en newgidmap, die subordinate UID/GID mapping afhandelen:
sudo apt-get update && sudo apt-get install -y uidmap docker-ce-rootless-extras
Controleer of je gebruiker ten minste 65.536 subordinate UIDs en GIDs heeft:
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid
Je zou output moeten zien als deploy:100000:65536. Als de entries ontbreken, voeg ze toe:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
Stop de systeembrede Docker daemon. Die heb je niet nodig voor rootless mode:
sudo systemctl disable --now docker.service docker.socket
Voer de rootless setup tool uit als je gewone gebruiker (niet root):
dockerd-rootless-setuptool.sh install
Het script toont omgevingsvariabelen die je moet instellen. Voeg ze toe aan je shell-profiel:
echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc
Schakel linger in zodat de rootless daemon bij het opstarten start, niet alleen wanneer je inlogt:
sudo loginctl enable-linger $(whoami)
Rootless mode verifiëren
docker context use rootless
docker run --rm hello-world
Controleer of het daemon-proces draait als jouw gebruiker, niet root:
ps aux | grep dockerd
De output moet je gebruikersnaam tonen, niet root. Bevestig ook dat Docker info rootless rapporteert:
docker info --format '{{.SecurityOptions}}'
Je zou rootless in de lijst moeten zien.
Wanneer rootless Docker problemen veroorzaakt
Rootless mode heeft echte beperkingen. Kennis ervan voorkomt uren debuggen.
| Beperking | Oorzaak | Workaround |
|---|---|---|
| Kan niet binden aan poorten onder 1024 | Niet-root gebruikers kunnen geen privileged ports binden | Stel sysctl net.ipv4.ip_unprivileged_port_start=0 in of gebruik een reverse proxy op de host |
| Bind mount permission errors | Hostbestanden die eigendom zijn van root zijn ontoegankelijk voor de geremapte UID | Verander eigenaarschap naar jouw gebruiker, of gebruik named volumes |
| Trager overlay-bestandssysteem | Rootless gebruikt fuse-overlayfs in plaats van native overlay2 |
Accepteer de overhead (5-15% voor I/O-intensieve workloads) of gebruik native overlay2 met --privileged (tenietdoet het doel) |
Geen --net=host |
Rootless networking gebruikt slirp4netns of pasta, niet de host network stack | Gebruik port mapping (-p) in plaats daarvan. Installeer voor betere prestaties pasta als network driver |
ping faalt in containers |
CAP_NET_RAW is beperkt |
Installeer slirp4netns >= 0.4.0 of gebruik pasta |
Opmerking over netwerkprestaties: Standaard gebruikt rootless Docker slirp4netns voor networking, wat NAT-overhead toevoegt. De pasta-driver kopieert de host-netwerkconfiguratie naar de container namespace zonder NAT en biedt betere doorvoer. Op Debian 12 en Ubuntu 24.04 installeer je het met:
sudo apt-get install -y passt
Docker pikt pasta automatisch op als het geïnstalleerd is.
Wanneer moet ik user namespace remapping gebruiken in plaats van rootless Docker?
User namespace remapping (userns-remap) mapt UID 0 in containers naar een onbevoegde UID op de host. In tegenstelling tot rootless Docker draait de daemon zelf nog steeds als root. Dit betekent dat je volledige Docker-functionaliteit behoudt (privileged ports, host networking, native overlay2) en toch voorkomt dat container-root gelijk is aan host-root.
Kies userns-remap wanneer rootless mode je workload breekt maar je nog steeds UID-isolatie wilt. Kies rootless als je met de beperkingen kunt leven.
| Feature | Rootless Docker | userns-remap | Standaard Docker |
|---|---|---|---|
| Daemon draait als | Gebruiker | Root | Root |
| Container root = host root | Nee | Nee | Ja |
| Privileged ports | Workaround nodig | Werkt | Werkt |
--net=host |
Nee | Ja | Ja |
| Storage driver | fuse-overlayfs | overlay2 | overlay2 |
| Installatiecomplexiteit | Gemiddeld | Laag | Geen |
userns-remap configureren
Maak de dockremap-gebruiker aan of gebruik de default-snelkoppeling die hem automatisch aanmaakt:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"userns-remap": "default"
}
EOF
sudo systemctl restart docker
Docker maakt de dockremap-gebruiker aan en voegt subordinate UID/GID-ranges toe aan /etc/subuid en /etc/subgid.
Controleer of het werkt:
sudo ls -ld /var/lib/docker/
Je zou een nieuwe subdirectory moeten zien, genoemd naar het geremapte UID-bereik, zoals /var/lib/docker/100000.100000/. Het exacte nummer hangt af van het subordinate UID-bereik dat is toegewezen aan de dockremap-gebruiker in /etc/subuid.
Start een container en controleer de process UID op de host:
docker run -d --name test-userns nginx:alpine
ps aux | grep nginx
Het nginx-proces zou een hoge UID moeten tonen (overeenkomend met het eerste nummer uit /etc/subuid voor dockremap), niet 0.
Opruimen:
docker rm -f test-userns
Volume-eigenaarschap gotcha: Bind-mounted bestanden die eigendom zijn van host root (UID 0) verschijnen als nobody in de container omdat UID 0 naar een ander bereik mapt. Gebruik named volumes of chown bestanden naar de geremapte UID.
Hoe verwijder ik Linux capabilities van een Docker-container?
Docker kent standaard 14 Linux capabilities toe aan containers. Elke capability is een kernel-permissie die een aanvaller kan misbruiken na een container escape of binnen een gecompromitteerde container. Alle capabilities verwijderen en alleen teruggeven wat je applicatie nodig heeft, verkleint het aanvalsoppervlak.
Standaard Docker capabilities
| Capability | Wat het toestaat | Houden of verwijderen? |
|---|---|---|
CAP_CHOWN |
Bestandseigenaarschap wijzigen | Verwijderen tenzij nodig |
CAP_DAC_OVERRIDE |
Lees/schrijf-permissiecontroles omzeilen | Verwijderen tenzij nodig |
CAP_FOWNER |
Permissiecontroles op bestandseigenaar omzeilen | Verwijderen tenzij nodig |
CAP_FSETID |
setuid/setgid-bits instellen | Verwijderen |
CAP_KILL |
Signalen sturen naar elk proces | Verwijderen tenzij nodig |
CAP_SETGID |
Proces-GID wijzigen | Houden voor de meeste apps |
CAP_SETUID |
Proces-UID wijzigen | Houden voor de meeste apps |
CAP_SETPCAP |
Procescapabilities wijzigen | Verwijderen |
CAP_NET_BIND_SERVICE |
Binden aan poorten onder 1024 | Houden als je poort 80/443 bindt |
CAP_NET_RAW |
Raw sockets gebruiken (pakketten maken) | Verwijderen tenzij je ping/traceroute nodig hebt |
CAP_SYS_CHROOT |
chroot gebruiken | Verwijderen |
CAP_MKNOD |
Device files aanmaken | Verwijderen |
CAP_AUDIT_WRITE |
Schrijven naar kernel audit log | Verwijderen tenzij nodig |
CAP_SETFCAP |
Bestandscapabilities instellen | Verwijderen |
Alles verwijderen, terugzetten wat je nodig hebt
CLI-syntax:
docker run -d \
--cap-drop ALL \
--cap-add CHOWN \
--cap-add NET_BIND_SERVICE \
--cap-add SETUID \
--cap-add SETGID \
--name hardened-nginx \
nginx:alpine
Nginx heeft CHOWN nodig omdat het entrypoint-script het eigenaarschap van cachedirectory's wijzigt bij het opstarten. Zonder dit stopt de container direct met een chown: Operation not permitted fout.
Compose-syntax:
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
Verifiëren dat capabilities zijn verwijderd
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep Cap'
Vergelijk het CapEff (effective capabilities) bitmask. Met alles verwijderd en vier teruggevoegd is de waarde veel lager dan de standaard 00000000a80425fb.
Voor leesbare output, installeer capsh op de host en decodeer de hex:
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep CapEff' | awk '{print $2}' | xargs -I{} capsh --decode=0x{}
Je zou alleen cap_chown,cap_setgid,cap_setuid,cap_net_bind_service in de output moeten zien.
Opruimen:
docker rm -f hardened-nginx
Wat voorkomt de no-new-privileges flag?
De no-new-privileges flag blokkeert processen in een container om extra privileges te verkrijgen via setuid- of setgid-binaries. Zonder deze flag kan een gecompromitteerd proces een setuid-binary uitvoeren (zoals su of sudo) en escaleren naar root. Met de flag ingeschakeld weigert de kernel de privilege-escalatie.
Per container toepassen
CLI:
docker run -d --security-opt no-new-privileges:true --name no-priv-test nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- no-new-privileges:true
Als daemon-standaard toepassen
Voeg het toe aan daemon.json zodat elke container deze flag automatisch krijgt:
{
"no-new-privileges": true
}
Herstart Docker na het bewerken:
sudo systemctl restart docker
Verifiëren dat het werkt
docker exec no-priv-test grep NoNewPrivs /proc/1/status
Verwachte output:
NoNewPrivs: 1
Een waarde van 1 betekent dat er geen nieuwe privileges verkregen kunnen worden. Een waarde van 0 betekent dat de flag niet is ingesteld.
Opruimen:
docker rm -f no-priv-test
Hoe maak ik een aangepast seccomp-profiel voor Docker?
Het standaard seccomp-profiel van Docker blokkeert ongeveer 44 syscalls van de meer dan 300. Met een aangepast profiel kun je containers beperken tot alleen de syscalls die je applicatie daadwerkelijk gebruikt. Als een aanvaller de container compromitteert, kan hij geen kernelkwetsbaarheden exploiteren via geblokkeerde syscalls.
Ontdek welke syscalls je applicatie nodig heeft
Gebruik strace op een draaiende container om de syscalls vast te leggen die het maakt tijdens normaal gebruik:
docker run -d --security-opt seccomp=unconfined --name trace-target nginx:alpine
# Install strace on the host
sudo apt-get install -y strace
# Get the container's PID
PID=$(docker inspect --format '{{.State.Pid}}' trace-target)
# Trace syscalls for 30 seconds during normal operation
sudo strace -f -p "$PID" -o /tmp/nginx-syscalls.log -e trace=all &
STRACE_PID=$!
sleep 30
# Send some test traffic to exercise the application
curl -s http://localhost:80 > /dev/null 2>&1 || true
kill "$STRACE_PID" 2>/dev/null
wait "$STRACE_PID" 2>/dev/null
Extraheer de unieke syscall-namen:
grep -oP '^\[pid \d+\] \K\w+|^\w+' /tmp/nginx-syscalls.log | sort -u
Dit geeft je de minimale set syscalls die je applicatie nodig heeft.
Het aangepaste profiel bouwen
Maak een JSON-bestand. De defaultAction is SCMP_ACT_ERRNO (alles weigeren wat niet expliciet is toegestaan). De syscall-lijst moet zowel bevatten wat je applicatie nodig heeft ALS wat de container runtime (runc) nodig heeft tijdens initialisatie. Het onderstaande profiel is getest met nginx:alpine op Docker Engine 29.x:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4",
"access",
"arch_prctl",
"bind",
"brk",
"capget",
"capset",
"chdir",
"chown",
"clone",
"clone3",
"close",
"close_range",
"connect",
"copy_file_range",
"dup",
"dup2",
"dup3",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"epoll_pwait2",
"epoll_wait",
"eventfd2",
"execve",
"exit",
"exit_group",
"faccessat",
"faccessat2",
"fchmod",
"fchmodat",
"fchown",
"fchownat",
"fcntl",
"fork",
"fstat",
"fstatfs",
"futex",
"getcwd",
"getdents",
"getdents64",
"getegid",
"geteuid",
"getgid",
"getpgrp",
"getpid",
"getppid",
"getrandom",
"getrlimit",
"getsockname",
"getsockopt",
"gettid",
"getuid",
"io_destroy",
"io_getevents",
"io_setup",
"io_submit",
"ioctl",
"kill",
"listen",
"lseek",
"lstat",
"madvise",
"memfd_create",
"mkdir",
"mkdirat",
"mmap",
"mount",
"mprotect",
"mremap",
"munmap",
"nanosleep",
"newfstatat",
"open",
"openat",
"pipe",
"pipe2",
"pivot_root",
"poll",
"ppoll",
"prctl",
"pread64",
"prlimit64",
"pwrite64",
"read",
"readlink",
"readlinkat",
"recvfrom",
"recvmsg",
"rename",
"renameat",
"rseq",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_getaffinity",
"sched_yield",
"seccomp",
"sendfile",
"sendmsg",
"sendto",
"set_robust_list",
"set_tid_address",
"setgid",
"setgroups",
"sethostname",
"setitimer",
"setsockopt",
"setuid",
"sigaltstack",
"socket",
"socketpair",
"stat",
"statfs",
"statx",
"symlink",
"symlinkat",
"sysinfo",
"tgkill",
"umask",
"umount2",
"uname",
"unlink",
"unlinkat",
"unshare",
"utimensat",
"wait4",
"write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
Waarom zoveel syscalls? De lijst bevat syscalls die nodig zijn voor drie lagen: de container runtime (runc) voor namespace-setup (clone3, mount, pivot_root, unshare, seccomp, statx), de Alpine shell en entrypoint-scripts (fork, open, pipe, wait4), en nginx zelf (accept4, bind, listen, io_setup). Een glibc-gebaseerde image zou minder shell-gerelateerde syscalls nodig hebben maar meer libc-interne.
Sla dit op als /etc/docker/seccomp-nginx.json.
Het aangepaste profiel toepassen
CLI:
docker run -d \
--security-opt seccomp=/etc/docker/seccomp-nginx.json \
--name seccomp-test \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- seccomp=/etc/docker/seccomp-nginx.json
Verifiëren dat het profiel actief is
docker inspect --format '{{.HostConfig.SecurityOpt}}' seccomp-test
De output toont de volledige seccomp-profiel JSON die op de container is toegepast. Docker leest het bestand bij het aanmaken van de container en sluit de profielinhoud in.
Test dat een beperkte operatie faalt:
docker exec seccomp-test unshare --mount /bin/sh -c 'echo escaped'
Dit zou moeten falen met "Operation not permitted". De container mist CAP_SYS_ADMIN (niet standaard toegekend), en het seccomp-profiel biedt een tweede verdedigingslaag door alleen de syscalls toe te staan die nodig zijn voor normaal gebruik.
Opruimen:
docker rm -f seccomp-test
rm -f /tmp/nginx-syscalls.log
Productietip: Begin met het standaardprofiel van Docker en verwijder syscalls in plaats van vanaf nul te bouwen. De strace-aanpak geeft je het strakst mogelijke profiel maar vereist grondig testen. Doorloop elk codepad dat je applicatie gebruikt tijdens de strace-capture: opstarten, normale requests, foutafhandeling, graceful shutdown (docker stop) en log-rotatie.
Iteratieve aanpak: Als bouwen vanaf strace riskant voelt, gebruik dan deze veiligere workflow:
- Kopieer Docker's standaard seccomp-profiel als startpunt.
- Draai je container met dit gekopieerde profiel. Het gedraagt zich identiek aan de standaard.
- Verwijder syscalls per groep (bijv. alle
key*syscalls, alleswap*syscalls). - Test de container na elke verwijdering. Als het breekt, voeg de laatst verwijderde syscall terug toe.
- Herhaal tot je alles hebt verwijderd wat je applicatie niet nodig heeft.
Dit is langzamer dan de strace-methode maar veiliger voor productiecontainers waar het missen van een syscall intermitterende fouten kan veroorzaken.
Hoe schrijf ik een AppArmor-profiel voor een Docker-container?
AppArmor beperkt welke bestanden, netwerkbronnen en capabilities een containerproces kan benaderen. Docker past automatisch een standaard docker-default profiel toe. Met een aangepast profiel kun je containers verder beperken tot alleen de bestandspaden en netwerkoperaties die ze nodig hebben.
Controleer of AppArmor actief is
sudo aa-status
Je zou docker-default in de lijst van geladen profielen moeten zien. Als AppArmor niet geïnstalleerd is:
sudo apt-get install -y apparmor apparmor-utils
Een aangepast profiel schrijven
Maak /etc/apparmor.d/containers/docker-nginx:
#include <tunables/global>
profile docker-nginx flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/nameservice>
# Capabilities needed by nginx
capability chown,
capability setuid,
capability setgid,
capability net_bind_service,
capability dac_override,
# Network access
network inet tcp,
network inet udp,
network inet6 tcp,
network inet6 udp,
# Shell and entrypoint scripts
/bin/** rix,
/usr/bin/** rix,
/usr/sbin/** rix,
/lib/** mr,
/usr/lib/** mr,
/docker-entrypoint.sh rix,
/docker-entrypoint.d/ r,
/docker-entrypoint.d/** rix,
/dev/null rw,
/dev/stdout rw,
/dev/stderr rw,
# Nginx binary
/usr/sbin/nginx ix,
# Config files (read only)
/etc/ r,
/etc/nginx/ r,
/etc/nginx/** r,
# Web root (read only)
/usr/share/nginx/html/** r,
# Temp and cache directories
/var/cache/nginx/ rw,
/var/cache/nginx/** rw,
/var/log/nginx/ rw,
/var/log/nginx/** rw,
/run/ rw,
/run/** rw,
/tmp/ rw,
/tmp/** rw,
# Proc filesystem (needed for nginx worker management)
/proc/** r,
# Deny sensitive files
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem r,
deny /sys/** w,
}
Het profiel heeft capability-declaraties nodig omdat AppArmor capability-gebruik onafhankelijk van Docker's --cap-add flags beheert. De entrypoint-scriptpaden (/docker-entrypoint.sh, /docker-entrypoint.d/) en shell-binaries (/bin/**) moeten expliciet worden toegestaan, anders start de container niet. De rix-permissie betekent lezen, execution context overerven en uitvoering toestaan.
Het profiel laden en toepassen
sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
Controleer of het geladen is:
sudo aa-status | grep docker-nginx
Draai een container met het aangepaste profiel:
CLI:
docker run -d \
--security-opt apparmor=docker-nginx \
--name apparmor-test \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
security_opt:
- apparmor=docker-nginx
AppArmor-handhaving verifiëren
docker exec apparmor-test cat /etc/shadow
Dit zou "Permission denied" moeten teruggeven omdat het profiel expliciet het lezen van /etc/shadow weigert.
Controleer de AppArmor-status van de container:
docker inspect --format '{{.AppArmorProfile}}' apparmor-test
Verwachte output: docker-nginx.
Opruimen:
docker rm -f apparmor-test
Hoe draai ik een Docker-container met een read-only bestandssysteem?
Een read-only root-bestandssysteem voorkomt dat aanvallers malware, backdoors of aangepaste binaries schrijven in een gecompromitteerde container. De container kan nog steeds schrijven naar expliciet gemounte tmpfs-volumes voor tijdelijke bestanden en runtime-gegevens.
CLI:
docker run -d \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--tmpfs /run:rw,noexec,nosuid,size=16m \
--tmpfs /var/cache/nginx:rw,noexec,nosuid,size=128m \
-p 8080:80 \
--name readonly-nginx \
nginx:alpine
Compose:
services:
web:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
Let op: de noexec flag op tmpfs-mounts. Dit voorkomt het uitvoeren van binaries vanuit tijdelijke directory's, een veelgebruikte techniek die aanvallers inzetten nadat ze schrijftoegang hebben verkregen.
Verifiëren dat het bestandssysteem read-only is
docker exec readonly-nginx touch /testfile
Verwachte output:
touch: /testfile: Read-only file system
Bevestig dat tmpfs-mounts werken:
docker exec readonly-nginx touch /tmp/testfile && echo "tmpfs works"
Controleer of de container verkeer afhandelt:
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080
Verwacht: 200.
Opruimen:
docker rm -f readonly-nginx
Hoe moet een geharde Docker daemon.json eruitzien?
Een geharde daemon.json past beveiligingsstandaarden toe op elke container op de host. Individuele containers kunnen sommige instellingen nog overschrijven, maar de daemon-configuratie stelt de basislijn in.
Maak of bewerk /etc/docker/daemon.json:
{
"no-new-privileges": true,
"icc": false,
"live-restore": true,
"userland-proxy": false,
"log-driver": "journald",
"log-opts": {
"tag": "{{.Name}}"
},
"default-ulimits": {
"nproc": {
"Name": "nproc",
"Hard": 512,
"Soft": 256
},
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 32768
}
},
"storage-driver": "overlay2"
}
Wat elke instelling doet:
no-new-privileges: Blokkeert setuid/setgid-escalatie in alle containers standaard.icc: false: Schakelt inter-container communicatie op het standaard bridge-netwerk uit. Containers kunnen elkaar alleen bereiken via expliciet gepubliceerde poorten of door de gebruiker gedefinieerde netwerken. Dit beperkt lateral movement als een container gecompromitteerd is.live-restore: Houdt containers draaiende tijdens daemon-herstarts. Voorkomt downtime tijdens Docker-upgrades.userland-proxy: false: Gebruikt iptables voor port mapping in plaats van een userland proxy-proces. Betere prestaties, minder open file descriptors.log-driver: journald: Stuurt containerlogs naar het systeemjournaal waar ze centraal beheerd en geroteerd worden.default-ulimits: Beperkt processen en open bestanden per container. Voorkomt fork bombs en file descriptor exhaustion.
Pas de wijzigingen toe:
sudo systemctl restart docker
Controleer of de daemon de configuratie heeft opgepikt:
docker info --format '{{.SecurityOptions}}'
Je zou no-new-privileges in de security options lijst moeten zien.
Controleer of ICC is uitgeschakeld:
docker network inspect bridge --format '{{index .Options "com.docker.network.bridge.enable_icc"}}'
Verwachte output: false.
Opmerking over userns-remap: Als je user namespace remapping hebt gekozen boven rootless mode, voeg dan "userns-remap": "default" toe aan deze configuratie. Combineer userns-remap niet met rootless Docker.
Versie verbergen: Overweeg ook om Docker API-versie-informatie te verbergen terwijl je daemon.json bewerkt. Docker toont standaard geen versieheaders zoals Nginx dat doet, maar als je de Docker API op een TCP-socket draait (doe dat niet, tenzij het echt moet), bescherm het dan met TLS client-certificaten. Een blootgestelde Docker API is gelijkwaardig aan root shell-toegang.
Je setup auditen: Draai Docker Bench for Security om je configuratie te auditen tegen de CIS Docker Benchmark. Kloon de repository en voer het script direct uit, aangezien de container-image een verouderde Docker-client bevat die niet compatibel is met Docker Engine 29.x:
git clone https://github.com/docker/docker-bench-security.git /tmp/docker-bench
cd /tmp/docker-bench && sudo bash docker-bench-security.sh
Bekijk de output op WARN entries. De geharde daemon.json hierboven adresseert de meeste daarvan. Items gemarkeerd als INFO zijn aanbevelingen, geen fouten.
Meerdere beveiligingslagen combineren in Docker Compose
Een productie Compose-bestand moet meerdere beveiligingsmaatregelen stapelen. Hier is een voorbeeld voor een Nginx-container met alle zeven lagen toegepast:
services:
web:
image: nginx:alpine
read_only: true
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
security_opt:
- no-new-privileges:true
- seccomp=/etc/docker/seccomp-nginx.json
- apparmor=docker-nginx
tmpfs:
- /tmp:rw,noexec,nosuid,size=64m
- /run:rw,noexec,nosuid,size=16m
- /var/cache/nginx:rw,noexec,nosuid,size=128m
ports:
- "80:80"
pids_limit: 100
deploy:
resources:
limits:
memory: 256M
cpus: '1.0'
Let op: pids_limit voorkomt fork bombs en deploy.resources.limits begrenst geheugen en CPU. Dit zijn geen security-opt flags, maar ze voorkomen denial-of-service vanuit de container. Voor meer over resource limits, zie Docker Compose resourcelimieten, healthchecks en herstartbeleid.
Controleer of alle beveiligingsopties actief zijn op de draaiende container:
docker compose up -d
docker inspect web --format '{{json .HostConfig.SecurityOpt}}' | python3 -m json.tool
Je zou no-new-privileges, je seccomp-profielpad en de AppArmor-profielnaam vermeld moeten zien.
Productie hardening checklist
| Maatregel | CLI flag | Compose key | daemon.json | Voorkomen dreiging |
|---|---|---|---|---|
| Rootless Docker | N/A (daemon-level) | N/A | N/A (aparte daemon) | Container escape geeft host root |
| userns-remap | N/A | N/A | "userns-remap": "default" |
Container root = host root |
| Drop capabilities | --cap-drop ALL --cap-add X |
cap_drop: [ALL] |
N/A | Kernel-aanvalsoppervlak |
| no-new-privileges | --security-opt no-new-privileges:true |
security_opt: [no-new-privileges:true] |
"no-new-privileges": true |
Setuid/setgid-escalatie |
| Aangepast seccomp | --security-opt seccomp=profile.json |
security_opt: [seccomp:path] |
N/A | Kernel exploit via geblokkeerde syscalls |
| AppArmor-profiel | --security-opt apparmor=name |
security_opt: [apparmor:name] |
N/A | Bestands-/netwerktoegang buiten app-behoeften |
| Read-only rootfs | --read-only |
read_only: true |
N/A | Persistente malware, binary tampering |
| ICC uitschakelen | N/A | N/A | "icc": false |
Lateral movement tussen containers |
| Processen beperken | --pids-limit 100 |
pids_limit: 100 |
"default-pids-limit": 100 |
Fork bombs |
| Userland proxy uitschakelen | N/A | N/A | "userland-proxy": false |
Resourceverspilling, verkleint aanvalsoppervlak |
Probleemoplossing
Container start niet na toevoegen van seccomp-profiel:
Je profiel mist een syscall die de applicatie nodig heeft. Draai tijdelijk met --security-opt seccomp=unconfined, strace het proces en voeg de ontbrekende syscalls toe aan je profiel.
Controleer kernel audit logs op geblokkeerde syscalls:
sudo journalctl -k | grep -i seccomp
AppArmor blokkeert legitieme operaties: Zet het profiel in complain mode om weigeringen te loggen zonder ze af te dwingen:
sudo aa-complain /etc/apparmor.d/containers/docker-nginx
Bekijk de logs:
sudo journalctl -k | grep apparmor | tail -20
Na het toevoegen van de vereiste permissies, schakel terug naar enforce mode:
sudo aa-enforce /etc/apparmor.d/containers/docker-nginx
Rootless Docker kan geen images pullen:
Controleer of DOCKER_HOST correct is ingesteld:
echo $DOCKER_HOST
Het moet verwijzen naar /run/user/<UID>/docker.sock. Controleer ook of de rootless daemon draait:
systemctl --user status docker
Bind mount permission denied met userns-remap: Bestanden op de host die eigendom zijn van root (UID 0) zijn ontoegankelijk omdat de UID 0 van de container naar een hoge host-UID mapt. Oplossing door eigenaarschap te wijzigen:
# Find the remapped UID
grep dockremap /etc/subuid
# Then chown to that UID
sudo chown -R <remapped-uid>:<remapped-uid> /path/to/bind/mount
Vervang <remapped-uid> door het eerste nummer uit /etc/subuid voor de dockremap-gebruiker (doorgaans 100000).
Containerlogs: Voor elk Docker-gerelateerd probleem, begin met de containerlogs en het Docker daemon-journaal:
docker logs <container-name>
sudo journalctl -u docker -f
Voor rootless Docker, controleer het journaal op gebruikersniveau:
journalctl --user -u docker -f
Copyright 2026 Virtua.Cloud. Alle rechten voorbehouden. Deze inhoud is een origineel werk van het Virtua.Cloud-team. Reproductie, herpublicatie of herdistributie zonder schriftelijke toestemming is verboden.
Klaar om het zelf te proberen?
Deploy uw eigen server in seconden. Linux, Windows of FreeBSD.
Bekijk VPS-aanbod