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.

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 Editorial Team

Our team of expert writers and editors.