+39 3662317539/+34 620899163
info@simonecosci.com

Docker Compose: secrets per le password, fuori dalle variabili d’ambiente

Sviluppo Siti Web a Tenerife

Docker Compose: secrets per le password, fuori dalle variabili d’ambiente

Il docker-compose.yml tipico di un progetto WordPress o Laravel ha un blocco così:

environment:
  MYSQL_ROOT_PASSWORD: una-password-vera
  MYSQL_PASSWORD: un-altra-password-vera

Funziona, ed è proprio il problema: funziona così bene che nessuno lo cambia più. La documentazione di Docker lo dice senza giri di parole: «If you’re injecting passwords and API keys as environment variables, you risk unintentional information exposure». E subito dopo il motivo concreto: le variabili d’ambiente «can also be printed in logs when debugging errors without your knowledge». Basta uno stack trace che stampa l’ambiente, o un env lanciato per capire un errore, e la password del database è in un file di log che nessuno ha pensato di proteggere.

Compose ha un’alternativa pensata apposta: i secrets.

Come funzionano

Un secret, nella definizione di Docker, è «any piece of data, such as a password, certificate, or API key». Invece di arrivare al container come variabile, arriva come file: «Secrets are mounted as a file in /run/secrets/<secret_name> inside the container».

Si dichiarano in due punti. In alto, a livello di file, si dice da dove viene il valore. Nel servizio si dice chi lo può leggere:

services:
  web:
    image: example/webapp
    secrets:
      - db-password

secrets:
  db-password:
    file: ./db_password.txt

Il secondo punto non è una formalità. La reference di Compose è esplicita: dichiarare un secret in alto non dà accesso a nessuno, ogni servizio deve chiederlo nel proprio blocco secrets. È il vantaggio vero rispetto a environment, che di solito si copia uguale in tre servizi: qui il container del web server non vede la password di root del database, se non gliela date voi.

La convenzione _FILE

Resta un dettaglio: l’applicazione deve sapere che la password sta in un file. Per molte immagini ufficiali non bisogna scrivere niente, perché seguono una convenzione: aggiungendo _FILE al nome della variabile, il valore viene letto dal file indicato. Docker la descrive come «a convention used by some images», e l’esempio della documentazione mette insieme proprio MySQL e WordPress:

services:
  db:
    image: mysql:latest
    volumes:
      - db_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
      MYSQL_DATABASE: wordpress
      MYSQL_USER: wordpress
      MYSQL_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_root_password
      - db_password

  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: db_password.txt
  db_root_password:
    file: db_root_password.txt

volumes:
  db_data:

Notate la distribuzione: db riceve entrambe le password, wordpress solo quella che gli serve. Il nome del database e l’utente restano variabili normali, perché non sono segreti.

Una precisazione importante: non tutte le variabili supportano _FILE, nemmeno nelle immagini che la adottano. La pagina dell’immagine ufficiale MySQL su Docker Hub elenca quali: MYSQL_ROOT_PASSWORD, MYSQL_ROOT_HOST, MYSQL_DATABASE, MYSQL_USER e MYSQL_PASSWORD. Per qualunque altra immagine, il controllo da fare è lo stesso: leggere la sua pagina, prima di dare per scontato che QUALCOSA_FILE venga letto. Se non viene letto, il container parte lo stesso, con un valore vuoto o di default, e l’errore lo scoprite dopo.

Nel vostro codice

Per un’applicazione vostra, PHP o altro, _FILE non esiste finché non lo scrivete. La lettura è una riga:

$password = trim(file_get_contents('/run/secrets/db_password'));

Il trim() non è decorativo: un file scritto con un editor finisce quasi sempre con un a capo, e quell’a capo diventerebbe parte della password.

Permessi: la trappola dei campi ignorati

La sintassi lunga permette di scegliere proprietario e permessi del file:

secrets:
  - source: api-token
    target: token.txt
    uid: "103"
    gid: "103"
    mode: 0440

Il default è «world-readable permissions (mode 0444)»: dentro il container chiunque può leggere il file. Ma prima di affidarvi a uid, gid e mode, leggete la nota della reference di Compose: sono supportati solo «when the source of the secret is environment». Con un secret da file, «Compose uses a bind-mount under the hood which doesn’t allow uid remapping, and these attributes are silently ignored».

Silently: nessun errore, nessun avviso. Scrivete mode: 0400, il container parte, e il file resta leggibile da tutti. Se i permessi contano, verificateli dentro il container con un ls -l /run/secrets/ invece di fidarvi del YAML.

Cosa evitare

  • Committare i file dei secrets. db_password.txt accanto al docker-compose.yml è comodo, e finisce nel repository alla prima git add .. Va nel .gitignore il giorno in cui lo create, non dopo.
  • Pensare che sia cifratura. Un secret di Compose è un file montato nel container. Toglie la password dall’ambiente e dai log, e permette di decidere quale servizio la vede. Non la protegge da chi ha accesso alla macchina, dove il file sorgente sta in chiaro.
  • Usare environment: come sorgente con docker stack deploy. La reference lo dice chiaramente: i secrets da variabile d’ambiente «are not supported when deploying with docker stack deploy». Su Swarm servono file o external.
  • Container Windows. «Secrets are supported on Linux containers only.»
  • Lasciare la vecchia variabile accanto alla nuova. Se aggiungete MYSQL_PASSWORD_FILE e dimenticate MYSQL_PASSWORD, la password in chiaro è ancora lì, e il lavoro è a metà.

In pratica

Su un progetto esistente il passaggio richiede poco: un file per ogni password, il blocco secrets in fondo al Compose, _FILE al posto della variabile dove l’immagine lo supporta, una riga di lettura nel vostro codice dove non lo supporta. Poi un controllo: docker compose config per vedere la configurazione risolta, e un env dentro il container per verificare che la password non compaia più.

Se avete uno stack Docker da mettere in ordine, o da portare in produzione senza password sparse nei file di configurazione, scriveteci.

Fonti: Docker Docs, Secrets in Compose; Compose file reference, secrets top-level e services; immagine ufficiale MySQL su Docker Hub.