DanLevy.net

Consigli essenziali per la sicurezza di Docker in self‑hosting

Proteggi ituoi servizi auto‑ospitati, dalla difesa al monitoraggio!

Indice

🧗‍♀️ Per i coraggiosi

Se stai auto‑ospitando servizi Docker, la sicurezza è tutta tua, dall’alto verso il basso—non c’è un provider cloud che ti protegga da scansioni di porte o configurazioni approssimative. Che tu stia lanciando applicazioni sulla tua rete domestica o noleggiando VPS da provider come Vultr, DigitalOcean, Linode, AWS, Azure o Google Cloud, dovrai chiudere le porte—e verificare di averlo fatto correttamente.

In questa guida percorreremo la sicurezza di Docker—da tecniche meno conosciute a quelle difficili da eseguire correttamente; esploreremo token canary, volumi in sola lettura, regole firewall, segmentazione e rinforzo della rete, aggiunta di proxy autenticati e molto altro.

Confronteremo anche le reti domestiche con le configurazioni cloud pubbliche e ti mostreremo come impostare un proxy con autenticazione di base usando Nginx. Alla fine avrai diverse opzioni per tenere fuori i fastidi (amici, famiglia e talvolta anche te stesso…)

È un sacco di roba! Ma gran parte è correlata, e puoi scegliere ciò che è più pertinente al tuo ambiente. 🍀

🔄 Il ballo del :latest

Mantenere le immagini aggiornate è fondamentale per la sicurezza. Tuttavia, fare affidamento su :latest può introdurre cambiamenti incompatibili o build vulnerabili senza una fase di revisione.

Il modo sicuro per aggiornare

Combina i comandi di aggiornamento con pull o build in modo da rinfrescare deliberatamente le immagini, poi riavvia durante una finestra in cui puoi notare eventuali rotture.

update-and-run.sh
#!/bin/bash
docker compose pull && \
docker compose up -d

Pinning della versione vs Latest

Scegliere la versione giusta da fissare è un equilibrio tra stabilità e sicurezza. Ecco alcune strategie comuni:

docker-compose.yml
# ...
# Pinning della versione esatta, ideale per servizi critici
image: postgres:17.2
# Pinning della patch, buono per servizi non critici
image: postgres:17.2
# Pinning della major version, perfetto per progetti hobby
image: postgres:17
# Yolo, da evitare se possibile
image: postgres:latest

Usa Dependabot o Renovate per aprire PR di aggiornamento revisionabili. Per tutto ciò che ti dispiacerebbe ricostruire alle 2 del mattino, fissa a una versione specifica o a un digest e lascia che l’automazione ti avvisi quando è il momento di passare.

Fammi sapere quali sono i tuoi strumenti preferiti per tenere aggiornate le immagini Docker!

🔐 Gestione dei segreti

Ci sono molti modi per gestire i segreti, ma una delle regole più importanti da rispettare è: non inserire mai segreti direttamente nelle tue immagini Docker né commetterli su git. È uno degli errori di sicurezza più comuni, comporta un rischio a lungo termine e richiede molto sforzo per essere corretto.

Conservare i segreti in modo sicuro è un argomento ampio con molte opzioni, dai file .env, Docker secrets, 1Password/Bitwarden, o un gestore di segreti come HashiCorp Vault o AWS Secrets Manager.

Dovrai scegliere il livello “giusto” di sforzo e sicurezza per il tuo caso d’uso.

Genera Segreti Forti

Ecco un piccolo script per generare nuovi segreti da inserire in un file .env:

generate-secrets.sh
#!/bin/bash
generate_secret() {
local length=${1:-30}
local generate_length=$((length + 4))
openssl rand -base64 "$generate_length" | tr -d '+=/\n' | cut -c1-"$length"
}
[ -f .env ] && { echo ".env file already exists!"; exit 1; }
cat > .env << EOL
POSTGRES_PASSWORD=$(generate_secret)
JWT_SECRET=$(generate_secret 64)
SESSION_KEY=$(generate_secret 24)
REDIS_PASSWORD=$(generate_secret 20)
UNSAFE_PLACEHOLDER=__WARNING_REPLACE_RANDOM_TEXT__
EOL
echo "New .env file generated with secure random values!"

Token Canary

Canary Tokens sono un ottimo metodo per rilevare se i tuoi segreti sono stati compromessi (e utilizzati). Funzionano come una trappola che puoi aggiungere a qualsiasi file sensibile, URL o token.

Considera di posizionarli accanto ai segreti che ti preoccupano davvero: file .env, variabili CI, gestori di password, cartelle di backup e credenziali cloud. Non trasformare tutto in uno spettacolo; metti le trappole dove un vero aggressore o un futuro te stesso potrebbero toccarle.

Esistono molti tipi di “token” canary tra cui scegliere, da token AWS, numeri di carta di credito falsi, file Excel e Word, file Kubeconfig, credenziali VPN, fino a dump SQL che possono contenere una trappola!

Buone Pratiche per i Token Canary

Aggiornamento da .env a Keychain macOS

Per gli utenti Mac, una delle soluzioni più semplici è usare Keychain.

Ecco un modo rapido per automatizzare il caricamento dei segreti dal portachiavi di macOS, con supporto a TouchID e una sicurezza leggermente superiore rispetto ai file .env.

Original credit: Brian Hetfield and Jan Schaumann.

Comandi di supportoPersisti segreti nell'ambienteUsa segreti per comando
keychain-secrets.sh
### Funzioni per impostare e recuperare variabili d'ambiente dal portachiavi OSX ###
### Adattato da: https://www.netmeister.org/blog/keychain-passwords.html e
Original credit: [Brian Hetfield](https://gist.github.com/bmhatfield/f613c10e360b4f27033761bbee4404fd) and [Jan Schaumann](https://www.netmeister.org/).
# Uso: get-keychain-secret VAR_ENV_SEGRETO
function get-keychain-secret () {
security find-generic-password -w -a ${USER} -D "environment variable" -s "${1}"
}
# Uso: set-keychain-secret VAR_ENV_SEGRETO
# Ti verrà chiesto di inserire il valore segreto!
function set-keychain-secret () {
[ -n "$1" ] || print "Manca il nome della variabile d'ambiente"
# richiedi all'utente il segreto
echo -n "Inserisci il segreto per ${1}"
read secret
[ -n "$secret" ] || return 1
( [ -n "$1" ] || [ -n "$secret" ] ) || return 1
security add-generic-password -U -a ${USER} -D "environment variable" -s "${1}" -w "${secret}"
}
~/code/app/.env-secrets.sh
source ~/keychain-secrets.sh
# Carica le variabili d'ambiente nella shell corrente
export AWS_ACCESS_KEY_ID=$(get-keychain-secret AWS_ACCESS_KEY_ID);
export AWS_SECRET_ACCESS_KEY=$(get-keychain-secret AWS_SECRET_ACCESS_KEY);
# Nota: se un attaccante può eseguire `env` nella tua shell, questi segreti potrebbero essere esposti!
~/code/app/scripts/env-run.sh
#!/usr/bin/env bash
source ~/keychain-secrets.sh
# Specifica tutti i segreti per questo progetto
AWS_ACCESS_KEY_ID=$(get-keychain-secret AWS_ACCESS_KEY_ID) \
AWS_SECRET_ACCESS_KEY=$(get-keychain-secret AWS_SECRET_ACCESS_KEY) \
"$@"
# Nota: usare un wrapper shell aiuta a impedire che i segreti rimangano
# nell'ambiente. Ed è sicuro da includere nel repository.
# Uso:
# ./scripts/env-run.sh docker compose up -d
# ./scripts/env-run.sh docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY ...

🌐 Pericolo di rete

Reti personalizzate e porte interne

Isolare correttamente i servizi con le reti Docker è un modo importante per ridurre la superficie di attacco.

Fai attenzione a non aprire buchi nella tua rete! Un singolo port forwarding configurato male può avere conseguenze molto gravi.

Per impostazione predefinita, i servizi su una LAN privata non saranno esposti a Internet – devi esplicitamente inoltrare le porte dal tuo router.

Docker su LAN

Che tu sia uno sviluppatore che esegue server di sviluppo in locale, o che stia auto‑ospitando servizi dalla tua rete domestica, le supposizioni sul modello di rete di Docker possono causare problemi.

Gli sviluppatori spesso rimangono sorpresi nel constatare che i metodi “tradizionali” per mettere in sicurezza i server Linux (iptables, restrizioni sulle opzioni sysctl tcp/ip) possono fallire silenziosamente sugli host Docker! Questo è particolarmente vero quando si auto‑ospita o si opera su una tipica rete domestica. (Per chi è in fondo: questo può consentire l’accesso ai container di sviluppo sul tuo MacBook!!!)

⚠️ Avviso #1: Le porte pubblicate da Docker possono aggirare le regole del firewall che pensavi stessero proteggendo l’host, soprattutto con UFW su Ubuntu/Debian. Questo non rende inutili tutte le regole del firewall, ma significa che “UFW dice deny” non è una prova. Vedi issue #690: Docker bypasses ufw firewall rules.

⚠️ Avviso #2: Il binding delle porte a indirizzi IP locali (es., -p 127.0.0.1:8080:80) è il valore predefinito corretto, ma le versioni del Docker Engine precedenti alla 28.0.0 presentavano casi in cui host sulla stessa rete L2 potevano ancora raggiungere le porte pubblicate su localhost. Docker documenta la limitazione nella sua guida al port publishing, e l’abitudine di verificare con nmap mostrata sotto resta importante.

Se sei sorpreso di scoprirlo, lo stesso!

Il binding a IP locali è ancora una buona pratica e ha un impatto significativo negli ambienti cloud gestiti e in reti appositamente configurate.

Esempio Docker Compose

Ecco un file docker-compose.yml di esempio che associa il servizio app a 127.0.0.1:8080 e collega entrambi i container alla rete personalizzata backend.

docker-compose.yml
networks:
backend:
services:
app:
networks:
- backend
ports:
# Bind to localhost if possible
- "127.0.0.1:8080:8080"
# ... other settings
database:
image: postgres:17.1
# No ports needed; accessible inside backend network.
networks:
- backend

Best Practice di Rete

🛡️ Controlli di Accesso

I controlli di accesso sono una parte cruciale per mettere in sicurezza i tuoi servizi Docker. Questo include limitare le capacità e i permessi dei container, restringere l’accesso al socket Docker, e altro ancora.

Limitare le Capacità del Container

Un’altra pratica solida di controllo di accesso è limitare le capacità dei tuoi container. Questo riduce il raggio d’azione di diverse minacce, dall’escalation di privilegi al dirottamento del traffico. Non è un campo di forza, ma elimina permessi che la maggior parte dei container non ha mai bisogno.

Cosa sono le capacità? Permessi o abilità nominati definiti dal kernel Linux. (La pagina man capabilities contiene l’elenco completo.) Includono cose come CAP_CHOWN (cambia il proprietario di un file), CAP_NET_ADMIN (configura interfacce di rete), CAP_KILL (termina qualsiasi processo) e molte altre.

I due modi per determinare le capacità necessarie sono:

  1. Prova ed errore: Questo metodo più lento ma efficace ti fa partire senza capacità, poi aggiungerle una alla volta finché l’app funziona.
  2. Cerca lavoro preesistente: Cerca “project-name cap_drop Dockerfile” o “project-name cap_drop docker-compose.yml” per vedere se altri hanno già fatto il lavoro per te. Un LLM può suggerire un punto di partenza, ma trattalo come un’ipotesi finché non testi il container e leggi la documentazione dell’immagine.

Best Practice sulle Capacità

Example: Drop/Limit Capabilities
services:
database:
image: postgres:17.1
networks: [ db-network ]
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_READ_SEARCH
- FOWNER
- SETGID
- SETUID
db-admin:
image: dpage/pgadmin4:4.1
networks: [ db-network ]
ports:
- "8081:80"
# ... other settings
networks:
db-network:

Ora i tuoi servizi possono comunicare tra loro tramite la rete db-network. Docker Compose creerà automaticamente quella rete.

Usa l’opzione --external/external: per unirti a una rete preesistente. Omettela per creare una nuova rete.

Accesso al Socket Docker

⚠️ Attenzione: docker.sock è praticamente l’accesso amministrativo dell’host

⚠️ L’opzione :ro non influisce sull’I/O inviato attraverso il socket!

Assicura solo che il percorso del socket sia montato in sola lettura. Le chiamate API inviate tramite quel socket possono comunque creare container, montare percorsi dell’host e fare altre cose molto interessanti che probabilmente non intendevi delegare.

Buona Pratica per il Socket

Blocco per Paese!

A volte utile, ma non costituisce una vera barriera di sicurezza.

Parliamo dell’entità geopolitica, non della musica…

Se ospiti applicazioni principalmente per la tua famiglia e i tuoi amici locali, puoi bloccare il traffico proveniente da paesi da cui non ti aspetti visite. Oppure consentire solo il traffico dai paesi che ti aspetti. Riduce il rumore; non ferma VPN, proxy, botnet o chiunque sia sufficientemente paziente.

Dai un’occhiata a questo script per bloccare tutto il traffico dalla Cina:

block-china.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone | \
while read line; do ufw deny from $line to any; done

Allo stesso modo, puoi consentire solo il traffico dagli Stati Uniti:

allow-usa.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/us.zone | \
while read line; do ufw allow from $line to any; done

Hardening CloudFlare Proxy Host

Se il tuo server domestico è protetto dietro un IP di CloudFlare (proxy), puoi limitare l’accesso solo agli IP di CloudFlare e alla tua rete locale.

È un po’ simile al blocco per Paese sopra, ma con un controllo molto più restrittivo.

whitelist-ingress-from-cloudflare.sh
ufw default deny incoming # Blocca tutto il traffico in ingresso!!!
ufw default allow outgoing # Consenti tutto il traffico in uscita
ufw allow ssh # Consenti SSH
# Consenti l’accesso per la subnet locale (idealmente una DMZ/VLAN dedicata per i servizi ospitati)
ufw allow from 10.0.0.0/8 to any port 443
# Consenti gli IP di CloudFlare
curl -fsSL https://www.cloudflare.com/ips-v4 | \
while read line; do ufw allow from $line to any port 443; done
# Aggiungi supporto IPv6
# curl -fsSL https://www.cloudflare.com/ips-v6 | \
# while read line; do ufw allow from $line to any port 443; done

Per testare modifiche basate sulla geolocalizzazione può tornare utile una VPN con endpoint nel paese desiderato. Vedi più dettagli nella sezione Monitoring & Verification.

Sicurezza a livello di applicazione

Una volta che la tua rete e host sono stati rinforzati, potresti scoprire che c’è ancora altro da fare.

Ora dobbiamo considerare il livello “applicazione” dei nostri servizi stessi.

Il database ha una password valida? Il container automatizza HTTPS/certificati? L’app include un’autenticazione integrata? Ci sono limiti su quali email possono registrarsi? Esistono credenziali predefinite o variabili d’ambiente da modificare?

L’unico modo per sapere è controllare. In questo caso, parti dal README e da altri file chiave come docker-compose.yml, Dockerfile e .env.*. Fai lo stesso sia per il progetto sia, idealmente, per i servizi di supporto (ad esempio Postgres, Redis, ecc.).

Reverse Proxy

Un altro livello di difesa è l’autenticazione di base. Non usarla senza HTTPS. Per i servizi legacy, mettere l’autenticazione di base davanti a una rotta amministrativa è spesso sufficiente a bloccare richieste casuali e crawler non autenticati che tentano di accedere direttamente.

/etc/nginx/conf.d/secure-admin.conf
location /admin {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://internal_admin:80;
proxy_set_header X-Real-IP $remote_addr;
}

Genera le credenziali:

Terminal window
htpasswd -c /etc/nginx/.htpasswd admin

Con un proxy che usa l’autenticazione di base, gli aggressori devono superare un ostacolo aggiuntivo—username e password—prima di raggiungere il tuo servizio interno.

Un’alternativa è utilizzare un servizio come Traefik o Caddy che può automatizzare HTTPS e l’autenticazione di base per te.

Se vuoi gestire molti domini e servizi tramite un’interfaccia grafica, ti consiglio Nginx Proxy Manager.

🔍 Monitoraggio e Verifica

Questo è il passo più importante e più trascurato. Puoi avere il firewall migliore, la rete migliore e le migliori pratiche, ma se non verifichi, non hai alcuna certezza che funzionino.

In più, conoscere solo qualche comando – o sapere dove trovarli – può fare la differenza nel prevenire una violazione. Sentirsi come un hacker è solo un bonus. (Per dettagli ed esempi, vai direttamente alla sezione Monitoraggio e Verifica.)

Non fidarti, verifica due volte

Controlla le tue porte

⚠️ IMPORTANTE: non scansionare host che non possiedi.

Che tu sia su una rete domestica o su un VPS, vorrai sapere quali porte sono aperte verso il mondo.

Ci sono 2 modi per farlo:

Testare al di fuori della tua rete

Ti servirà il tuo attuale IP pubblico, facilmente ottenibile con servizi come ifconfig.me: curl https://ifconfig.me. Oppure controllalo nella dashboard del tuo provider.

Get Public IP
curl -fsSL https://ifconfig.me
# --> CURRENT PUBLIC IP

Una volta ottenuto l’IP pubblico, devi connetterti a una rete esterna. Puoi usare il computer di un amico, un hotspot telefonico/5G, o un server dedicato.

nmap External Scan
target_host="$(curl -fsSL https://ifconfig.me)"
#Nota: Assicurati che `target_host` sia l'IP desiderato
# Scansiona porte specifiche:
nmap -A -p 80,443,8080 --open --reason $target_host
# Le 100 porte più comuni:
nmap -A --top-ports 100 --open --reason $target_host
# Tutte le porte
nmap -A -p1-65535 --open --reason $target_host

Test nella tua rete

Esercitati con nmap, scansiona la tua rete locale o uno dei tuoi server, controlla il router, la stampante, il frigorifero intelligente.

Esempi di comandi di scansione

Terminal window
# Scansiona il tuo localhost per tutte le porte aperte
nmap -sT localhost
# Scansiona l'IP privato della tua macchina per i servizi
nmap -sV 192.168.1.10
# Trova i dettagli dei servizi nella tua rete
nmap -sn 192.168.0.0/24
nmap -sn 10.0.0.0/24
# O su un Docker 172.18.0.1/16
nmap -sn 172.18.0.1/16
nmap Scan
% nmap -A --open --reason 192.168.0.87
Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-06 13:51 MST
Nmap scan report for dev02.local (192.168.0.87)
Host is up, received syn-ack (0.0067s latency).
Not shown: 995 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|_ 256 {FINGERPRINT} (ED25519)
80/tcp open http syn-ack Caddy httpd
|_http-server-header: Caddy
|_http-title: Dev02.DanLevy.net
443/tcp open ssl/https syn-ack
|_http-title: Dev02.DanLevy.net
1234/tcp open http syn-ack Node.js Express framework
|_http-cors: GET POST PUT DELETE PATCH
|_http-title: Dev02.DanLevy.net (application/json; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.36 seconds

Visualizza le porte aperte

Familiarizzati con lsof – è disponibile su macOS e Linux. Mostra lo stato della rete a livello granulare e l’attività del disco.

lsof Commands
# Monitora una porta specifica
sudo lsof -i:80 -Pn
# Monitora le connessioni ESTABLISHED
sudo lsof -i -Pn | grep ESTABLISHED
# Visualizza le porte LISTEN
sudo lsof -i -Pn | grep LISTEN
# per vedere i nomi di rete invece degli indirizzi IP (può essere molto lento a causa delle risoluzioni DNS inverse)
sudo lsof -i -P | grep LISTEN
# Monitorare tutte le connessioni di rete
sudo watch -n1 "lsof -i -Pn"

Output di esempio

nmap scan for listeners

Monitoraggio dei file

Per identificare quali processi stanno consumando più larghezza di banda del disco, puoi usare iotop:

Terminal window
sudo iotop

Per osservare le modifiche a singoli file, usa inotifywait su Linux o fswatch su macOS:

Questo può risultare utile per rilevare comportamenti non autorizzati o anomali a livello di cartella o dell’intero sistema.

Terminal window
# Monitorare tutte le modifiche ai file in una directory
sudo inotifywait -m /path/to/directory

Su macOS puoi usare fswatch:

Installa con brew install fswatch

Terminal window
fswatch -r /path/to/directory

⏰ Suggerimenti spesso trascurati

  1. Rate Limiting per i tentativi di autenticazione e per qualsiasi altro endpoint critico. Che sia tramite il modulo limit_req di Nginx o fail2ban per l’accesso SSH, limitare i brute‑force è probabilmente una buona idea. Dico probabilmente perché, nell’era di IPv6 e botnet a basso costo, le cose non sono più come una volta.

  2. Usa volumi in sola lettura dove possibile:

    services:
    webapp:
    volumes:
    - ./config:/config:ro

    In combinazione con altre best practice (utenti non root, permessi di cartella minimi), l’opzione di mount :ro aggiunge una salvaguardia contro modifiche accidentali e alcuni tentativi di scrittura dall’interno del container. Non protegge l’host da un processo che già possiede privilegi più ampi.

  3. Esegui audit regolari sull’accesso ai container.
    Se un container non ha bisogno di un secret, di una porta o di un mount, rimuovili!

  4. Fai attenzione al Wi‑Fi
    Sono sicuro che non darai mai la password del tuo Wi‑Fi a sconosciuti, vero? Beh, tranne qualche amico… Ok, forse anche alla famiglia. Non sai mai quali app usano e quali potrebbero condividere SSID e password con il mondo.

Rete domestica vs. Provider pubblico vs. Tunneling

  1. Isolamento virtuale/DMZ: per i server domestici, posizionali su una VLAN o DMZ separata, se possibile. Questo mantiene i tuoi dispositivi interni fuori dalla portata di eventuali compromissioni provenienti dal server.

    • Usa un router o una VLAN dedicata per il tuo server domestico.
    • Usa una rete Wi‑Fi separata per il tuo server domestico.
    • Usa una subnet separata per il tuo server domestico.
  2. Provider Cloud: Hetzner, Vultr, DigitalOcean, Linode, AWS, Azure e Google Cloud offrono funzionalità firewall differenti.

    • Alcuni provider e servizi bloccano le porte per impostazione predefinita. Alcuni offrono opt‑in o componenti aggiuntivi. Controlla la documentazione del tuo provider.
    • Molti provider mettono a disposizione servizi avanzati di monitoraggio e rilevamento delle minacce.
  3. VPN e Tunneling: Valuta l’uso di un’opzione tipo VPN o di un servizio di tunneling per collegare in modo sicuro i servizi attraverso Internet senza esporli al pubblico.

    • TailScale, ngrok, ZeroTier.
    • WireGuard, OpenVPN.

🚀 Checklist di Produzione

📚 Ulteriori Letture

Grazie

Un ringraziamento a qualche Redditor attento:

Grazie per aver letto! Spero che questa guida ti sia stata utile. Se hai domande o suggerimenti, contattami sui miei social qui sotto, oppure clicca sul collegamento Edit on GitHub per aprire una PR! ❤️