Dove finiscono le variabili d’ambiente di un container
Avvia un container con -e DB_PASSWORD=hunter2 ed esegui docker inspect sul container appena avviato:
| |
| |
La password resta lì in chiaro, leggibile da chiunque abbia accesso al socket Docker, da docker exec api env e da qualsiasi processo in esecuzione dentro il container. Non è un bug: è esattamente a cosa servono le variabili d’ambiente. Il problema è usarle per valori che non dovrebbero mai essere così visibili.
Dove impostare una variabile d’ambiente Docker: build time vs run time
Quattro meccanismi, tre cicli di vita diversi. Dove imposti una variabile decide quanto dura e chi può rileggerla:
| Meccanismo | Impostata dove | Finisce nell’immagine? | Visibile dopo docker inspect? |
|---|---|---|---|
ARG nel Dockerfile | Solo in build | No (a meno che non venga copiata in ENV) | No, ma resta nella build history |
ENV nel Dockerfile | In build | Sì, incorporata in ogni layer successivo | Sì |
-e / --env su docker run | All’avvio del container | No | Sì |
--env-file su docker run | All’avvio del container | No | Sì |
A run time le passi una alla volta, o da un file:
| |
.env.production è fatto così:
| |
--env-file è la scelta migliore appena superi due o tre variabili: tiene il comando di avvio leggibile e i valori in un unico posto che puoi confrontare con un diff.
Variabili d’ambiente in Docker Compose: environment, env_file e .env
Compose ha tre posti dove mettere le variabili, e due sono file con un nome quasi identico. .env e env_file: fanno lavori completamente diversi.
.envnella root del progetto viene letto da Compose stesso, per sostituire i placeholder${VARIABILE}dentrocompose.yaml. Non arriva mai al container a meno che tu non lo richiami anche sottoenvironment:.env_file:elenca file il cui contenuto viene iniettato nell’ambiente del container, esattamente come--env-filesudocker run.environment:imposta le variabili direttamente nel file Compose, in linea.
| |
Quando una variabile è definita in piĂą di un posto, Compose la risolve in quest’ordine, dal piĂą prioritario: un -e passato a docker compose run, poi environment:, poi env_file:, poi il valore ENV giĂ incorporato nell’immagine. Se NODE_ENV compare sia in environment: sia in .env.api, vince il valore in environment:.
PerchĂ© le variabili d’ambiente non sono il posto giusto per i secrets
Nessuno dei meccanismi sopra nasconde un valore a chi ha accesso al container o all’host:
docker inspectstampa ogni variabile a run time, come visto sopra.docker exec <container> envle stampa dall’interno.- Un processo figlio eredita l’intero ambiente, incluso un debugger, un crash reporter o una dipendenza che non hai controllato.
- Qualsiasi cosa che stampa il proprio ambiente all’avvio manda il valore nel tuo log aggregator, e un numero sorprendente di framework lo fa proprio quando il debug logging è attivo.
- Le variabili d’ambiente impostate in build con
ENVsono permanenti:docker history --no-trunc my-apimostra il valore esatto, e quel valore resta nell’immagine su ogni registry su cui la pubblichi.
Niente di tutto questo richiede che un attaccante comprometta il container. Basta l’accesso che moltissime persone hanno giĂ : il socket Docker, i log della CI, il registry delle immagini.
Come funzionano i secrets Docker: il valore viene montato come file
Smetti di consegnare le credenziali al container come variabili d’ambiente e montale come file. Compose lo fa da solo, senza bisogno di Swarm:
| |
Compose monta db_password.txt su /run/secrets/db_password dentro il container, in sola lettura e da nessun’altra parte: non compare mai in docker inspect, in docker exec ... env, nĂ© nell’immagine. Il suffisso _FILE è una convenzione giĂ supportata dalle immagini ufficiali di Postgres, MySQL e MongoDB: all’avvio, lo script di entrypoint legge il file invece di aspettarsi il valore diretto.
Se la tua applicazione non supporta questa convenzione, leggi il file da sola all’avvio: una riga nella maggior parte dei linguaggi, ad esempio open('/run/secrets/db_password').read().strip() in Python.
Su un cluster Swarm, il secret equivalente viene creato e distribuito dall’orchestratore invece che da un file locale:
| |
Swarm conserva il secret cifrato a riposo e in transito, e lo monta in un filesystem in memoria su ogni replica: non viene mai scritto sul layer scrivibile del container.
Tenere i secrets fuori dall’immagine in build
ARG e ENV in un Dockerfile hanno lo stesso problema un passo prima: un valore passato come build argument resta registrato nella build history dell’immagine anche se non lo trasformi mai in una ENV.
| |
| |
Quel comando docker history lo trova. Il token sparisce dal filesystem finale se non fai COPY del file di configurazione in avanti, ma resta leggibile per sempre nei metadati dell’immagine, che viaggiano con essa su ogni registry.
Il flag --secret di BuildKit evita il problema montando il valore in un singolo step RUN come file in memoria che non diventa mai un layer:
| |
| |
npm_token esiste solo per la durata di quell’istruzione RUN e non viene registrato in nessun posto che docker history possa vedere.
Quando usare una variabile d’ambiente e quando un secret
| Situazione | Usa |
|---|---|
| Configurazione non sensibile (porta, log level, feature flag) | -e, --env-file, o environment:/env_file: di Compose |
| Password database, API key, chiave TLS a run time | Un secret Docker, montato come file |
Token di autenticazione necessario solo durante docker build | --secret di BuildKit con RUN --mount=type=secret |
| Valore incorporato nell’immagine di proposito (versione app, commit di build) | ARG copiato in ENV: va bene, non è un secret |
Una sola domanda decide tutto: ti dispiacerebbe se questo valore trapelasse? Se sì, non passa da -e, --env-file, environment: o un ARG/ENV nel Dockerfile. Passa da un secret montato come file. Le impostazioni che la tua app è felice di stampare nei propri log restano variabili d’ambiente normali, piĂą facili da sovrascrivere per ambiente.
Quindi vai a controllare. Esegui docker inspect --format '{{json .Config.Env}}' sui container che hai in esecuzione adesso, e leggi l’output come se fosse il diff di una pull request. Qualsiasi cosa lì dentro che non vorresti veder revisionata in pubblico va spostata in un blocco secrets: nel tuo file Compose, con una variabile _FILE al posto della password. Se il file deve sopravvivere al container invece di arrivare dal build context, mettilo su un volume Docker.