Sicurezza Docker: Rootless Mode, Seccomp e AppArmor su VPS
Sette livelli di hardening per Docker su VPS. Ogni sezione spiega la minaccia, mostra la soluzione con sintassi CLI e Compose, e verifica che funzioni.
La configurazione predefinita di Docker sacrifica la sicurezza per la comodità. I container girano come root sull'host. Tutte le 14 Linux capabilities restano attive. Seccomp blocca solo 44 delle oltre 300 syscall. Il traffico tra container scorre liberamente.
Su un VPS, questo conta più che su una macchina di sviluppo locale. Condividi un host fisico con altri tenant. Un container escape significa che un attaccante atterra come root sul kernel esposto all'hypervisor. Ogni livello di hardening che aggiungi riduce il raggio dell'impatto.
Questo tutorial copre sette misure di hardening. Ogni sezione spiega la minaccia che previene, mostra l'implementazione (sia con flag docker run che con sintassi Compose) e include un passaggio di verifica. Abbiamo testato ogni comando su Ubuntu 24.04 con Docker Engine 29.x.
Prerequisiti: Un VPS con Debian 12 o Ubuntu 24.04 e Docker Engine installato. Accesso SSH come utente non-root con sudo. Se non hai ancora messo in sicurezza l'host, parti dalla nostra guida alla sicurezza VPS Linux. Per problemi di firewall con Docker, vedi Docker UFW Firewall Fix.
Questo articolo fa parte della serie .
Come configuro rootless Docker su Ubuntu 24.04 o Debian 12?
Rootless Docker esegue il daemon e tutti i container sotto un account utente normale invece che root. Se un attaccante evade dal container, atterra come utente senza privilegi sull'host. Nessun accesso root. Tra tutte le misure di questa guida, questa ha l'impatto maggiore.
Installare rootless Docker
Installa i pacchetti necessari. Il pacchetto uidmap fornisce newuidmap e newgidmap, che gestiscono la mappatura degli UID/GID subordinati:
sudo apt-get update && sudo apt-get install -y uidmap docker-ce-rootless-extras
Verifica che il tuo utente abbia almeno 65.536 UID e GID subordinati:
grep "^$(whoami):" /etc/subuid
grep "^$(whoami):" /etc/subgid
Dovresti vedere un output come deploy:100000:65536. Se le voci mancano, aggiungile:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $(whoami)
Ferma il daemon Docker di sistema. Non ti serve per la modalità rootless:
sudo systemctl disable --now docker.service docker.socket
Esegui lo strumento di setup rootless come utente normale (non root):
dockerd-rootless-setuptool.sh install
Lo script stampa le variabili d'ambiente da impostare. Aggiungile al tuo profilo shell:
echo 'export PATH=/usr/bin:$PATH' >> ~/.bashrc
echo 'export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock' >> ~/.bashrc
source ~/.bashrc
Abilita il linger così il daemon rootless parte al boot, non solo quando fai login:
sudo loginctl enable-linger $(whoami)
Verificare la modalità rootless
docker context use rootless
docker run --rm hello-world
Controlla che il processo del daemon giri col tuo utente, non root:
ps aux | grep dockerd
L'output deve mostrare il tuo nome utente, non root. Conferma anche che Docker info riporta rootless:
docker info --format '{{.SecurityOptions}}'
Dovresti vedere rootless nella lista.
Quando rootless Docker crea problemi
La modalità rootless ha limitazioni reali. Conoscerle evita ore di debug.
| Limitazione | Causa | Soluzione |
|---|---|---|
| Non può fare bind su porte sotto la 1024 | Gli utenti non-root non possono fare bind su porte privilegiate | Imposta sysctl net.ipv4.ip_unprivileged_port_start=0 o usa un reverse proxy sull'host |
| Errori di permessi sui bind mount | I file dell'host di proprietà di root sono inaccessibili all'UID rimappato | Cambia ownership al tuo utente, o usa named volumes |
| Filesystem overlay più lento | Rootless usa fuse-overlayfs invece del nativo overlay2 |
Accetta l'overhead (5-15% per workload pesanti in I/O) o usa overlay2 nativo con --privileged (vanifica lo scopo) |
Niente --net=host |
Il networking rootless usa slirp4netns o pasta, non lo stack di rete dell'host | Usa il port mapping (-p). Per prestazioni migliori, installa pasta come driver di rete |
ping fallisce nei container |
CAP_NET_RAW è limitata |
Installa slirp4netns >= 0.4.0 o usa pasta |
Nota sulle prestazioni di rete: Per impostazione predefinita, rootless Docker usa slirp4netns per il networking, che aggiunge overhead NAT. Il driver pasta copia la configurazione di rete dell'host nel namespace del container senza NAT e offre un throughput migliore. Su Debian 12 e Ubuntu 24.04, installalo con:
sudo apt-get install -y passt
Docker rileva pasta automaticamente se è installato.
Quando usare user namespace remapping invece di rootless Docker?
User namespace remapping (userns-remap) mappa l'UID 0 dentro i container a un UID senza privilegi sull'host. A differenza di rootless Docker, il daemon stesso gira ancora come root. Questo significa che mantieni tutte le funzionalità Docker (porte privilegiate, host networking, overlay2 nativo) impedendo comunque che root nel container equivalga a root sull'host.
Scegli userns-remap quando la modalità rootless non funziona col tuo workload ma vuoi comunque l'isolamento UID. Scegli rootless quando puoi convivere con le sue limitazioni.
| Funzionalità | Rootless Docker | userns-remap | Docker standard |
|---|---|---|---|
| Il daemon gira come | Utente | Root | Root |
| Root nel container = root sull'host | No | No | Sì |
| Porte privilegiate | Serve workaround | Funziona | Funziona |
--net=host |
No | Sì | Sì |
| Storage driver | fuse-overlayfs | overlay2 | overlay2 |
| Complessità del setup | Media | Bassa | Nessuna |
Configurare userns-remap
Crea l'utente dockremap o usa la scorciatoia default che lo crea automaticamente:
sudo tee /etc/docker/daemon.json > /dev/null <<'EOF'
{
"userns-remap": "default"
}
EOF
sudo systemctl restart docker
Docker crea l'utente dockremap e aggiunge i range di UID/GID subordinati a /etc/subuid e /etc/subgid.
Verifica che funzioni:
sudo ls -ld /var/lib/docker/
Dovresti vedere una nuova sottodirectory con il nome del range UID rimappato, ad esempio /var/lib/docker/100000.100000/. Il numero esatto dipende dal range di UID subordinati assegnato all'utente dockremap in /etc/subuid.
Esegui un container e controlla l'UID del processo sull'host:
docker run -d --name test-userns nginx:alpine
ps aux | grep nginx
Il processo nginx deve mostrare un UID alto (corrispondente al primo numero da /etc/subuid per dockremap), non 0.
Pulizia:
docker rm -f test-userns
Attenzione alla ownership dei volumi: I file montati con bind di proprietà di root (UID 0) appaiono come nobody dentro il container perché l'UID 0 viene mappato a un range diverso. Usa named volumes o fai chown dei file all'UID rimappato.
Come rimuovo le Linux capabilities da un container Docker?
Docker concede 14 Linux capabilities ai container per impostazione predefinita. Ogni capability è un permesso kernel che un attaccante può sfruttare dopo un container escape o dentro un container compromesso. Rimuovere tutte le capabilities e riaggiungere solo quelle necessarie riduce la superficie d'attacco.
Capabilities Docker predefinite
| Capability | Cosa consente | Tenere o rimuovere? |
|---|---|---|
CAP_CHOWN |
Cambiare la ownership dei file | Rimuovere se non necessaria |
CAP_DAC_OVERRIDE |
Bypassare i controlli di permessi in lettura/scrittura | Rimuovere se non necessaria |
CAP_FOWNER |
Bypassare i controlli di permessi sul proprietario del file | Rimuovere se non necessaria |
CAP_FSETID |
Impostare i bit setuid/setgid | Rimuovere |
CAP_KILL |
Inviare segnali a qualsiasi processo | Rimuovere se non necessaria |
CAP_SETGID |
Cambiare il GID del processo | Tenere per la maggior parte delle app |
CAP_SETUID |
Cambiare l'UID del processo | Tenere per la maggior parte delle app |
CAP_SETPCAP |
Modificare le capabilities del processo | Rimuovere |
CAP_NET_BIND_SERVICE |
Fare bind su porte sotto la 1024 | Tenere se fai bind su porta 80/443 |
CAP_NET_RAW |
Usare raw socket (creare pacchetti) | Rimuovere a meno che servano ping/traceroute |
CAP_SYS_CHROOT |
Usare chroot | Rimuovere |
CAP_MKNOD |
Creare file di device | Rimuovere |
CAP_AUDIT_WRITE |
Scrivere nel log di audit del kernel | Rimuovere se non necessaria |
CAP_SETFCAP |
Impostare le capabilities dei file | Rimuovere |
Rimuovi tutto, riaggiungi quello che serve
Sintassi CLI:
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 ha bisogno di CHOWN perché il suo entrypoint script cambia la ownership delle directory di cache all'avvio. Senza, il container si ferma subito con un errore chown: Operation not permitted.
Sintassi Compose:
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- CHOWN
- NET_BIND_SERVICE
- SETUID
- SETGID
Verificare che le capabilities siano state rimosse
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep Cap'
Confronta la bitmask CapEff (effective capabilities). Con tutte rimosse e quattro riattivate, il valore è molto più basso del predefinito 00000000a80425fb.
Per un output leggibile, installa capsh sull'host e decodifica l'esadecimale:
docker exec hardened-nginx sh -c 'cat /proc/1/status | grep CapEff' | awk '{print $2}' | xargs -I{} capsh --decode=0x{}
Dovresti vedere solo cap_chown,cap_setgid,cap_setuid,cap_net_bind_service nell'output.
Pulizia:
docker rm -f hardened-nginx
Cosa previene il flag no-new-privileges?
Il flag no-new-privileges impedisce ai processi dentro un container di acquisire privilegi aggiuntivi tramite binari setuid o setgid. Senza questo flag, un processo compromesso può eseguire un binario setuid (come su o sudo) e scalare a root. Con il flag attivo, il kernel rifiuta l'escalation dei privilegi.
Applicare per container
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
Applicare come default del daemon
Aggiungilo a daemon.json così ogni container riceve questo flag automaticamente:
{
"no-new-privileges": true
}
Riavvia Docker dopo la modifica:
sudo systemctl restart docker
Verificare che funzioni
docker exec no-priv-test grep NoNewPrivs /proc/1/status
Output atteso:
NoNewPrivs: 1
Il valore 1 significa che non possono essere acquisiti nuovi privilegi. Il valore 0 significa che il flag non è impostato.
Pulizia:
docker rm -f no-priv-test
Come creo un profilo seccomp personalizzato per Docker?
Il profilo seccomp predefinito di Docker blocca circa 44 syscall su oltre 300. Un profilo personalizzato ti permette di limitare i container alle sole syscall che la tua applicazione usa effettivamente. Se un attaccante compromette il container, non può sfruttare vulnerabilità del kernel tramite syscall bloccate.
Scoprire quali syscall servono alla tua applicazione
Usa strace su un container in esecuzione per catturare le syscall effettuate durante il funzionamento normale:
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
Estrai i nomi delle syscall uniche:
grep -oP '^\[pid \d+\] \K\w+|^\w+' /tmp/nginx-syscalls.log | sort -u
Questo ti dà il set minimo di syscall necessarie alla tua applicazione.
Costruire il profilo personalizzato
Crea un file JSON. La defaultAction è SCMP_ACT_ERRNO (nega tutto ciò che non è esplicitamente consentito). La lista di syscall deve includere sia quelle necessarie alla tua applicazione SIA quelle usate dal container runtime (runc) durante l'inizializzazione. Il profilo qui sotto è stato testato con nginx:alpine su 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"
}
]
}
Perché così tante syscall? La lista include syscall necessarie a tre livelli: il container runtime (runc) per il setup dei namespace (clone3, mount, pivot_root, unshare, seccomp, statx), la shell Alpine e gli entrypoint script (fork, open, pipe, wait4), e nginx stesso (accept4, bind, listen, io_setup). Un'immagine basata su glibc avrebbe bisogno di meno syscall relative alla shell, ma di più interne alla libc.
Salva il file come /etc/docker/seccomp-nginx.json.
Applicare il profilo personalizzato
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
Verificare che il profilo sia attivo
docker inspect --format '{{.HostConfig.SecurityOpt}}' seccomp-test
L'output mostra il JSON del profilo seccomp applicato al container. Docker legge il file al momento della creazione del container e incorpora il contenuto del profilo.
Testa che un'operazione limitata fallisca:
docker exec seccomp-test unshare --mount /bin/sh -c 'echo escaped'
Deve fallire con "Operation not permitted". Il container non ha CAP_SYS_ADMIN (non concessa per impostazione predefinita), e il profilo seccomp fornisce un secondo livello di difesa consentendo solo le syscall necessarie al funzionamento normale.
Pulizia:
docker rm -f seccomp-test
rm -f /tmp/nginx-syscalls.log
Consiglio per la produzione: Parti dal profilo predefinito di Docker e rimuovi syscall piuttosto che costruire da zero. L'approccio con strace ti dà il profilo più restrittivo possibile ma richiede test approfonditi. Esercita ogni percorso di codice della tua applicazione durante la cattura con strace: avvio, richieste normali, gestione errori, spegnimento controllato (docker stop) e rotazione dei log.
Approccio iterativo: Se costruire da strace sembra troppo rischioso, usa questo workflow più sicuro:
- Copia il profilo seccomp predefinito di Docker come punto di partenza.
- Esegui il container con questo profilo copiato. Si comporta in modo identico a quello predefinito.
- Rimuovi syscall un gruppo alla volta (ad es. tutte le syscall
key*, tutte le syscallswap*). - Testa il container dopo ogni rimozione. Se si rompe, riaggiungi l'ultima syscall rimossa.
- Ripeti finché non hai tagliato tutto ciò che la tua applicazione non usa.
Questo è più lento del metodo con strace ma più sicuro per container di produzione dove mancare una syscall potrebbe causare errori intermittenti.
Come scrivo un profilo AppArmor per un container Docker?
AppArmor limita i file, le risorse di rete e le capabilities a cui un processo container può accedere. Docker applica automaticamente un profilo predefinito docker-default. Un profilo personalizzato permette di limitare ulteriormente i container ai soli percorsi del filesystem e alle operazioni di rete necessarie.
Verificare che AppArmor sia attivo
sudo aa-status
Dovresti vedere docker-default nella lista dei profili caricati. Se AppArmor non è installato:
sudo apt-get install -y apparmor apparmor-utils
Scrivere un profilo personalizzato
Crea /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,
}
Il profilo ha bisogno delle dichiarazioni capability perché AppArmor controlla l'uso delle capability in modo indipendente dai flag --cap-add di Docker. I percorsi degli entrypoint script (/docker-entrypoint.sh, /docker-entrypoint.d/) e dei binari shell (/bin/**) devono essere esplicitamente consentiti, altrimenti il container non parte. Il permesso rix significa lettura, ereditarietà del contesto di esecuzione e permesso di esecuzione.
Caricare e applicare il profilo
sudo apparmor_parser -r -W /etc/apparmor.d/containers/docker-nginx
Verifica che sia caricato:
sudo aa-status | grep docker-nginx
Esegui un container con il profilo personalizzato:
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
Verificare l'enforcement di AppArmor
docker exec apparmor-test cat /etc/shadow
Deve restituire "Permission denied" perché il profilo nega esplicitamente la lettura di /etc/shadow.
Controlla lo stato AppArmor del container:
docker inspect --format '{{.AppArmorProfile}}' apparmor-test
Output atteso: docker-nginx.
Pulizia:
docker rm -f apparmor-test
Come eseguo un container Docker con filesystem in sola lettura?
Un filesystem root in sola lettura impedisce agli attaccanti di scrivere malware, backdoor o binari modificati dentro un container compromesso. Il container può comunque scrivere su volumi tmpfs montati esplicitamente per file temporanei e dati di runtime.
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
Nota il flag noexec sui mount tmpfs. Questo impedisce l'esecuzione di binari dalle directory temporanee, una tecnica comune usata dagli attaccanti dopo aver ottenuto accesso in scrittura.
Verificare che il filesystem sia in sola lettura
docker exec readonly-nginx touch /testfile
Output atteso:
touch: /testfile: Read-only file system
Conferma che i mount tmpfs funzionino:
docker exec readonly-nginx touch /tmp/testfile && echo "tmpfs works"
Verifica che il container stia servendo traffico:
curl -s -o /dev/null -w '%{http_code}' http://localhost:8080
Atteso: 200.
Pulizia:
docker rm -f readonly-nginx
Come deve essere un daemon.json Docker protetto?
Un daemon.json protetto applica impostazioni di sicurezza predefinite a ogni container sull'host. I singoli container possono comunque sovrascrivere alcune impostazioni, ma la configurazione del daemon stabilisce la baseline.
Crea o modifica /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"
}
Cosa fa ogni impostazione:
no-new-privileges: Blocca l'escalation setuid/setgid in tutti i container per impostazione predefinita.icc: false: Disabilita la comunicazione tra container sulla rete bridge predefinita. I container possono raggiungersi solo tramite porte esplicitamente pubblicate o reti definite dall'utente. Questo limita il movimento laterale se un container viene compromesso.live-restore: Mantiene i container in esecuzione durante i riavvii del daemon. Previene i downtime durante gli aggiornamenti di Docker.userland-proxy: false: Usa iptables per il port mapping invece di un processo proxy userland. Prestazioni migliori, meno file descriptor aperti.log-driver: journald: Invia i log dei container al journal di sistema dove vengono gestiti centralmente e ruotati.default-ulimits: Limita i processi e i file aperti per container. Previene fork bomb e esaurimento dei file descriptor.
Applica le modifiche:
sudo systemctl restart docker
Verifica che il daemon abbia recepito la configurazione:
docker info --format '{{.SecurityOptions}}'
Dovresti vedere no-new-privileges nella lista delle opzioni di sicurezza.
Controlla che ICC sia disabilitato:
docker network inspect bridge --format '{{index .Options "com.docker.network.bridge.enable_icc"}}'
Output atteso: false.
Nota su userns-remap: Se hai scelto user namespace remapping invece della modalità rootless, aggiungi "userns-remap": "default" a questa configurazione. Non combinare userns-remap con rootless Docker.
Nascondere la versione: Mentre modifichi daemon.json, considera anche di nascondere le informazioni sulla versione dell'API Docker. Docker non espone header di versione per impostazione predefinita come fa Nginx, ma se esponi l'API Docker su un socket TCP (non farlo, a meno che non sia strettamente necessario), proteggila con certificati TLS client. Un'API Docker esposta equivale a un accesso root shell.
Audit della configurazione: Esegui Docker Bench for Security per verificare la tua configurazione rispetto al CIS Docker Benchmark. Clona il repository ed esegui lo script direttamente, perché l'immagine container include un client Docker datato incompatibile con 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
Esamina l'output per le voci WARN. Il daemon.json protetto qui sopra ne risolve la maggior parte. Le voci segnate come INFO sono raccomandazioni, non errori.
Combinare più livelli di hardening in Docker Compose
Un file Compose di produzione deve sovrapporre più misure di hardening. Ecco un esempio per un container Nginx con tutti e sette i livelli applicati:
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'
Nota che pids_limit previene le fork bomb e deploy.resources.limits limita memoria e CPU. Non sono flag security-opt ma prevengono il denial-of-service dall'interno del container. Per saperne di più sui limiti di risorse, vedi .
Verifica che tutte le opzioni di sicurezza siano attive sul container in esecuzione:
docker compose up -d
docker inspect web --format '{{json .HostConfig.SecurityOpt}}' | python3 -m json.tool
Dovresti vedere no-new-privileges, il percorso del tuo profilo seccomp e il nome del profilo AppArmor elencati.
Checklist di hardening per la produzione
| Misura | Flag CLI | Chiave Compose | daemon.json | Minaccia prevenuta |
|---|---|---|---|---|
| Rootless Docker | N/A (livello daemon) | N/A | N/A (daemon separato) | Container escape dà root sull'host |
| userns-remap | N/A | N/A | "userns-remap": "default" |
Root nel container = root sull'host |
| Rimuovere capabilities | --cap-drop ALL --cap-add X |
cap_drop: [ALL] |
N/A | Superficie d'attacco kernel |
| no-new-privileges | --security-opt no-new-privileges:true |
security_opt: [no-new-privileges:true] |
"no-new-privileges": true |
Escalation setuid/setgid |
| Seccomp personalizzato | --security-opt seccomp=profile.json |
security_opt: [seccomp:path] |
N/A | Exploit kernel via syscall bloccate |
| Profilo AppArmor | --security-opt apparmor=name |
security_opt: [apparmor:name] |
N/A | Accesso a file/rete oltre le necessità dell'app |
| Rootfs in sola lettura | --read-only |
read_only: true |
N/A | Malware persistente, manomissione binari |
| Disabilitare ICC | N/A | N/A | "icc": false |
Movimento laterale tra container |
| Limitare i processi | --pids-limit 100 |
pids_limit: 100 |
"default-pids-limit": 100 |
Fork bomb |
| Disabilitare userland proxy | N/A | N/A | "userland-proxy": false |
Spreco risorse, riduce la superficie d'attacco |
Risoluzione dei problemi
Il container non parte dopo l'aggiunta del profilo seccomp:
Al tuo profilo manca una syscall necessaria all'applicazione. Esegui temporaneamente con --security-opt seccomp=unconfined, fai strace del processo e aggiungi le syscall mancanti al profilo.
Controlla i log di audit del kernel per le syscall bloccate:
sudo journalctl -k | grep -i seccomp
AppArmor blocca operazioni legittime: Imposta il profilo in modalità complain per loggare i dinieghi senza applicarli:
sudo aa-complain /etc/apparmor.d/containers/docker-nginx
Osserva i log:
sudo journalctl -k | grep apparmor | tail -20
Dopo aver aggiunto i permessi necessari, torna alla modalità enforce:
sudo aa-enforce /etc/apparmor.d/containers/docker-nginx
Rootless Docker non riesce a scaricare le immagini:
Controlla che DOCKER_HOST sia impostato correttamente:
echo $DOCKER_HOST
Deve puntare a /run/user/<UID>/docker.sock. Verifica anche che il daemon rootless sia in esecuzione:
systemctl --user status docker
Permesso negato sui bind mount con userns-remap: I file sull'host di proprietà di root (UID 0) sono inaccessibili perché l'UID 0 del container viene mappato a un UID alto sull'host. Risolvi cambiando la ownership:
# Find the remapped UID
grep dockremap /etc/subuid
# Then chown to that UID
sudo chown -R <remapped-uid>:<remapped-uid> /path/to/bind/mount
Sostituisci <remapped-uid> con il primo numero da /etc/subuid per l'utente dockremap (di solito 100000).
Log dei container: Per qualsiasi problema legato a Docker, parti dai log del container e dal journal del daemon Docker:
docker logs <container-name>
sudo journalctl -u docker -f
Per rootless Docker, controlla il journal a livello utente:
journalctl --user -u docker -f
Copyright 2026 Virtua.Cloud. Tutti i diritti riservati. Questo contenuto è un'opera originale del team Virtua.Cloud. La riproduzione, ripubblicazione o redistribuzione senza autorizzazione scritta è vietata.
Pronto a provare?
Distribuisci il tuo server in pochi secondi. Linux, Windows o FreeBSD.
Vedi piani VPS