Vai al contenuto
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after a top-up of $10 or more. Until Nov 30.

How it works

Come creare una rete Docker isolata per il tuo lab

Crea una rete Docker senza accesso a internet, avvia OWASP Juice Shop al suo interno e pubblica una copia per il browser solo su 127.0.0.1. Verifica ogni passo.

Base12 min di letturaDi NotHoly

Un lab è sicuro quanto lo sono le strade per entrarci e per uscirne. Se la tua app vulnerabile può raggiungere internet, può contattare posti che non volevi toccare. Se ascolta su ogni interfaccia della tua macchina, chiunque sia sullo stesso Wi-Fi può raggiungerla. Docker ti dà il controllo di entrambe le strade con pochi comandi. Questa guida usa OWASP Juice Shop come bersaglio e li percorre uno per uno.

Creerai una rete senza via d'uscita, ci avvierai Juice Shop, chiamerai l'app da un secondo container e dimostrerai che la rete non raggiunge internet. Poi eseguirai una copia separata per il browser, pubblicata solo sull'interfaccia di loopback, così niente sulla tua rete locale può vederla. L'isolamento è il primo controllo di sicurezza di qualsiasi lab, e ogni esercizio successivo, dalla scansione agli esercizi di sfruttamento, dipende da questo.

Se non hai ancora eseguito Juice Shop, comincia da OWASP Juice Shop con Docker. Questo articolo dà per note le basi sui container di quella guida.

Perché l'isolamento viene prima

Un lab di sicurezza esegue software fatto per essere rotto. Juice Shop ha falle volute, ed è proprio il suo scopo. Il rischio non è l'app in sé, ma quello che succede quando una falla, una configurazione sbagliata o un errore di battitura danno all'app, o a uno strumento che le lanci contro, una strada verso qualcos'altro.

Tre casi mostrano come può andare storto:

  • L'app ha una funzione che scarica un URL, e un payload che stai provando le fa richiedere un host che non volevi. Senza controllo sul traffico in uscita, la richiesta lascia la tua macchina.
  • Un container di strumenti esegue un comando contro un indirizzo scritto male. Senza un confine di rete, quell'indirizzo potrebbe essere del tuo router, del tuo datore di lavoro o del server di uno sconosciuto.
  • Pubblichi l'app con un semplice -p 3000:3000. Docker allora accetta connessioni su ogni interfaccia dell'host, quindi gli altri dispositivi della tua rete possono raggiungere l'app vulnerabile.

L'isolamento copre ciascun caso. Una rete interna non ha rotte verso l'esterno. Una porta legata al loopback non ha strade attraverso la LAN. Una rete dedicata tiene i container del lab separati dai tuoi altri progetti. Nessuno di questi passi è difficile. Sono facili da dimenticare quando vai di fretta, per questo la guida te li fa verificare uno a uno.

Le reti Docker in cinque minuti

Ti servono tre idee perché i comandi abbiano senso.

Reti bridge

Docker collega i container a reti virtuali. Il driver predefinito è bridge. Ogni container su una rete bridge ottiene un proprio indirizzo IP su una subnet privata. Su Linux il bridge sta sull'host stesso. Su Windows e macOS, Docker Desktop esegue il motore dentro una macchina virtuale Linux, quindi il bridge sta dentro quella VM.

I container sul bridge predefinito si raggiungono per indirizzo IP ma non per nome. Una rete bridge che crei tu con docker network create risolve i nomi dei container via DNS, con un resolver integrato in Docker. Per questo il container di strumenti di questa guida può chiamare http://juice:3000 invece di dipendere da un IP che cambia dopo un riavvio.

Reti interne

Quando crei una rete con --internal, Docker ne limita l'accesso esterno. La documentazione di Docker lo descrive così: i container su una rete interna possono parlare tra loro, ma non con ciò che sta all'esterno, internet compresa. Lo vedrai al passo 4, quando la risoluzione di un host esterno fallisce.

Porte pubblicate

Per impostazione predefinita, la porta di un container non è pubblicata verso altre macchine. L'opzione -p o --publish cambia le cose. Mappa un indirizzo e una porta dell'host su una porta del container, e l'indirizzo che scegli decide quali interfacce accettano connessioni.

-p 3000:3000 ascolta su tutte le interfacce dell'host, compreso il tuo indirizzo LAN. -p 127.0.0.1:3000:3000 ascolta solo sul loopback, quindi possono connettersi solo i programmi sulla stessa macchina. La documentazione di Docker sulla pubblicazione delle porte mostra esattamente questa forma. La differenza è un prefisso, e pesa.

Cosa costruirai

Il lab ha cinque parti:

  • juicenet, una rete interna senza rotte verso l'esterno.
  • juice, Juice Shop collegato solo a juicenet, senza porte pubblicate.
  • Un container usa e getta curlimages/curl su juicenet che chiama juice per nome.
  • Lo stesso container usa e getta che prova a raggiungere example.com, cosa che dovrebbe fallire.
  • juice-local, una seconda istanza di Juice Shop pubblicata su 127.0.0.1:3000 per il tuo browser.

Il container juice è quello che isoli. juice-local è una copia di comodo per navigare. Gira sulla rete predefinita, quindi può raggiungere internet. Tienilo presente e non inserire nell'app credenziali reali o dati personali.

Prima di iniziare

Ti serve:

  • Docker installato e il motore in esecuzione. docker info dovrebbe stampare i dettagli del server senza errori. Sulla macchina usata per questa guida, Docker Engine 29.8.2 girava su Windows 11 tramite Docker Desktop.
  • La porta 3000 libera sull'host per la copia da browser.
  • Una shell. Gli esempi usano Git Bash su Windows 11. I comandi funzionano allo stesso modo nelle shell di macOS e Linux. In PowerShell, le stringhe di formato -w tra apici singoli richiedono un quoting diverso.

Controlla i nomi prima di sceglierli. Altri progetti sulla tua macchina potrebbero già usare nomi come juice o juicenet. Esegui prima questo:

bash
docker ps -a --format '{{.Names}}'
docker network ls --format '{{.Name}}'

Se uno dei due elenchi mostra juice, juice-local o juicenet, scegli altri nomi o rimuovi prima quelli vecchi. Non cancellare niente che non hai creato tu.

Controlla anche l'immagine che stai per eseguire. Juice Shop pubblica le etichette di versione sulla sua immagine:

bash
docker image inspect bkimminich/juice-shop --format '{{index .Config.Labels "org.opencontainers.image.version"}}'

Questo comando fallisce finché l'immagine non è in locale, perché è il primo docker run a scaricarla. Sulla macchina usata per questa guida, l'immagine ha riportato la versione 20.3.0. La tua potrebbe riportarne un'altra, quindi annotala.

Passo 1: crea la rete interna

bash
docker network create --internal juicenet

Docker stampa l'ID della nuova rete. Il tuo sarà diverso. Questo è ciò che ha stampato sulla macchina di prova:

text
44a865311b8b0df7808f81dccd11f4b50ca38800d7a38a6d17bc80fc104dadf9

Conferma che il flag ha avuto effetto:

bash
docker network inspect juicenet --format '{{.Internal}}'
text
true

Se l'output è false, hai creato la rete senza --internal. Rimuovila con docker network rm juicenet e ricreala. Non andare avanti finché non vedi true, perché tutte le prove successive danno per attivo il flag.

Passo 2: avvia Juice Shop sulla rete interna

bash
docker run -d --name juice --network juicenet bkimminich/juice-shop

Ogni opzione ha un compito. -d esegue il container in background. --name juice gli dà un nome DNS che gli altri container su juicenet possono risolvere. --network juicenet lo collega solo a quella rete interna. Non c'è -p, quindi il container non pubblica porte verso l'host.

Docker stampa l'ID del container:

text
d9f182bbbec2dc8db7dcde02f3e94b054274b6cf8addb1a5f994e9ee23ddebf8

L'app all'interno ascolta sulla porta 3000. Juice Shop ha bisogno di un po' di tempo per partire dopo che il container è su, quindi non provare subito. Usa un ciclo che controlla se un container di strumenti riceve un HTTP 200:

bash
for i in $(seq 1 30); do
  s=$(docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000)
  echo "try $i: $s"
  [ "$s" = "200" ] && break
  sleep 3
done

Mentre l'app parte, il ciclo stampa 000, che significa che curl non è riuscito a connettersi. Sulla macchina di prova ha stampato 000 quattro volte e poi 200 al quinto tentativo. A te potrebbero servirne di più o di meno. Il ciclo si arrende dopo 30 tentativi. Se non vedi mai un 200, cerca errori in docker logs juice.

Passo 3: raggiungi l'app da un container di strumenti

bash
docker run --rm --network juicenet curlimages/curl -s -o /dev/null -w '%{http_code}\n' http://juice:3000
text
200

Leggi ogni parte. --rm cancella il container di strumenti quando esce. --network juicenet mette lo strumento sulla stessa rete interna dell'app. curlimages/curl è una piccola immagine che contiene curl. http://juice:3000 usa il nome del container, che Docker risolve dentro juicenet. Il flag -s nasconde la barra di avanzamento, -o /dev/null scarta il corpo della risposta e -w stampa solo il codice di stato.

Questo è lo schema che riuserai. Uno scanner, un proxy o uno script si unisce alla rete del lab e punta al container per nome. Sul tuo host non serve nessuna rotta verso l'app.

Il tag dell'immagine qui non è fissato, quindi Docker scarica quello che oggi è latest. Per un lab che pensi di ripetere, fissa il tag dopo averlo provato.

Passo 4: dimostra che la rete non ha internet

Una prova riuscita significa qualcosa solo se le sta accanto una prova fallita. Il passo 3 ha mostrato che il container di strumenti raggiunge l'app, quindi la rete funziona. Ora controlla l'altra direzione:

bash
docker run --rm --network juicenet curlimages/curl -sS -m 5 https://example.com; echo "exit=$?"
text
curl: (28) Resolving timed out after 5000 milliseconds
exit=28

Il flag -m 5 limita la richiesta a cinque secondi. -sS nasconde la barra di avanzamento ma lascia visibili gli errori. Il codice di uscita 28 è il codice di timeout di curl. L'errore avviene nella risoluzione dei nomi: la ricerca di example.com non riceve mai risposta, quindi curl non apre mai una connessione. È quello che ti aspetti da una rete senza rotte verso l'esterno.

Questo è il risultato che vuoi. Il lab raggiunge i propri container e non raggiunge l'esterno. Salva l'output dei passi 3 e 4 negli appunti del lab con la data e la versione di Docker. Quella traccia ti aiuta quando torni e qualcosa è cambiato.

Passo 5: esegui una copia solo su loopback per il browser

Il tuo browser non può risolvere il nome juice, e il container juice non pubblica porte, quindi dall'host non lo raggiungi. Per navigare, avvia una seconda copia e pubblicala sul loopback:

bash
docker run -d --name juice-local -p 127.0.0.1:3000:3000 bkimminich/juice-shop

L'opzione -p 127.0.0.1:3000:3000 mappa la porta 3000 dell'interfaccia di loopback sulla porta 3000 dentro il container. Questa copia usa il bridge predefinito, non juicenet, quindi non è isolata da internet. È un compromesso voluto per una copia da navigazione. Se ti serve qualcosa di più restrittivo, devi progettare e provare una configurazione separata.

La copia impiega un momento a partire. Usa lo stesso tipo di ciclo di prima, puntato sull'host:

bash
for i in $(seq 1 30); do
  s=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/)
  echo "try $i: $s"
  [ "$s" = "200" ] && break
  sleep 3
done

Sulla macchina di prova la copia ha risposto 200 al decimo tentativo. Ora conferma dove è legata la porta:

bash
docker port juice-local
text
3000/tcp -> 127.0.0.1:3000

Passa al lato LAN. Trova l'indirizzo IPv4 della tua macchina sulla rete locale con ipconfig su Windows o ip addr su Linux. Poi richiedi l'app a quell'indirizzo:

bash
curl -s -m 5 -o /dev/null -w '%{http_code}\n' http://YOUR-LAN-IP:3000/

Sostituisci YOUR-LAN-IP con il tuo indirizzo. Sulla macchina di prova ha stampato 000 e curl è uscito con codice 7, che significa che non è riuscito a connettersi. La stessa richiesta a 127.0.0.1 ha restituito 200. Il senso del bind sul loopback sta tutto in questa differenza.

Ora apri http://127.0.0.1:3000 nel browser. Dovresti vedere la home di Juice Shop. Usa l'app solo da quella stessa macchina.

Esegui docker ps per vedere i due container fianco a fianco:

bash
docker ps --filter name=juice --format 'table {{.Names}}\t{{.Ports}}'
text
NAMES         PORTS
juice-local   127.0.0.1:3000->3000/tcp
juice         3000/tcp

La riga di juice mostra 3000/tcp senza indirizzo dell'host. La porta esiste dentro la rete ma non è mappata sull'host. La riga di juice-local mostra il binding sull'host.

Passo 6: fai pulizia

Ferma e rimuovi il lab. I container vanno rimossi prima della rete, perché Docker si rifiuta di rimuovere una rete che ha ancora container collegati:

bash
docker rm -f juice juice-local && docker network rm juicenet
text
juice
juice-local
juicenet

Docker stampa i nomi che ha rimosso. Poi conferma che non è rimasto niente:

bash
docker ps -a --filter name=juice --format '{{.Names}}'
docker network ls --filter name=juicenet --format '{{.Name}}'

Nessuno dei due comandi dovrebbe stampare niente. I filtri trovano anche parti di un nome, quindi leggi l'output prima di considerarlo completo e rimuovi solo ciò che hai avviato tu. Nota anche che --rm pulisce solo i container di strumenti usa e getta. I container avviati con -d restano finché non li rimuovi, ed è per questo che esiste questo passo.

Errori comuni

Pubblicare senza un indirizzo. -p 3000:3000 ascolta su ogni interfaccia dell'host. Usa il prefisso 127.0.0.1: per qualsiasi app del lab che apri dalla stessa macchina, e controlla il risultato con docker port.

Dimenticare --internal. La rete funziona senza il flag e niente ti avvisa. Esegui docker network inspect juicenet --format '{{.Internal}}' e cerca true.

Collegare l'app alla rete predefinita. Un container avviato senza --network si unisce al bridge predefinito, che raggiunge l'esterno. Se l'app deve restare isolata, indica la rete ogni volta.

Fidarsi di una prova fallita. Un errore al passo 4 potrebbe venire dal DNS, da un proxy, da una VPN o da un errore di battitura. Il passo 3 è il controllo che mostra che la prova misura la cosa giusta.

Usare il nome dall'host. Il nome juice si risolve solo per i container su juicenet. Il tuo browser non può usarlo. Per questo esiste la copia da browser.

Lasciare i container in esecuzione. Un container in background continua a usare risorse e tiene legata la sua porta. Chiudi ogni sessione con il passo di pulizia ed esegui docker ps subito dopo.

Cosa non dimostra questo lab

L'opzione di rete interna è un controllo, e ha dei limiti.

Non impedisce a uno strumento su juicenet di raggiungere ogni altro container su quella rete. I container sulla stessa rete possono parlare tra loro, ed è ciò che fa funzionare il passo 3. Tieni i servizi estranei fuori dalla rete del lab.

La pubblicazione sul loopback ti protegge dalle altre macchine della tua LAN. Non ti protegge dai programmi già in esecuzione sul tuo computer, perché anche quelli possono connettersi a 127.0.0.1.

La prova controlla un solo nome e un solo host esterno. Non dimostra che ogni percorso in uscita sia chiuso. Il comportamento di Docker può variare tra Docker Desktop, motori Linux e configurazioni rootless, quindi ripeti i controlli sulla macchina che usi davvero.

Infine, questo lab mostra solo che l'isolamento funziona. Non mostra che Juice Shop sia sicuro da esporre. Non lo è, ed è il motivo per tenerlo isolato.

Per andare oltre

Quando il lab funziona, qualche piccola modifica lo rende più utile:

  • Scrivi un breve script che esegue i controlli dei passi da 3 a 5 e stampa pass o fail per ciascuno. Eseguilo ogni volta che tiri su il lab.
  • Aggiungi un secondo servizio a juicenet, per esempio un database, e conferma che l'app lo raggiunge per nome. Così si costruiscono i lab con più container.
  • Leggi la documentazione sulle reti Docker per le opzioni dei driver che non hai ancora usato, come i bridge definiti dall'utente con subnet personalizzate.
  • Impara le basi dell'host discovery prima di scansionare qualsiasi cosa. L'articolo successivo tratta le basi di Nmap sul tuo lab. Scansiona solo reti e host che sono tuoi o che hai il permesso scritto di provare. La rete juicenet è un buon primo bersaglio perché controlli ogni container che ci sta sopra.

Fonti

Fonti

  1. Docker Engine networking overview
  2. docker network create (CLI reference)
  3. Docker port publishing and mapping
  4. OWASP Juice Shop (GitHub repository)

Altro su Laboratori di hacking

Laboratori di hacking

Perché usare NotHoly per CTF e laboratori

Un modello che scrive l'exploit del tuo challenge e spiega perché funziona, triage statico del binario, e un solve che gira nel tuo lab, non nel nostro.

Intermedio3 min di lettura

Laboratori di hacking

Come scrivere un buon report di pentest

Dai una struttura a un report di pentest e scrivi un finding che altri possano riprodurre, con prove, impatto, un vettore CVSS v4.0 e una correzione.

Intermedio8 min di lettura

Laboratori di hacking

Come costruire un home lab legale

Costruisci un home lab che hai il diritto di attaccare: un file di perimetro, porte Docker legate al loopback, una rete isolata, un quaderno di appunti e uno smontaggio pulito.

Base4 min di lettura