In Evidenza
Docker Compose secrets: separare credenziali e progetto
Tips

Docker Compose secrets: separare credenziali e progetto

Chi lavora con Docker prima o poi si trova davanti alla stessa domanda: dove mettere la password del database, una chiave API o il token necessario a un servizio? Docker Compose Secrets permette di evitare che questi valori finiscano direttamente nel file compose.yaml o nelle variabili d’ambiente dei container. Non è una cassaforte magica, ma è un modo più ordinato e controllabile di distribuire le credenziali ai soli servizi che ne hanno bisogno.

La differenza conta soprattutto nei progetti che crescono. Oggi abbiamo un container applicativo e un database; domani arrivano un worker, una coda e un servizio di backup. Copiare la stessa password in cinque sezioni environment è semplice, ma aumenta le occasioni di esporla per errore. Vediamo quindi come funziona il meccanismo, come provarlo con un esempio reale e quali limiti bisogna conoscere prima di adottarlo in produzione.

Perché una password nel file Compose è un problema

Immaginiamo una configurazione molto comune:

services:
  db:
    image: mysql:8.4
    environment:
      MYSQL_ROOT_PASSWORD: "password-di-esempio"

Il container può partire, ma la credenziale è scritta in chiaro nella configurazione. Se qualcuno carica il progetto su Git, allega il file a un ticket oppure condivide uno screenshot, rischia di consegnare anche la password. Le variabili d’ambiente, inoltre, possono comparire nei dettagli di configurazione restituiti dagli strumenti di gestione e nei log prodotti da script poco prudenti.

Una soluzione diffusa consiste nello spostare i valori in un file .env. È utile per separare i parametri dal YAML e per non ripeterli, ma non trasforma automaticamente quei parametri in segreti protetti: Compose li usa spesso per valorizzare le normali variabili d’ambiente del container. Anche un .env va protetto, escluso dal repository quando contiene credenziali e gestito con attenzione nei backup.

Con Docker Compose Secrets cambia il modo in cui l’applicazione riceve il dato. Il valore viene esposto come file all’interno del container, tipicamente nel percorso /run/secrets/nome_secret, e soltanto ai servizi che lo dichiarano. Un processo che non riceve quel montaggio non trova automaticamente lo stesso file nel proprio filesystem.

Docker Compose Secrets: come funziona davvero

La configurazione ha due livelli. Nella sezione generale secrets indichiamo da dove proviene il contenuto; nella sezione del singolo servizio scegliamo quali secret montare. Non basta definire una voce in fondo al YAML per renderla disponibile a tutti i container.

Per esempio, un database può ricevere la propria password amministrativa, mentre l’applicazione web può ricevere soltanto la password di un utente con privilegi limitati. Questa separazione rispetta un principio semplice: ogni componente dovrebbe avere accesso al minimo indispensabile.

Occorre però distinguere il meccanismo da quello di Docker Swarm. Con Docker Compose locale, quando il secret proviene da un file, Docker lo rende disponibile tramite un bind mount del file presente sull’host. Il valore non viene cifrato automaticamente sul disco del server e un amministratore dell’host può comunque accedervi. Non bisogna quindi presentare Compose Secrets come un gestore centralizzato di credenziali o come un sostituto della sicurezza del sistema operativo.

La documentazione ufficiale di Docker descrive il comportamento e gli esempi aggiornati nella guida Manage secrets securely in Docker Compose.

Un esempio pratico con WordPress e MySQL

Per capire il funzionamento possiamo usare due servizi: MySQL conserva i dati e WordPress si collega al database. L’esempio è un laboratorio locale, non una configurazione di produzione pronta da pubblicare su Internet. Useremo versioni esplicite delle immagini per rendere il test più ripetibile; in un progetto reale vanno valutati aggiornamenti e pinning più rigoroso.

1. Creare i file delle password

Nella cartella del progetto creiamo una sottocartella privata per i due valori. Su Linux possiamo utilizzare questi comandi, sostituendo le stringhe dimostrative con password robuste e differenti:

mkdir -p secrets
printf '%s' 'password-root-solo-di-prova' > secrets/db_root_password.txt
printf '%s' 'password-app-solo-di-prova' > secrets/db_password.txt
chmod 700 secrets
chmod 600 secrets/*.txt

Le password qui riportate sono segnaposto non adatti alla produzione. Per i sistemi reali è preferibile usare un generatore affidabile e il canale previsto dalla propria organizzazione. Se si lavora con Docker Desktop, le regole di accesso dipendono anche dal filesystem e dall’ambiente host.

Aggiungiamo subito secrets/ al .gitignore, ma ricordiamo un dettaglio: .gitignore evita soltanto nuovi inserimenti accidentali. Non cancella una credenziale già pubblicata nella cronologia Git. Se una password reale è finita in un repository condiviso, bisogna considerarla compromessa e sostituirla.

2. Scrivere il file compose.yaml

Questa configurazione mostra l’assegnazione dei due secret:

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

  wordpress:
    image: wordpress:php8.3-apache
    restart: unless-stopped
    depends_on:
      - db
    ports:
      - "127.0.0.1:8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

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

volumes:
  db_data:

Nel file osserviamo due cose importanti. MySQL riceve entrambi i secret perché inizializza l’account amministrativo e quello applicativo. WordPress riceve soltanto db_password: non c’è motivo di consegnare al sito la password di root del database. La porta di WordPress è legata a 127.0.0.1 per limitare l’esposizione nel laboratorio; non è una configurazione di pubblicazione con TLS o reverse proxy.

La sintassi MYSQL_PASSWORD_FILE e WORDPRESS_DB_PASSWORD_FILE non è una funzionalità universale del motore Docker. È una convenzione che queste immagini supportano: invece di leggere direttamente una password da una variabile, gli script di avvio leggono il contenuto del file indicato. Se utilizziamo una nostra applicazione PHP, Python o Node.js, dovremo implementare esplicitamente questa lettura oppure verificare che il framework la preveda.

3. Avviare e controllare i container

Prima facciamo validare la configurazione e poi avviamo i servizi:

docker compose config --quiet
docker compose up -d
docker compose ps

Se tutto è configurato correttamente, WordPress sarà raggiungibile da browser sul computer locale all’indirizzo http://127.0.0.1:8080. La prima inizializzazione di MySQL richiede un po’ di tempo: depends_on nell’esempio determina l’ordine di avvio, non certifica che il database sia già pronto ad accettare connessioni. Se necessario, in un’applicazione più strutturata conviene aggiungere un healthcheck e una politica di attesa appropriata.

Possiamo verificare il montaggio dei file senza stamparne il contenuto:

docker compose exec wordpress sh -c 'test -r /run/secrets/db_password && echo "Secret disponibile"'
docker compose exec wordpress sh -c 'test ! -e /run/secrets/db_root_password && echo "Root secret non montato"'

Il primo comando controlla che WordPress possa leggere il file della propria password; il secondo verifica che non sia stato montato quello amministrativo. Evitiamo comandi come cat /run/secrets/db_password nelle dimostrazioni, negli screenshot e nelle richieste di assistenza: mostrare il valore non serve per provare che il montaggio funzioni.

Al termine del laboratorio possiamo fermare i container con docker compose down. Non aggiungiamo -v senza motivo: quell’opzione elimina anche i volumi del progetto e quindi, in questo esempio, i dati del database.

Come leggere un secret in un’applicazione personalizzata

Non tutti i software riconoscono le variabili con suffisso _FILE. Se sviluppiamo un’applicazione PHP, possiamo leggere il file direttamente, senza copiare la password in una variabile d’ambiente:

<?php
$path = '/run/secrets/db_password';

if (!is_readable($path)) {
    throw new RuntimeException('Secret del database non disponibile');
}

$password = file_get_contents($path);
if ($password === false || $password === '') {
    throw new RuntimeException('Secret del database vuoto o illeggibile');
}

// Usare $password nella configurazione della connessione.
// Non stamparla nei log e non includerla nei messaggi di errore.

Questo frammento è volutamente semplice: verifica la presenza e la leggibilità del file e segnala un errore senza rivelare la credenziale. Se il formato del valore prevede un ritorno a capo finale, bisogna gestirlo in modo coerente con il sistema che autentica; non conviene applicare un trim() indiscriminato a password arbitrarie.

Un’altra possibilità è utilizzare il percorso tramite una variabile d’ambiente, ad esempio DB_PASSWORD_FILE=/run/secrets/db_password. In quel caso la variabile contiene il nome del file, non la password. La nostra applicazione deve comunque aprire quel percorso.

Quali rischi restano anche usando i secret

Il vantaggio è reale, ma non elimina tutti gli scenari di esposizione. Chi controlla l’host Docker con privilegi sufficienti può leggere i file sorgenti; un processo compromesso all’interno di un container può leggere i secret che quel container ha ricevuto. Inoltre un’applicazione può continuare a scrivere accidentalmente password nei log, anche se le ha ottenute da /run/secrets/.

Bisogna prestare attenzione anche ai permessi. Nella specifica Compose esistono opzioni come uid, gid e mode per i secret, ma con sorgenti file Docker Compose non implementa queste impostazioni come ci si potrebbe aspettare, perché utilizza un bind mount. Non è quindi corretto promettere che mode: 0400 nel YAML risolva da solo ogni problema di accesso. Verifichiamo i permessi effettivi sul sistema host e dentro il container, soprattutto quando il processo gira come utente non privilegiato. La specifica dei servizi Compose descrive questa limitazione.

Un altro rischio deriva dai backup e dagli strumenti di sincronizzazione. Se la cartella secrets viene copiata su un archivio condiviso o caricata nel cloud senza protezione, il problema si sposta semplicemente altrove. È utile sapere dove vivono i valori originali, chi può leggerli, come vengono distribuiti e quando devono essere eliminati.

Come cambiare una password senza interrompere tutto

La rotazione delle credenziali è uno dei punti meno intuitivi. Potremmo pensare che basti modificare secrets/db_password.txt, ma non è una regola affidabile. Alcuni programmi leggono il file soltanto all’avvio; inoltre la password memorizzata da MySQL per un utente già creato non cambia automaticamente perché aggiorniamo il file che era servito all’inizializzazione.

Per un database esistente occorre quindi coordinare due interventi: aggiornare la credenziale sul server che autentica e fare in modo che il client utilizzi il nuovo valore. Possiamo programmare una finestra di manutenzione oppure, quando l’architettura lo consente, impiegare una transizione con credenziali temporaneamente distinte. In ogni caso conviene testare la procedura fuori produzione prima di intervenire.

Dopo una rotazione verifichiamo l’accesso con la nuova credenziale, la corretta ripartenza dell’applicazione e la revoca di quella precedente. Per credenziali già finite in un repository o in un log accessibile a terzi, la semplice rimozione del file non basta: la revoca è parte della correzione.

Secret durante la build: un problema diverso

C’è poi una distinzione che spesso passa inosservata. Un token necessario per scaricare una dipendenza privata mentre costruiamo l’immagine non è necessariamente utile al container quando gira. Inserirlo nel Dockerfile con ARG o copiarlo in un layer può lasciarne traccia negli artefatti finali.

Docker BuildKit offre i build secrets: li rende disponibili temporaneamente a una specifica istruzione RUN, tramite RUN --mount=type=secret. Anche Compose può dichiarare secret utilizzati nella fase di build, ma build secrets e runtime secrets non sono intercambiabili: rispondono a esigenze diverse e vanno configurati separatamente.

Per maggiori dettagli esiste la guida ufficiale Build secrets. Il principio da ricordare è semplice: il token necessario per compilare non dovrebbe finire nell’immagine finale o essere consegnato all’applicazione in esecuzione, se non le serve.

Quando bastano Compose Secrets e quando serve altro

Su una singola macchina, per piccoli servizi e ambienti gestiti direttamente, Docker Compose Secrets rappresenta un buon passo avanti rispetto alle password copiate nel YAML. Riduce la diffusione accidentale delle credenziali e rende più chiaro quali container ricevono un valore. È particolarmente utile per database, chiavi API, certificati e credenziali di integrazione.

In ambienti con molti server, più team o necessità di audit avanzato, invece, può essere opportuno valutare un sistema dedicato di gestione dei segreti. Prodotti come HashiCorp Vault e i secret manager dei principali cloud risolvono problemi più ampi: controllo centralizzato, distribuzione, politiche di accesso e, in alcuni casi, rotazione automatizzata. La scelta dipende dall’infrastruttura e non richiede di introdurre subito strumenti complessi in un semplice laboratorio.

La stessa logica del privilegio minimo è centrale anche nella guida alla sicurezza Zero Trust: concedere l’accesso solo quando serve, al componente che ne ha davvero bisogno, riduce la superficie di rischio.

Mettere in sicurezza i secret senza complicare il progetto

Se oggi le password si trovano dentro compose.yaml, il primo miglioramento può essere molto concreto: scegliere una credenziale, spostarla in un file protetto, montarla soltanto nel servizio interessato e verificare che il software sappia leggerla. Poi possiamo replicare il metodo alle altre integrazioni, senza trasformare una modifica semplice in una riscrittura dell’infrastruttura.

Docker Compose Secrets aiuta a tenere separati configurazione e dati riservati, ma funziona bene soltanto se proteggiamo anche host, repository, backup e log. Un file sotto /run/secrets/ non basta da solo a rendere sicuro un container: conta chi può leggerlo, come viene usato e quanto velocemente siamo in grado di sostituirlo quando serve.

Valuta anche...

Una piccola guida per avvicinare chi non ha ancora avuto esperienza con Docker a questo potente strumento. In queste pagine, esplorerò i concetti fondamentali e le operazioni di base, cercando di fare chiarezza su come funziona Docker e su come può semplificare il lavoro di sviluppatori e professionisti IT.

Docker - Creare, distribuire e gestire applicazioni con i container

Condividi questo articolo su: