Linux

Linux Hardening Docker nie jest piaskownicą. 15 błędów, które regularnie widzę na produkcyjnych serwerach

Linux Hardening: Docker nie jest piaskownicą. 15 błędów, które regularnie widzę na produkcyjnych serwerach

Jest taki moment, który powtarza się zaskakująco często. Administrator spędza godzinę na zabezpieczaniu Ubuntu – wyłącza logowanie roota przez SSH, konfiguruje UFW, sprawdza AppArmor i instaluje Fail2Ban. Potem wpisuje jedno polecenie:

Bash

docker run -d nginx

Kilka minut później serwer działa. Strona odpowiada. Wszystko wygląda poprawnie.

Tylko że właśnie otworzyliśmy zupełnie nową warstwę systemu, która rządzi się własnymi zasadami bezpieczeństwa.

Największy problem z Dockerem polega na tym, że przez lata narosło wokół niego kilka niebezpiecznych mitów. Najbardziej popularny brzmi: „kontener jest odizolowany od hosta, więc nic złego się nie stanie”.

To wygodne uproszczenie.

I właśnie ono prowadzi do większości błędów.

Pierwsza rzecz, którą sprawdzam na obcym serwerze

Kiedy dostaję dostęp do serwera z Dockerem, nie zaczynam od docker ps.

Najpierw interesuje mnie, jak Docker został uruchomiony.

Bash

docker info

To jedno polecenie potrafi zdradzić więcej niż kilkadziesiąt minut przeglądania konfiguracji.

Szukam przede wszystkim:

  • wersji Dockera,
  • sterownika storage,
  • włączonych mechanizmów bezpieczeństwa,
  • rootless mode,
  • AppArmor,
  • seccomp.

Dopiero później patrzę na kontenery.

Dlaczego?

Bo źle skonfigurowany host pozostaje źle skonfigurowany niezależnie od liczby kontenerów.

Błąd numer jeden: --privileged

Jeżeli miałbym wskazać jedną opcję, której nadużywanie widuję najczęściej, byłoby to właśnie:

Bash

docker run --privileged

Działa.

Rozwiązuje problemy.

Przestają pojawiać się komunikaty o braku uprawnień.

I właśnie dlatego jest tak niebezpieczna.

Wyobraź sobie różnicę.

Normalny kontener:

Host
 │
 └── Kontener
      ├── ograniczone capability
      ├── seccomp
      ├── AppArmor
      └── namespaces

Kontener uruchomiony z --privileged:

Host
 │
 └── Kontener
      ├── prawie pełne capability
      ├── szeroki dostęp do urządzeń
      └── znacznie słabsze granice izolacji

To nie jest niewielka zmiana.

To zmiana filozofii bezpieczeństwa.

Jeżeli jakaś aplikacja wymaga --privileged, najpierw chcę zrozumieć dlaczego.

Root w kontenerze nie oznacza tego samego

Tutaj pojawia się kolejny mit.

„Przecież to tylko root w kontenerze.”

To zależy.

Domyślnie proces wewnątrz kontenera często działa jako UID 0.

Sprawdźmy.

Bash

docker exec app id

Możemy zobaczyć:

uid=0(root)

To nie oznacza automatycznie pełnego roota hosta.

Ale oznacza, że warto ograniczyć to tam, gdzie aplikacja tego nie potrzebuje.

Znacznie lepiej wygląda:

dockerfile

USER app

To jedna linijka.

A potrafi znacząco zmniejszyć skutki kompromitacji aplikacji.

Rootless Docker zmienia sposób myślenia

Jedną z najciekawszych zmian ostatnich lat jest rozwój Rootless Docker.

Tutaj Docker sam działa bez pełnych uprawnień roota.

Schemat wygląda inaczej.

Standard

root
 │
Docker
 │
Kontenery


Rootless

Użytkownik
 │
Docker
 │
Kontenery

To nie rozwiązuje wszystkich problemów.

Ale zmniejsza konsekwencje niektórych scenariuszy kompromitacji.

Porty publikowane na oślep

To klasyczny przypadek.

Bash

docker run -p 5432:5432 postgres

Działa.

Ale czy PostgreSQL naprawdę powinien być publicznie dostępny?

Często wystarczy:

Internet
    │
    ▼
Nginx
    │
    ▼
Aplikacja
    │
    ▼
PostgreSQL

Baza nie potrzebuje własnego publicznego portu.

Im mniej wystawionych usług, tym prostszy model bezpieczeństwa.

Sieć Dockera to nie magia

Docker tworzy własne sieci.

Wiele osób nigdy do nich nie zagląda.

A warto.

Bash

docker network ls

Potem:

Bash

docker network inspect bridge

Nagle okazuje się, że kilka aplikacji widzi się wzajemnie.

Czasem zupełnie niepotrzebnie.

Lepiej budować oddzielne sieci dla różnych usług.

Przykład.

Frontend
    │
    ▼
frontend_net

Backend
    │
    ▼
backend_net

Database
    │
    ▼
db_net

To prostsze niż późniejsze tłumaczenie, dlaczego kontener testowy widział bazę produkcyjną.

Obrazy nie powinny być wieczne

To bardzo częsty problem.

Ktoś buduje obraz.

Uruchamia.

I zapomina.

Miesiąc później.

Rok później.

Dwa lata później.

Kontener nadal działa.

Problem polega na tym, że podatności nie przestają istnieć tylko dlatego, że aplikacja działa poprawnie.

Regularne przebudowy obrazów są równie ważne jak aktualizacje systemu.

 

latest nie jest wersją

Nigdy nie lubiłem:

YAML

image: nginx:latest

Dlaczego?

Bo za miesiąc może oznaczać coś innego.

Znacznie bezpieczniej:

YAML

image: nginx:1.27

Albo jeszcze lepiej – konkretny digest obrazu.

Przewidywalność jest częścią bezpieczeństwa.

Capability są ważniejsze niż większość osób myśli

Linux posiada mechanizm capabilities.

Docker pozwala nimi zarządzać.

Zamiast dawać wszystko:

ALL

można zostawić tylko to, czego aplikacja naprawdę potrzebuje.

Przykładowo.

YAML

cap_drop:
  - ALL

cap_add:
  - NET_BIND_SERVICE

To znacznie bardziej świadome podejście.

Seccomp działa, nawet jeśli go nie widzisz

Jednym z najbardziej niedocenianych mechanizmów Dockera jest seccomp.

Większość administratorów nigdy go nie dotyka.

A mimo to korzysta z niego codziennie.

Domyślny profil ogranicza wiele potencjalnie niebezpiecznych wywołań systemowych.

Można sprawdzić konfigurację:

Bash

docker inspect app

i poszukać informacji o profilu bezpieczeństwa.

To kolejna warstwa, która działa „po cichu”.

AppArmor nie kończy się na hoście

Jeżeli host korzysta z AppArmor, kontenery również mogą z niego korzystać.

To bardzo ciekawe połączenie.

Host ogranicza proces.

Docker uruchamia proces.

AppArmor nadal kontroluje zachowanie procesu.

To pokazuje, że warstwy bezpieczeństwa nie konkurują ze sobą.

One się uzupełniają.

Wolumeny potrafią zdradzić więcej niż aplikacja

To kolejna pułapka.

YAML

volumes:
  - /:/host

Tak.

Ludzie naprawdę to robią.

Jeżeli kontener nie potrzebuje całego systemu plików, nie powinien go widzieć.

Lepiej:

/app/data

niż:

/

Minimalizacja dostępu pozostaje jedną z najważniejszych zasad hardeningu.

Tajemnice nie powinny mieszkać w obrazie

Czasami widzę:

dockerfile

ENV PASSWORD=SuperSecret123

To nie jest miejsce na hasła.

Znacznie lepiej korzystać z:

  • Docker Secrets,
  • zmiennych środowiskowych dostarczanych podczas wdrożenia,
  • zewnętrznych systemów zarządzania sekretami.

Obraz powinien być możliwie neutralny.

Logi potrafią zapełnić dysk

To problem bardziej praktyczny niż spektakularny.

Kontener działa.

Loguje wszystko.

Po kilku tygodniach:

No space left on device

Dlatego ograniczam rozmiar logów.

Przykład.

YAML

logging:
  options:
    max-size: "10m"
    max-file: "5"

To drobiazg.

Ale właśnie takie drobiazgi odróżniają środowisko testowe od produkcyjnego.

Docker Compose to dokumentacja

Coraz częściej traktuję docker-compose.yml jako dokument architektury.

Dobry plik Compose pozwala zrozumieć:

  • które usługi istnieją,
  • które porty są publiczne,
  • które sieci są prywatne,
  • które wolumeny przechowują dane.

To znacznie lepsze niż odtwarzanie konfiguracji z historii poleceń.

Aktualizacja nie kończy się na docker pull

Wielu administratorów wykonuje:

Bash

docker pull nginx

i uznaje temat za zamknięty.

Nowy obraz nie zastąpi działającego kontenera.

Trzeba go jeszcze wdrożyć.

To oczywiste.

A jednak często o tym zapominamy.

Jak wygląda mój minimalny standard produkcyjny?

Jeżeli uruchamiam nową usługę, chcę zobaczyć mniej więcej taki obraz.

                 INTERNET
                     |
                     ▼
                 NGINX
                     |
             frontend_net
                     |
                     ▼
              APPLICATION
                     |
             backend_net
                     |
                     ▼
              POSTGRESQL

Dodatkowo:

  • brak --privileged,
  • użytkownik niebędący rootem,
  • ograniczone capability,
  • domyślny seccomp,
  • AppArmor,
  • osobne sieci,
  • wersjonowane obrazy,
  • kontrolowane logi.

To nie jest maksymalnie restrykcyjna konfiguracja.

To konfiguracja, którą da się utrzymać przez lata.

Najważniejsza lekcja z Dockera

Największy błąd nie polega na używaniu złego polecenia.

Polega na traktowaniu kontenera jak magicznej piaskownicy.

Docker jest świetnym narzędziem.

Ale bezpieczeństwo nie wynika z samego faktu uruchomienia aplikacji w kontenerze.

Bezpieczeństwo wynika z decyzji administratora:

  • jakie capability zostawić,
  • jakie porty wystawić,
  • jakie wolumeny udostępnić,
  • jakiego użytkownika uruchomić,
  • jak zbudować sieć,
  • jak zarządzać sekretami.

Kontener nie zastępuje hardeningu.

Jest kolejną warstwą, którą również trzeba świadomie zabezpieczyć.

Polecane wpisy
Wirtualizacja a Konteneryzacja: Podobieństwa i Różnice
Wirtualizacja a Konteneryzacja: Podobieństwa i Różnice

📚 Wirtualizacja a Konteneryzacja: Podobieństwa i Różnice Wirtualizacja i konteneryzacja to dwa kluczowe podejścia wykorzystywane we współczesnym zarządzaniu środowiskami IT. Czytaj dalej

Jak zainstalować Javę na Ubuntu
Jak zainstalować Javę na Ubuntu

Jak zainstalować Javę na Ubuntu – Kompletny przewodnik Java jest jednym z najpopularniejszych języków programowania na świecie, wykorzystywanym do tworzenia Czytaj dalej

Marek "Netbe" Lampart Inżynier informatyki Marek Lampart to doświadczony inżynier informatyki z ponad 25-letnim stażem w zawodzie. Specjalizuje się w systemach Windows i Linux, bezpieczeństwie IT, cyberbezpieczeństwie, administracji serwerami oraz diagnostyce i optymalizacji systemów. Na netbe.pl publikuje praktyczne poradniki, analizy i instrukcje krok po kroku, pomagając administratorom, specjalistom IT oraz zaawansowanym użytkownikom rozwiązywać realne problemy techniczne.