Wprowadzenie — dlaczego bezpieczeństwo kontenerów Docker wymaga osobnej strategii
Jeśli śledzisz tę serię, to wiesz, że omówiliśmy już sporo: hartowanie jądra Linux (sysctl, Seccomp, Landlock), zabezpieczanie SSH (OpenSSH 10, kryptografia postkwantowa, FIDO2), budowę zahartowanego firewalla z nftables i wdrażanie kontroli MAC z SELinux i AppArmor. Wszystkie te warstwy wzmacniają system na poziomie hosta — ale co się dzieje, gdy na tym hoście zaczynasz uruchamiać kontenery Docker?
No właśnie. Kontener to izolowany proces działający na współdzielonym jądrze. Jeden błąd w konfiguracji i izolacja znika — atakujący uzyskuje dostęp do hosta lub innych kontenerów. Statystyki z 2025 roku mówią same za siebie: ponad 65% naruszeń bezpieczeństwa kontenerów wykorzystywało uprawnienia roota wewnątrz kontenera.
Żeby było jeszcze ciekawiej — jądro Linux zarejestrowało 5 530 CVE w samym 2025 roku. To średnio 8–9 nowych podatności dziennie. Docker Engine 28 (wydany w 2026) załatał trzy krytyczne podatności umożliwiające pełną ucieczkę z kontenera poprzez ominięcie restrykcji runc przy zapisie do /proc. Tak, pełną ucieczkę.
Ten artykuł to praktyczny przewodnik po hartowaniu kontenerów Docker na produkcji. Obejmuje zabezpieczenie hosta i demona, tryb rootless, profile Seccomp i AppArmor, skanowanie obrazów z Trivy, monitoring runtime z Falco oraz audyt zgodności z CIS Docker Benchmark v1.8.0. Każda sekcja zawiera działające przykłady — żadnej suchej teorii bez pokrycia w praktyce.
Zabezpieczenie hosta — fundament, bez którego reszta nie ma sensu
Bezpieczeństwo Dockera zaczyna się poza Dockerem — na hoście. Kontenery współdzielą jądro z hostem, więc jeśli jądro jest podatne, to każdy kontener na nim jest podatny. Proste jak budowa cepa.
Aktualizacja jądra i Docker Engine
Ustal harmonogram aktualizacji z maksymalnym dopuszczalnym wiekiem łatek. Oto rozsądne minimum:
- Łatki bezpieczeństwa jądra: w ciągu 7 dni od wydania
- Krytyczne poprawki jądra (container escape, privilege escalation): w ciągu 24–48 godzin
- Docker Engine: aktualizacja do najnowszej stabilnej wersji w ciągu 14 dni
# Sprawdź aktualną wersję Docker Engine
docker version --format '{{.Server.Version}}'
# Aktualizuj Docker Engine (Debian/Ubuntu)
sudo apt-get update && sudo apt-get install -y docker-ce docker-ce-cli containerd.io
# Sprawdź wersję jądra
uname -r
Minimalizacja powierzchni ataku hosta
Host dedykowany obciążeniom kontenerowym nie potrzebuje kompilatorów, narzędzi biurowych ani losowych utilsów. Mniej pakietów to mniej CVE i mniej wektorów pivotu dla atakującego. Szczerze mówiąc, byłem zaskoczony ile niepotrzebnych rzeczy znajdowałem na „oczyszczonych" serwerach — warto sprawdzić samemu.
# Wylistuj zainstalowane pakiety (Debian/Ubuntu)
dpkg --list | wc -l
# Usuń niepotrzebne pakiety
sudo apt-get purge -y gcc make perl wget curl telnet
sudo apt-get autoremove -y
# Wyłącz niepotrzebne usługi
sudo systemctl disable --now avahi-daemon cups bluetooth
Hartowanie jądra pod kontenery
Jeśli czytałeś nasz artykuł o hartowaniu jądra, wiele z tych ustawień prawdopodobnie już masz. Oto parametry szczególnie istotne dla środowisk kontenerowych:
# /etc/sysctl.d/99-docker-hardening.conf
# Wyłącz routing IPv4 (chyba że host jest routerem)
net.ipv4.ip_forward = 1 # Docker wymaga tego ustawienia
# Ochrona przed IP spoofingiem
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
# Ignoruj pakiety ICMP redirect
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
# Ochrona przed atakami SYN flood
net.ipv4.tcp_syncookies = 1
# Ogranicz dostęp do dmesg (ważne dla wykrywania seccomp)
kernel.dmesg_restrict = 1
# Ogranicz dostęp do /proc/kallsyms
kernel.kptr_restrict = 2
# Włącz ASLR
kernel.randomize_va_space = 2
# Zastosuj ustawienia
sudo sysctl --system
Konfiguracja demona Docker — daemon.json jako tarcza
Demon Docker działa z uprawnieniami roota i kontroluje każdy aspekt cyklu życia kontenerów. Jego konfiguracja w /etc/docker/daemon.json to jeden z najważniejszych plików bezpieczeństwa w całym systemie. Warto poświęcić mu chwilę uwagi.
{
"icc": false,
"no-new-privileges": true,
"userns-remap": "default",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"storage-driver": "overlay2",
"live-restore": true,
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 64000,
"Soft": 64000
},
"nproc": {
"Name": "nproc",
"Hard": 4096,
"Soft": 4096
}
}
}
Co tu się dzieje? Rozbijmy to na czynniki pierwsze:
"icc": false— wyłącza domyślną komunikację między kontenerami przez mostek docker0. Kontenery muszą być jawnie połączone w sieci użytkownika. To sama w sobie potężna zmiana."no-new-privileges": true— zapobiega eskalacji uprawnień wewnątrz kontenerów (blokuje setuid/setgid)."userns-remap": "default"— włącza remapowanie przestrzeni nazw użytkowników. Root (UID 0) w kontenerze mapuje się na nieuprzywilejowany UID na hoście."live-restore": true— kontenery działają dalej nawet po restarcie demona Docker. Przydatne przy aktualizacjach.
# Zastosuj konfigurację
sudo systemctl restart docker
# Zweryfikuj ustawienia
docker info --format '{{.SecurityOptions}}'
Tryb rootless — Docker bez uprawnień roota
Tryb rootless to moim zdaniem najskuteczniejsza pojedyncza zmiana, jaką możesz wprowadzić w bezpieczeństwie Docker. Demon Docker i wszystkie kontenery działają jako nieuprzywilejowany użytkownik — nawet jeśli atakującemu uda się uciec z kontenera, nie uzyskuje uprawnień roota na hoście. I to jest piękne.
Instalacja trybu rootless
# Wymagania: uidmap, dbus-user-session
sudo apt-get install -y uidmap dbus-user-session
# Sprawdź subordinate UID/GID
grep $USER /etc/subuid /etc/subgid
# Jeśli brak wpisów, dodaj je:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER
# Zainstaluj Docker rootless
dockerd-rootless-setuptool.sh install
# Skonfiguruj zmienne środowiskowe
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
Weryfikacja trybu rootless
# Sprawdź, czy Docker działa w trybie rootless
docker info --format '{{.SecurityOptions}}'
# Oczekiwane: [name=rootless ...]
# Sprawdź PID demona i jego właściciela
ps aux | grep dockerd
# Demon powinien działać jako zwykły użytkownik, nie root
# Test — uruchom kontener i sprawdź UID na hoście
docker run --rm alpine id
# uid=0(root) gid=0(root) — ale to root TYLKO w namespace!
# Sprawdź rzeczywisty UID procesu kontenera na hoście
docker run -d --name test-rootless alpine sleep 300
ps aux | grep "sleep 300"
# UID powinien być w zakresie 100000+, nie 0
Docker Engine 28 — ulepszenia rootless
Docker Engine 28 (2026) wprowadza kilka istotnych ulepszeń trybu rootless, które warto znać:
- RootlessKit v2.3.4 — poprawiona stabilność i wydajność
- Automatyczny fallback sieciowy — jeśli
slirp4netnsnie jest zainstalowany, Docker próbuje użyćpasta(passt) - Automatyczny wybór katalogu certyfikatów — eliminuje ręczną konfigurację w trybie rootless
- Poprawki setupu — skrypt
dockerd-rootless-setuptool.shprawidłowo raportuje błędy subuid/subgid
Ograniczenia trybu rootless
Tryb rootless nie jest idealny i ma kilka ograniczeń, o których warto wiedzieć zanim wdrożysz go w produkcji:
- Brak obsługi kontenerów uprzywilejowanych (
--privileged) - Domyślnie nie można bindować portów poniżej 1024
- Nieznaczny narzut na operacje sieciowe i systemu plików — ale szczerze, dla większości obciążeń webowych i API jest on zanedbywany
- Niektóre sterowniki storage mogą nie być dostępne
Profile Seccomp — zapora na poziomie wywołań systemowych
Seccomp (secure computing mode) to w zasadzie firewall na syscalle. Domyślny profil Dockera blokuje około 44 z ponad 300 wywołań systemowych — te niebezpieczne jak reboot, mount czy kexec_load. Ale dla aplikacji produkcyjnych zdecydowanie warto pójść dalej i stworzyć niestandardowy profil oparty na zasadzie minimalnych uprawnień.
Krok 1: Tryb audytu — sprawdź, czego potrzebuje Twoja aplikacja
Zamiast zgadywać, które syscalle są potrzebne (i walczyć z losowymi crashami), użyj profilu audytowego, który loguje zamiast blokować:
{
"defaultAction": "SCMP_ACT_LOG",
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
"syscalls": [
{
"names": [
"read", "write", "close", "fstat", "mmap",
"mprotect", "munmap", "brk", "exit", "exit_group"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Uruchom kontener z profilem audytowym
docker run --rm -d --name seccomp-audit \
--security-opt seccomp=/opt/seccomp/audit-profile.json \
myapp:latest
# Wykonaj typowe operacje aplikacji, a następnie sprawdź logi
sudo dmesg | grep -i seccomp
# Alternatywnie — użyj strace do wygenerowania listy syscalli
docker run --rm --security-opt seccomp=unconfined \
--entrypoint strace myapp:latest \
-c -f -S name /app/entrypoint.sh 2>&1 | \
grep -E '^\s+[0-9]' | awk '{print $NF}' | sort -u
Krok 2: Stwórz niestandardowy profil produkcyjny
Na podstawie audytu stwórz profil, który blokuje wszystko domyślnie i dopuszcza tylko te syscalle, których Twoja aplikacja faktycznie potrzebuje:
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32"],
"syscalls": [
{
"names": [
"accept4", "access", "arch_prctl", "bind", "brk",
"clone", "close", "connect", "dup2", "epoll_create1",
"epoll_ctl", "epoll_wait", "execve", "exit", "exit_group",
"fchown", "fcntl", "fstat", "futex", "getcwd",
"getdents64", "getegid", "geteuid", "getgid", "getpid",
"getppid", "getsockname", "getsockopt", "getuid",
"ioctl", "listen", "lseek", "madvise", "mmap",
"mprotect", "munmap", "nanosleep", "newfstatat",
"openat", "pipe2", "poll", "prctl", "pread64",
"pwrite64", "read", "recvfrom", "recvmsg", "rt_sigaction",
"rt_sigprocmask", "sendmsg", "sendto", "set_robust_list",
"set_tid_address", "setsockopt", "shutdown", "sigaltstack",
"socket", "stat", "uname", "unlink", "write", "writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
# Uruchom kontener z produkcyjnym profilem seccomp
docker run --rm -d --name myapp-hardened \
--security-opt seccomp=/opt/seccomp/myapp-production.json \
--security-opt no-new-privileges \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--read-only \
myapp:latest
Seccomp w Docker Compose
# docker-compose.yml z niestandardowym profilem seccomp
services:
api:
image: myapp:latest
ports:
- "8080:8080"
security_opt:
- seccomp=./profiles/myapp-seccomp.json
- no-new-privileges
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
Profile AppArmor — kontrola MAC dla kontenerów
Docker domyślnie stosuje profil docker-default AppArmor, co jest lepsze niż nic. Ale niestandardowy profil daje znacznie lepszą ochronę — pozwala precyzyjnie kontrolować, do jakich plików, katalogów i operacji sieciowych kontener ma dostęp.
Tworzenie niestandardowego profilu AppArmor
# /etc/apparmor.d/docker-myapp
#include <tunables/global>
profile docker-myapp flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
#include <abstractions/nameservice>
# Sieć — tylko TCP/UDP
network inet stream,
network inet dgram,
network inet6 stream,
network inet6 dgram,
# Odmów dostępu do wrażliwych ścieżek hosta
deny /proc/*/mem rwklx,
deny /proc/kcore rwklx,
deny /proc/sysrq-trigger rwklx,
deny /sys/firmware/** rwklx,
deny /sys/kernel/security/** rwklx,
# Odmów montowania systemów plików
deny mount,
# Odmów ładowania modułów jądra
deny /sbin/insmod x,
deny /sbin/modprobe x,
# Pozwól na odczyt konfiguracji aplikacji
/app/** r,
/app/bin/* ix,
# Pozwól na zapis do katalogu tymczasowego
/tmp/** rw,
# Pozwól na odczyt bibliotek
/usr/lib/** r,
/lib/** r,
# Logowanie
/var/log/app/** w,
}
# Załaduj profil
sudo apparmor_parser -r /etc/apparmor.d/docker-myapp
# Uruchom kontener z niestandardowym profilem
docker run --rm -d --name myapp-apparmor \
--security-opt apparmor=docker-myapp \
--security-opt no-new-privileges \
--cap-drop ALL \
myapp:latest
# Sprawdź zastosowany profil
docker inspect myapp-apparmor --format '{{.AppArmorProfile}}'
Budowanie bezpiecznych obrazów — od Dockerfile do rejestru
Bezpieczeństwo kontenera zaczyna się od FROM w Dockerfile. To może brzmieć jak truizm, ale w praktyce widziałem zbyt wiele obrazów produkcyjnych opartych na pełnym Debianie z kompilatorem C w środku. Oto praktyki, które eliminują większość typowych podatności.
Wieloetapowy build z minimalnym obrazem bazowym
# Etap 1: Budowanie
FROM golang:1.24-alpine AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server
# Etap 2: Obraz produkcyjny (distroless)
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /app/server
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/app/server"]
Checklist bezpiecznego Dockerfile
- Używaj pinowanych wersji — nigdy
:latest. Dla najwyższego bezpieczeństwa pinuj do digestu:FROM alpine@sha256:abc123... - Uruchamiaj jako non-root —
USER nonrootlub utwórz dedykowanego użytkownika - Stosuj wieloetapowe buildy — obraz produkcyjny zawiera tylko runtime i binarkę, nic więcej
- Nie kopiuj sekretów do obrazu — używaj Docker secrets lub montowania w runtime
- Używaj
.dockerignore— wyklucz.git,.env,node_modules - Preferuj obrazy distroless lub Alpine — distroless potrafi zredukować powierzchnię ataku o nawet 95%
Skanowanie obrazów z Trivy — CVE, sekrety i SBOM w pipeline
Trivy to wiodący skaner open source do wykrywania podatności, sekretów i błędów konfiguracji w obrazach kontenerów. Integruje się ze wszystkimi głównymi systemami CI/CD i — co ważne — jest naprawdę prosty w użyciu.
Skanowanie lokalne
# Instalacja Trivy
sudo apt-get install -y trivy
# Skanowanie obrazu — podatności + sekrety + błędy konfiguracji
trivy image --severity HIGH,CRITICAL --scanners vuln,secret,misconfig myapp:latest
# Generowanie SBOM w formacie CycloneDX
trivy image --format cyclonedx --output sbom.cdx.json myapp:latest
# Skanowanie SBOM (bez potrzeby pobierania obrazu ponownie)
trivy sbom sbom.cdx.json
Trivy w GitHub Actions
# .github/workflows/container-security.yml
name: Container Security Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: HIGH,CRITICAL
exit-code: 1
format: table
- name: Generate SBOM
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: cyclonedx
output: sbom.cdx.json
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: sbom.cdx.json
Polityka „zero krytycznych podatności"
Ustaw exit-code: 1 w Trivy, żeby pipeline CI/CD automatycznie blokował buildy zawierające krytyczne podatności. Brzmi drastycznie? Może. Ale studium przypadku z sektora medycznego pokazało, że egzekwowanie tej polityki (wraz z migracją na minimalne obrazy) zmniejszyło rozmiary obrazów o 30% i wyeliminowało krytyczne podatności w produkcji do zera. Warto.
Monitoring runtime z Falco — wykrywanie ataków w czasie rzeczywistym
Skanowanie obrazów wykrywa znane podatności w czasie budowania — ale co z atakami, które pojawiają się w runtime? Tutaj wchodzi Falco, projekt graduated CNCF, który monitoruje wywołania systemowe przez eBPF i wykrywa podejrzane zachowania w czasie rzeczywistym. Można go traktować jako system IDS dla kontenerów.
Wdrożenie Falco z Docker
# Uruchomienie Falco z nowoczesnym sterownikiem eBPF
docker run --rm -d --name falco \
--privileged \
-v /sys/kernel/tracing:/sys/kernel/tracing:ro \
-v /var/run/docker.sock:/host/var/run/docker.sock \
-v /proc:/host/proc:ro \
-v /etc:/host/etc:ro \
falcosecurity/falco:0.43.0 falco
Wdrożenie Falco na Kubernetes (Helm)
# Instalacja Falco z eBPF i Falcosidekick
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set driver.kind=modern_ebpf \
--set collectors.kubernetes.enabled=true \
--set falcosidekick.enabled=true
Niestandardowe reguły Falco dla Docker
Domyślne reguły Falco są całkiem niezłe, ale na produkcji warto dodać własne — dopasowane do Twojego środowiska:
# /etc/falco/rules.d/docker-custom.yaml
# Wykryj uruchomienie shella w kontenerze produkcyjnym
- rule: Shell spawned in production container
desc: Wykryto uruchomienie powłoki w kontenerze produkcyjnym
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, dash, ash) and
not container.image.repository in (allowed_debug_images)
output: >
Shell uruchomiony w kontenerze produkcyjnym
(user=%user.name command=%proc.cmdline container=%container.name
image=%container.image.repository)
priority: WARNING
tags: [container, shell, mitre_execution]
# Wykryj próbę odczytu wrażliwych plików
- rule: Sensitive file read in container
desc: Odczyt wrażliwego pliku w kontenerze
condition: >
open_read and container and
fd.name in (/etc/shadow, /etc/gshadow, /etc/passwd)
output: >
Odczyt wrażliwego pliku w kontenerze
(file=%fd.name user=%user.name container=%container.name)
priority: CRITICAL
tags: [container, filesystem, mitre_credential_access]
# Wykryj modyfikację plików systemowych
- rule: Write to system directories in container
desc: Zapis do katalogów systemowych w kontenerze
condition: >
write and container and
fd.directory in (/bin, /sbin, /usr/bin, /usr/sbin, /lib, /usr/lib) and
not proc.name in (dpkg, apt-get, yum, rpm)
output: >
Zapis do katalogu systemowego w kontenerze
(file=%fd.name user=%user.name container=%container.name)
priority: CRITICAL
tags: [container, filesystem, mitre_persistence]
Uruchamianie zahartowanych kontenerów — kompletne polecenie
Dobra, czas na zebranie wszystkich technik w jedno polecenie docker run. Oto jak wygląda naprawdę zahartowany kontener:
docker run -d --name myapp-production \
--user 1000:1000 \
--read-only \
--tmpfs /tmp:noexec,nosuid,size=100m \
--security-opt seccomp=/opt/seccomp/myapp-production.json \
--security-opt apparmor=docker-myapp \
--security-opt no-new-privileges \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--memory 512m \
--cpus 1.0 \
--pids-limit 100 \
--network myapp-internal \
--health-cmd "curl -f http://localhost:8080/health || exit 1" \
--health-interval 30s \
--health-timeout 5s \
--health-retries 3 \
--restart unless-stopped \
myapp:v2.1.0@sha256:abc123...
Sporo flag, prawda? Oto co robi każda z nich:
--user 1000:1000— kontener działa jako non-root--read-only— system plików tylko do odczytu (zapis blokowany)--tmpfs /tmp:noexec,nosuid— katalog tymczasowy bez prawa wykonywania--cap-drop ALL --cap-add NET_BIND_SERVICE— wszystkie capabilities usunięte poza bindowaniem portów--memory 512m --cpus 1.0 --pids-limit 100— limity zasobów (ochrona przed DoS)--network myapp-internal— izolowana sieć użytkownika (brak dostępu do docker0)
Docker Compose — zahartowany stos
W praktyce pewnie użyjesz Docker Compose. Oto jak wygląda zahartowany stos:
# docker-compose.hardened.yml
services:
api:
image: myapp:v2.1.0
user: "1000:1000"
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=100m
security_opt:
- seccomp=./profiles/api-seccomp.json
- apparmor=docker-myapp
- no-new-privileges
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
pids: 100
networks:
- internal
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
db:
image: postgres:16-alpine
user: "999:999"
read_only: true
tmpfs:
- /tmp:noexec,nosuid
- /run/postgresql:size=50m
security_opt:
- no-new-privileges
cap_drop:
- ALL
volumes:
- db-data:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: "2.0"
memory: 1G
networks:
- internal
networks:
internal:
driver: bridge
internal: true
volumes:
db-data:
Audyt zgodności z CIS Docker Benchmark v1.8.0
CIS Docker Benchmark to uznany standard bezpieczeństwa definiujący bezpieczną konfigurację całego ekosystemu Docker — od hosta, przez demona, po obrazy i runtime. Najnowsza wersja (v1.8.0, luty 2026) obejmuje aktualizacje dostosowane do Docker Engine 28.
Docker Bench for Security — automatyczny audyt
# Klonuj i uruchom docker-bench-security
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh
# Wynik zapisywany do:
# - docker-bench-security.log (tekstowy)
# - docker-bench-security.log.json (JSON)
Aqua docker-bench — nowsza alternatywa
Oficjalny docker/docker-bench-security bazuje na CIS v1.6.0, więc jest nieco w tyle. Dla aktualniejszych testów polecam narzędzie Aqua Security:
# Instalacja Aqua docker-bench
go install github.com/aquasecurity/docker-bench@latest
# Audyt z określoną wersją benchmarku
docker-bench --benchmark cis-1.8
# Eksport wyników do JSON
docker-bench --benchmark cis-1.8 --json > cis-audit-$(date +%Y%m%d).json
Kluczowe sekcje audytu CIS
Audyt CIS Docker Benchmark obejmuje sześć głównych obszarów:
- Konfiguracja hosta — parametry jądra, reguły audytu, osobna partycja dla
/var/lib/docker - Konfiguracja demona Docker — TLS, autoryzacja, logowanie, user namespaces
- Uprawnienia plików konfiguracyjnych —
daemon.json, certyfikaty TLS, socket - Obrazy i pliki Dockerfile — bazowe obrazy, użytkownicy, sekrety, HEALTHCHECK
- Runtime kontenerów — capabilities, seccomp, AppArmor, limity zasobów
- Operacje bezpieczeństwa Docker — audyt logów, zarządzanie sekretami, Content Trust
Zarządzanie sekretami — nigdy w obrazie, nigdy w zmiennych środowiskowych
Zapamiętaj jedną zasadę: jeśli sekret znajduje się w warstwach obrazu — traktuj go jako publiczny. Nawet jeśli usuniesz go w późniejszej warstwie Dockerfile, nadal istnieje we wcześniejszych warstwach. Widziałem to w praktyce zbyt wiele razy.
Docker Secrets (Swarm mode)
# Utwórz secret
echo "SuperTajneHaslo123!" | docker secret create db_password -
# Użyj w usłudze Swarm
docker service create --name api \
--secret db_password \
myapp:latest
# Wewnątrz kontenera secret jest dostępny pod:
# /run/secrets/db_password
Zewnętrzne zarządzanie sekretami
Dla środowisk produkcyjnych zdecydowanie zalecane są dedykowane systemy zarządzania sekretami:
- HashiCorp Vault — automatyczna rotacja, audyt dostępu, dynamiczne sekrety
- AWS Secrets Manager / SSM Parameter Store — integracja z ECS/EKS
- Azure Key Vault — integracja z AKS
Kluczowa zasada: sekrety powinny być montowane w runtime, ograniczone do konkretnego workloadu (nigdy cluster-wide) i automatycznie rotowane. Bez wyjątków.
Bezpieczeństwo sieci kontenerowych
Domyślna sieć Docker (docker0) pozwala na komunikację między wszystkimi kontenerami. Na deweloperskim laptopie to wygodne, ale w produkcji to proszenie się o kłopoty. Należy stosować izolowane sieci użytkownika:
# Utwórz izolowaną sieć wewnętrzną (brak dostępu do internetu)
docker network create --internal --driver bridge backend-net
# Utwórz sieć z dostępem do internetu dla frontendu
docker network create --driver bridge frontend-net
# Podłącz kontenery do odpowiednich sieci
docker run -d --name api --network backend-net myapp:latest
docker run -d --name db --network backend-net postgres:16-alpine
docker run -d --name proxy --network frontend-net --network backend-net nginx:alpine
Sieć z flagą --internal nie ma dostępu do internetu — kontenery mogą komunikować się tylko między sobą. To idealne rozwiązanie dla baz danych i usług backendowych, które nie potrzebują łączności z zewnętrznym światem.
FAQ — najczęściej zadawane pytania
Czy tryb rootless Docker nadaje się na produkcję?
Tak, jak najbardziej. W Docker Engine 28 tryb rootless jest stabilny i zalecany dla większości obciążeń produkcyjnych. Narzut na operacje sieciowe i systemu plików jest zanedbywany w porównaniu ze znaczącą redukcją ryzyka ucieczki z kontenera. Główne ograniczenie to brak obsługi kontenerów uprzywilejowanych i domyślna niemożność bindowania portów poniżej 1024 — ale w praktyce aplikacje produkcyjne rzadko tego potrzebują, bo i tak za kontenery wystawiasz reverse proxy.
Jaka jest różnica między profilem Seccomp a AppArmor w Dockerze?
Seccomp działa na poziomie wywołań systemowych jądra — kontroluje, które syscalle proces może wykonać. AppArmor działa na poziomie zasobów systemowych — kontroluje, do jakich plików, katalogów, sieci i capabilities proces ma dostęp. Są komplementarne: Seccomp ogranicza co proces może robić na poziomie jądra, a AppArmor ogranicza do czego ten proces ma dostęp. W porządnie zahartowanym kontenerze powinny działać oba jednocześnie.
Jak skanować obrazy Docker pod kątem podatności w pipeline CI/CD?
Najskuteczniejszym podejściem jest integracja Trivy w pipeline CI/CD. Dodaj krok skanowania po zbudowaniu obrazu z flagą --exit-code 1 --severity HIGH,CRITICAL, żeby automatycznie blokować buildy z krytycznymi podatnościami. Dodatkowo generuj SBOM (Software Bill of Materials) w formacie CycloneDX lub SPDX — dzięki temu szybko zidentyfikujesz ekspozycję na nowe CVE bez ponownego skanowania obrazu.
Czy Docker jest bezpieczny bez dodatkowego hartowania?
Krótka odpowiedź: nie na produkcji. Domyślna konfiguracja Dockera priorytetyzuje elastyczność i kompatybilność, nie bezpieczeństwo. Bez hartowania kontenery działają jako root, komunikują się między sobą swobodnie, mają dostęp do setek syscalli i nie stosują limitów zasobów. To akceptowalne w środowisku deweloperskim, ale na produkcji stanowi poważne ryzyko. Badania z 2025 roku potwierdzają, że ponad 65% naruszeń bezpieczeństwa kontenerów wykorzystywało właśnie domyślne, niezahartowane konfiguracje.
Jak monitorować kontenery Docker pod kątem ataków runtime?
Falco jest tu zdecydowanym liderem — projekt graduated CNCF, który monitoruje wywołania systemowe przez eBPF w czasie rzeczywistym. Wykrywa podejrzane zachowania: uruchomienie shella w kontenerze, odczyt wrażliwych plików, eskalację uprawnień i nietypowy ruch sieciowy. W połączeniu z Falcosidekick alerty trafiają natychmiast do Slacka, PagerDuty lub systemu SIEM. Alternatywą jest Tetragon od Cilium, który oferuje podobne możliwości z natywną integracją Kubernetes.
Podsumowanie — wielowarstwowa obrona kontenerów
Hartowanie kontenerów Docker to proces wielowarstwowy, nie jednorazowa konfiguracja. Każda warstwa — od zahartowanego hosta, przez tryb rootless, profile Seccomp i AppArmor, bezpieczne obrazy, skanowanie z Trivy, monitoring z Falco, po audyt CIS — dodaje kolejną barierę, którą atakujący musi pokonać. A im więcej barier, tym więcej szumu generuje atak i tym większa szansa na jego wykrycie.
Oto checklist do wdrożenia:
- Host zaktualizowany i zminimalizowany
- Demon Docker skonfigurowany z
icc: false,no-new-privileges,userns-remap - Tryb rootless włączony (Docker Engine 28+)
- Niestandardowe profile Seccomp i AppArmor dla każdego workloadu
- Kontenery działają jako non-root z
--read-onlyi--cap-drop ALL - Obrazy oparte na distroless/Alpine, budowane wieloetapowo, pinowane do digestu
- Trivy w pipeline CI/CD z polityką „zero krytycznych CVE"
- Falco monitoruje runtime z niestandardowymi regułami
- Sekrety zarządzane zewnętrznie, montowane w runtime
- Sieci kontenerowe izolowane, bazy danych w sieciach
--internal - Regularny audyt CIS Docker Benchmark v1.8.0
W następnym artykule z tej serii przejdziemy na wyższy poziom abstrakcji — hartowanie Kubernetes: zabezpieczenie API servera, etcd, RBAC, polityki sieciowe i Pod Security Standards. To naturalna kontynuacja, gdy Twoje kontenery Docker są już zahartowane i gotowe do orkiestracji.