Docker compose e il firewall
Modulo 9: Docker e docker compose · Tempo di lettura: 12 minuti
Obiettivo della lezione: Dopo questa lezione Lei sarà in grado di descrivere servizi in un file compose, di avviarli, aggiornarli e fermarli, e saprà spiegare perché le porte pubblicate da Docker aggirano ufw e come evitarlo.
Concetti chiave: compose.yaml, services, image, ports, volumes, environment, .env, restart, networks, depends_on, healthcheck, docker compose up -d, pull, logs, down, version: obsoleto, Docker e ufw, DOCKER-USER, pubblicazione su 127.0.0.1, segreti, docker.sock
Perché compose?
Con docker run (lezione 9.2) Lei avvia un solo container con una lunga riga di opzioni. Un'applicazione reale è spesso composta da più container: un programma, un database, un reverse proxy. Ricordare quelle righe e digitarle di nuovo dopo un riavvio o un aggiornamento prima o poi porta a un errore.
Docker Compose descrive tutti i container di un'applicazione in un unico file: compose.yaml. Lei mette quel file in una cartella dedicata, per esempio /srv/status/compose.yaml, e gestisce tutto con docker compose. Il file è al tempo stesso la Sua documentazione: che cosa è in esecuzione, quale image, quali porte, dove si trovano i dati. Inserisca quella cartella nel Suo backup (lezione 8.1) e potrà ripristinare l'applicazione su un nuovo server in pochi minuti.
Il plug-in si chiama docker compose (con uno spazio, versione 2). Il vecchio programma separato docker-compose (con il trattino) è stato sostituito; lo si trova ancora nelle vecchie guide. Anche il campo version: "3.8" in cima ai file più vecchi è obsoleto: Compose usa sempre lo schema più recente e con version: mostra soltanto un avviso.
Leggere un file compose
Un esempio con un programma e un database PostgreSQL:
# /srv/notities/compose.yaml
services:
app:
image: ghcr.io/example/notities:1.4
restart: unless-stopped
ports:
- "127.0.0.1:8081:8080"
environment:
DB_HOST: db
DB_USER: notities
DB_PASSWORD: ${DB_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: notities
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notities"]
interval: 10s
retries: 5
volumes:
db-data:Passo per passo:
- services: ogni servizio diventa un container. Il nome (
app,db) è anche il nome host con cui i container si trovano a vicenda. Compose crea una rete propria per l'applicazione, quindiappsi collega al database sudb:5432. - image: sempre con un tag di versione (lezione 9.1).
- restart: che cosa fa Docker quando il container si ferma o il server si riavvia.
unless-stoppedlo riavvia, a meno che Lei non lo abbia fermato di proposito. Senza una policy di restart la Sua applicazione non torna attiva dopo un riavvio (lezione 4.2). - ports: solo l'app pubblica una porta, e soltanto su
127.0.0.1. Il database non pubblica nulla: l'app lo raggiunge tramite la rete interna. Un database conports: - "5432:5432"è uno degli errori più frequenti (vedere più avanti). - environment: impostazioni per il programma. La password non è scritta in chiaro, ma come
${DB_PASSWORD}. - volumes: il volume con nome
db-dataconserva il database quando il container viene sostituito. - depends_on con healthcheck: l'app parte solo quando il database è davvero pronto, non semplicemente avviato.
Segreti in un file .env
Compose legge variabili come ${DB_PASSWORD} da un file .env nella stessa cartella:
# /srv/notities/.env
DB_PASSWORD=Kies-hier-een-lang-willekeurig-wachtwoordanna@web1:/srv/notities$ sudo chmod 600 .envIn questo modo la password non compare nel file compose che magari conserva in Git o incolla in un forum per chiedere aiuto. Aggiunga .env a .gitignore. Generi la password con un gestore di password oppure con openssl rand -base64 24.
Uso quotidiano
Tutti i comandi vanno eseguiti nella cartella che contiene il file compose:
anna@web1:/srv/notities$ sudo docker compose config # verificare il file, variabili compilate
anna@web1:/srv/notities$ sudo docker compose up -d # avviare tutto (o aggiornare in base al file)
anna@web1:/srv/notities$ sudo docker compose ps # stato per servizio
anna@web1:/srv/notities$ sudo docker compose logs -f app
anna@web1:/srv/notities$ sudo docker compose pull # scaricare le nuove immagini
anna@web1:/srv/notities$ sudo docker compose up -d # sostituire i container con le nuove immagini
anna@web1:/srv/notities$ sudo docker compose down # arrestare e rimuovere i containerdown lascia intatti i volumi. down -v rimuove anche i volumi dell'applicazione, e con essi il Suo database. Lo digiti solo se vuole davvero ricominciare da zero.
docker compose config è l'equivalente di nginx -t e sshd -t: mostra come Compose interpreta il Suo file, con tutte le variabili sostituite, prima che Lei avvii qualcosa. Faccia attenzione a non incollare l'output da nessuna parte, perché contiene le password.
Docker aggira ufw
Arriviamo al punto di sicurezza più importante di questo modulo. Nella lezione 5.2 Lei ha impostato ufw su "rifiuta tutto, tranne SSH, 80 e 443". Ci si aspetterebbe che un container con ports: - "5432:5432" non sia quindi raggiungibile. Non è così.
La documentazione di Docker lo dice esplicitamente: Docker e ufw usano le regole del firewall in modi tra loro incompatibili. Quando Lei pubblica una porta, Docker inserisce direttamente delle regole nel firewall del kernel. Queste regole inoltrano il traffico al container (traduzione degli indirizzi, DNAT) prima che vengano applicate le regole di ufw. Il pacchetto quindi aggira ufw. ufw status non mostra nulla di particolare, mentre la porta è raggiungibile da tutto internet. Lo stesso vale per firewalld.
Inoltre, senza un indirizzo, Docker pubblica su 0.0.0.0 e [::], quindi sia su IPv4 sia su IPv6 (lezione 9.1).
Tre modi per farlo correttamente, dal più semplice al più complesso:
- Non pubblicare nulla che non debba essere pubblico. Database, cache e servizi interni non ricevono
ports:. I container della stessa applicazione compose si raggiungono tramite la rete interna. - Pubblicare su loopback.
"127.0.0.1:8081:8080". Così solo il server stesso può accedervi, e Lei mette davanti un reverse proxy (nginx o Caddy, modulo 7) in ascolto sulla 443. Docker può anche avere questo comportamento come predefinito: un'impostazione del daemon fa sì che tutte le porte pubblicate senza indirizzo finiscano su127.0.0.1. - Regole nella catena DOCKER-USER. Docker crea una catena vuota
DOCKER-USERche viene attraversata prima delle sue regole. Lì Lei può inserire regole proprie, per esempio "solo questo indirizzo IP può accedere alle porte pubblicate". È un lavoro avanzato con iptables o nftables, e facile da sbagliare. Per la maggior parte dei piccoli server bastano i punti 1 e 2.
Verifichi sempre dall'esterno, come nella lezione 5.1: nc -vz -w 3 <ip> 5432 dal Suo portatile, oppure con il controllo del corso degli esercizi Z5.1 e Z10.4.
Altre due cose da non fare
- Non montare
/var/run/docker.sockin un container, a meno che non sappia esattamente perché. Un container con accesso al socket di Docker può avviare a sua volta dei container, anche uno con l'intero disco dell'host (lezione 9.2). Alcuni strumenti (dashboard, aggiornatori automatici) lo richiedono; in pratica significa dare a quel programma i privilegi di root. - Non usare
privileged: trueonetwork_mode: hostsolo perché lo dice una guida. Entrambi eliminano in gran parte l'isolamento del container. Li usi solo se lo sviluppatore spiega perché sono necessari.
Riepilogo
- Un file compose (
compose.yaml) descrive tutti i container di un'applicazione; è al tempo stesso documentazione e va incluso nel Suo backup. - Usi tag di versione, una policy di restart, volumi con nome, un healthcheck dove possibile, e i segreti in un
.envcon permessi 600. docker compose config,up -d,ps,logs,pull,down;down -vrimuove anche i volumi.- Le porte pubblicate aggirano ufw e firewalld. Pubblichi solo ciò che deve essere pubblico, altrimenti su
127.0.0.1con un reverse proxy davanti. - Niente docker.sock,
privilegedonetwork_mode: hostsenza un buon motivo.
Quiz
Esercizio
Esercizio Z9.3: Verificare un file compose. Nel file di esempio compose-nakijken.txt si trova (come file di testo) il file compose di un piccolo negozio online con un'app, un database, una cache e uno strumento di amministrazione. Contiene sei errori trattati in questa lezione o nelle lezioni 9.1 e 9.2. Indichi per ogni blocco di righe numerato se è corretto o sbagliato, e per ogni errore scelga la correzione giusta. Il programma confronta le risposte con la chiave fissa. Se ha un Suo file compose, lo controlli poi secondo gli stessi punti.
Per approfondire
- Documentazione di Docker, How Compose works (compose.yaml): https://docs.docker.com/compose/intro/compose-application-model/
- Documentazione di Docker, Version and name top-level elements: https://docs.docker.com/reference/compose-file/version-and-name/
- Documentazione di Docker, Packet filtering and firewalls (Docker and ufw): https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Documentazione di Docker, Port publishing and mapping: https://docs.docker.com/engine/network/port-publishing/
- Documentazione di Docker, Interpolation (variabili e .env): https://docs.docker.com/reference/compose-file/interpolation/
