Hartowanie kontenerów Docker w 2026 — rootless, Seccomp, AppArmor i audyt CIS

Praktyczny przewodnik po hartowaniu kontenerów Docker na produkcji w 2026. Tryb rootless (Docker Engine 28), profile Seccomp i AppArmor, skanowanie z Trivy, monitoring Falco i audyt CIS Benchmark v1.8.0 — z działającymi przykładami.

Hartowanie Docker 2026: Rootless & CIS Benchmark

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 slirp4netns nie 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.sh prawidł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-rootUSER nonroot lub 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:

  1. Konfiguracja hosta — parametry jądra, reguły audytu, osobna partycja dla /var/lib/docker
  2. Konfiguracja demona Docker — TLS, autoryzacja, logowanie, user namespaces
  3. Uprawnienia plików konfiguracyjnychdaemon.json, certyfikaty TLS, socket
  4. Obrazy i pliki Dockerfile — bazowe obrazy, użytkownicy, sekrety, HEALTHCHECK
  5. Runtime kontenerów — capabilities, seccomp, AppArmor, limity zasobów
  6. 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-only i --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.

O Autorze Tobias Lindqvist

Tobias spent six years on Canonical's kernel team in the LTS hardening group, mostly working on Livepatch tooling and the long tail of CVE backports nobody wants to do. He left in 2025 to consult independently with Nordic infrastructure customers running large Ubuntu fleets, where most of the work is AppArmor profiles, unattended-upgrades that don't break things, and explaining what kernel.unprivileged_userns_clone actually does. He co-maintains a small open-source tool for diffing kernel config across distributions and contributes occasionally to the linux-hardening mailing list. His side project is a Yocto-based minimal image for industrial gateways that boots in under four seconds with full secure boot. Tobias writes mostly about kernel-level security primitives - namespaces, seccomp, LSMs - with the assumption that you've already read the man page.