Gdzie kończą zmienne środowiskowe kontenera
Uruchamiasz kontener poleceniem -e DB_PASSWORD=hunter2 i sprawdzasz go docker inspect:
| |
| |
Hasło leży tam jawnym tekstem, czytelne dla każdego z dostępem do socketa Docker, dla docker exec api env i dla każdego procesu działającego w kontenerze. To nie jest błąd — dokładnie do tego służą zmienne środowiskowe. Problem pojawia się, gdy używasz ich do wartości, które nigdy nie powinny być aż tak widoczne.
Gdzie ustawić zmienną środowiskową Docker: build time czy run time
Cztery mechanizmy, trzy różne cykle życia. To, gdzie ustawisz zmienną, decyduje, jak długo żyje i kto może ją odczytać:
| Mechanizm | Ustawiana gdzie | Trafia do obrazu? | Widoczna po docker inspect? |
|---|---|---|---|
ARG w Dockerfile | Tylko przy budowaniu | Nie (chyba że skopiowana do ENV) | Nie, ale zapisana w historii builda |
ENV w Dockerfile | Przy budowaniu | Tak, na stałe w każdej kolejnej warstwie | Tak |
-e / --env w docker run | Przy starcie kontenera | Nie | Tak |
--env-file w docker run | Przy starcie kontenera | Nie | Tak |
W czasie działania przekazujesz je pojedynczo albo z pliku:
| |
.env.production wygląda tak:
| |
--env-file staje się lepszym wyborem, gdy masz więcej niż dwie-trzy zmienne — komenda startowa zostaje czytelna, a wartości siedzą w jednym miejscu, które możesz porównać diffem.
Zmienne środowiskowe w Docker Compose: environment, env_file i .env
Compose daje trzy miejsca na zmienne, a dwa z nich to pliki o niemal identycznej nazwie. .env i env_file: robią zupełnie różne rzeczy.
.envw katalogu głównym projektu jest czytany przez sam Compose, żeby podstawić placeholdery${ZMIENNA}wcompose.yaml. Nigdy nie trafia do kontenera, chyba że dodatkowo odwołasz się do niego wenvironment:.env_file:wskazuje pliki, których zawartość trafia do środowiska kontenera, dokładnie tak jak--env-filewdocker run.environment:ustawia zmienne bezpośrednio w pliku Compose, inline.
| |
Gdy zmienna jest zdefiniowana w więcej niż jednym miejscu, Compose rozstrzyga to w takiej kolejności, od najwyższego priorytetu: -e przekazane do docker compose run, potem environment:, potem env_file:, na końcu ENV już zaszyte w obrazie. Jeśli NODE_ENV pojawia się zarówno w environment:, jak i w .env.api, wygrywa wartość z environment:.
Dlaczego zmienne środowiskowe to złe miejsce na secrets
Żaden z powyższych mechanizmów nie ukrywa wartości przed kimś, kto ma dostęp do kontenera albo hosta:
docker inspectwypisuje każdą zmienną w czasie działania, jak pokazano wyżej.docker exec <kontener> envwypisuje je od środka.- Proces potomny dziedziczy całe środowisko, łącznie z debuggerem, crash reporterem albo zależnością, której nie sprawdziłeś.
- Wszystko, co przy starcie zrzuca swoje środowisko, wysyła wartość do twojego agregatora logów, a zaskakująco wiele frameworków robi dokładnie to, gdy włączone jest debug logging.
- Zmienne środowiskowe ustawione przy budowaniu przez
ENVsą trwałe:docker history --no-trunc my-apipokazuje dokładną wartość, która zostaje w obrazie na każdym registry, do którego go wypchniesz.
Nic z tego nie wymaga, żeby atakujący przejął kontener. Wystarczy dostęp, który wiele osób już ma: socket Docker, logi CI, registry obrazów.
Jak działają secrets Docker: wartość montowana jako plik
Przestań przekazywać dane uwierzytelniające kontenerowi jako zmienne środowiskowe i montuj je jako pliki. Compose robi to samodzielnie, bez potrzeby Swarm:
| |
Compose montuje db_password.txt pod /run/secrets/db_password w kontenerze, tylko do odczytu i nigdzie indziej — nie pojawia się nigdy w docker inspect, w docker exec ... env ani w obrazie. Sufiks _FILE to konwencja, którą już wspierają oficjalne obrazy Postgresa, MySQL i MongoDB: przy starcie skrypt entrypoint czyta plik zamiast oczekiwać wartości wprost.
Jeśli twoja aplikacja nie obsługuje tej konwencji, wczytaj plik sam przy starcie — jedna linia w większości języków, na przykład open('/run/secrets/db_password').read().strip() w Pythonie.
W klastrze Swarm odpowiedni secret tworzy i dystrybuuje orkiestrator zamiast lokalnego pliku:
| |
Swarm przechowuje secret zaszyfrowany w spoczynku i w tranzycie oraz montuje go w pamięciowym systemie plików na każdej replice — nigdy nie trafia do zapisywalnej warstwy kontenera.
Trzymanie secrets poza obrazem przy budowaniu
ARG i ENV w Dockerfile mają ten sam problem, tylko krok wcześniej: wartość przekazana jako argument builda zostaje zapisana w historii builda obrazu, nawet jeśli nigdy nie zamienisz jej w ENV.
| |
| |
Ta komenda docker history go znajdzie. Token znika z finalnego systemu plików, jeśli nie zrobisz COPY pliku konfiguracyjnego dalej, ale zostaje trwale czytelny w metadanych obrazu, które wędrują razem z nim na każde registry.
Flaga --secret z BuildKit omija ten problem, montując wartość na czas jednego kroku RUN jako plik w pamięci, który nigdy nie staje się warstwą:
| |
| |
npm_token istnieje tylko na czas tej instrukcji RUN i nie jest zapisywany nigdzie, gdzie docker history mógłby go zobaczyć.
Kiedy zmienna środowiskowa, a kiedy secret
| Sytuacja | Użyj |
|---|---|
| Konfiguracja bez znaczenia dla bezpieczeństwa (port, log level, feature flag) | -e, --env-file albo environment:/env_file: z Compose |
| Hasło do bazy danych, klucz API, klucz TLS w czasie działania | Secret Docker, zamontowany jako plik |
Token uwierzytelniający potrzebny tylko podczas docker build | --secret z BuildKit z RUN --mount=type=secret |
| Wartość celowo zaszyta w obrazie (wersja aplikacji, commit builda) | ARG skopiowany do ENV — to w porządku, to nie jest secret |
Jedno pytanie rozstrzyga sprawę: przeszkadzałoby ci, gdyby ta wartość wyciekła? Jeśli tak, nie idzie przez -e, --env-file, environment: ani przez ARG/ENV w Dockerfile. Idzie przez secret zamontowany jako plik. Ustawienia, które twoja aplikacja bez problemu wypisuje we własnych logach, zostają zwykłymi zmiennymi środowiskowymi — i tak łatwiej je nadpisać per środowisko.
Sprawdź to teraz, u siebie. Uruchom docker inspect --format '{{json .Config.Env}}' na kontenerach, które masz teraz uruchomione, i przeczytaj wynik tak, jakby to był diff pull requesta. Wszystko, czego nie chciałbyś zobaczyć zrecenzowanego publicznie, powinno trafić do bloku secrets: w twoim pliku Compose, ze zmienną _FILE w miejscu hasła. Jeśli plik ma przetrwać dłużej niż kontener, zamiast pochodzić z kontekstu builda, umieść go na woluminie Docker.