Pubblicare temporaneamente su Internet un’applicazione configurata per rispondere alla richieste su localhost è un’esigenza molto comune per sviluppatori e amministratori: una demo da mostrare a un collega, una Web app ancora in sviluppo, una dashboard interna o un servizio che deve restare accessibile solo per qualche ora.
Servizi come ngrok e Cloudflare Quick Tunnels hanno reso l’operazione quasi banale, ma dietro quel meccanismo non c’è nulla che obblighi a dipendere da un’infrastruttura esterna. Vincent Bernat ha mostrato una soluzione particolarmente elegante, che vogliamo approfondire insieme a voi: costruire un tunnel HTTP self-hosted usando soltanto OpenSSH e nginx, due componenti che spesso sono già presenti sul server.
L’idea sfrutta una funzione di SSH disponibile da molto tempo: il remote port forwarding. Con l’opzione -R il client chiede al server SSH di aprire una porta dalla propria parte e di inoltrare ogni connessione attraverso il canale cifrato verso una porta della macchina locale. OpenSSH permette inoltre di specificare la porta remota 0, lasciando al server il compito di sceglierne automaticamente una libera. È proprio questa possibilità a trasformare SSH in una base sorprendentemente efficace per costruire un servizio simile, almeno per alcuni scenari, ai classici tunnel HTTP commerciali.
SSH diventa un tunnel HTTP: cosa succede dietro le quinte
Il punto di partenza è un comando molto semplice:
ssh -N -R 0:localhost:8080 server.example.com
Con l’opzione -N SSH apre la connessione senza avviare una shell sul server remoto: la sessione resta attiva esclusivamente per gestire il port forwarding. Specificando 0 come porta remota, OpenSSH assegna dinamicamente un socket disponibile sul server e comunica al client il numero scelto.
Cosa serve per costruire il tunnel: server, dominio, DNS e HTTPS
Prima di arrivare al traguardo prefisso servono però alcuni requisiti precisi.
Non basta disporre di un server SSH pubblico: bisogna avere anche il controllo di un nome di dominio, o almeno di un suo sottodominio, e poter modificare la relativa zona DNS.
Nel caso illustrato da Bernat, per esempio, tutti gli indirizzi del tipo p41535.ssh.example.com, p42001.ssh.example.com e così via devono poter essere risolti verso lo stesso server. Per evitare di creare manualmente un record per ogni tunnel si configura quindi un record DNS wildcard, ad esempio *.ssh.example.com, diretto verso la macchina su cui girano OpenSSH e nginx.
Serve inoltre un certificato TLS valido per lo stesso wildcard, cioè *.ssh.example.com, così nginx può offrire HTTPS qualunque sia il numero inserito nel sottodominio (ne parliamo più avanti).
In generale bisogna controllare tre elementi: il server pubblico, la configurazione DNS del dominio e la terminazione HTTPS affidata a nginx.
Come OpenSSH collega la porta remota all’applicazione locale
Supponiamo che OpenSSH scelga automaticamente la porta 41535 sul server. Da quel momento tutto il traffico che raggiunge 127.0.0.1:41535 è trasportato attraverso il tunnel SSH fino alla porta dell’applicazione sul computer locale dello sviluppatore.
Resta però un problema: come trasformare quella porta numerica in un normale indirizzo Web? Qui entra in gioco il web server nginx.
Bernat costruisce il nome host inserendo direttamente il numero della porta nel sottodominio: per esempio p41535.ssh.example.com. Quando arriva una richiesta HTTPS diretta a quell’indirizzo, nginx legge il numero 41535 dal nome host e lo usa per decidere a quale porta locale inoltrare la connessione.
In pratica, p41535.ssh.example.com diventa una sorta di alias Web per 127.0.0.1:41535. Nginx non ha bisogno di una configurazione separata per ogni nuovo tunnel: usa una regola generica capace di riconoscere il numero presente nel sottodominio. Una direttiva server_name basata sull’uso di un’espressione regolare può, per esempio, intercettare la parte numerica con una regola come ~^p(?<port>\d\d\d\d\d)\.ssh\.example\.com$. Il valore catturato finisce nella variabile $port, che proxy_pass può poi utilizzare per inoltrare la richiesta verso http://127.0.0.1:$port.
Il trucco, alla fine, è quindi semplice: SSH crea una porta temporanea sul server; il numero di quella porta entra nel nome del sottodominio; nginx lo estrae e manda il traffico proprio a quel socket. Da lì SSH lo riporta fino all’applicazione che gira in locale.

DNS wildcard e TLS: una sola configurazione per tutti i tunnel
Per evitare di creare un record DNS per ogni porta, Bernat si è servito di un record wildcard del tipo *.ssh.example.com diretto verso il server che ospita Nginx.
La stessa logica vale per la connessione TLS: un certificato wildcard consente a nginx di servire indirizzi come p41535.ssh.example.com, p41781.ssh.example.com e così via senza generare un certificato separato a ogni connessione.
Let’s Encrypt supporta i certificati wildcard tramite la challenge ACME DNS-01: il controllo del dominio avviene inserendo un record TXT sotto _acme-challenge. È una scelta particolarmente adatta a un sistema del genere perché il numero di sottodomini possibili è elevato e cambia continuamente, mentre il certificato resta lo stesso.
Una volta predisposti DNS e certificato, creare un nuovo tunnel non richiede più interventi sulla configurazione del server web: la porta scelta da OpenSSH finisce direttamente nel nome host e nginx decide dinamicamente dove inoltrare la connessione!
Perché il numero della porta non può essere considerato una password
Qui emerge il primo vero problema di sicurezza. Se l’unico elemento difficile da conoscere fosse 41535, il tunnel avrebbe una protezione molto debole. Su Linux l’intervallo predefinito ip_local_port_range è normalmente compreso tra 32768 e 60999: sono 28.232 valori, equivalenti a meno di 15 bit di informazione teorica.
Non serve quindi una sofisticata capacità di calcolo per provare sistematicamente tutte le porte plausibili; anche modificando l’intervallo, il principio resterebbe sbagliato: un identificatore assegnato dal sistema operativo non dovrebbe diventare una credenziale.
La soluzione proposta da Bernat aggiunge perciò un tokenlegato alla porta e a una data di scadenza.
Il risultato produce URL simili a https://TOKEN--SCADENZA@p41535.ssh.example.com/. Il componente prima del carattere @ è interpretato dai client HTTP come nome utente e può quindi arrivare a nginx attraverso la normale HTTP Basic Authentication.
La protezione deriva da HTTPS: senza TLS, inserire token o password nel meccanismo di autenticazione HTTP Basic sarebbe del tutto insufficiente.

Il modulo secure_link di nginx genera URL che scadono
Per verificare il token Bernat usa ngx_http_secure_link_module. Il modulo nasce per controllare l’autenticità degli URL e limitarne la durata; nginx non lo include nelle build standard.
La configurazione estrae dal nome utente HTTP due elementi: hash e timestamp di scadenza. nginx ricostruisce quindi il valore atteso combinando timestamp, porta del tunnel e un segreto custodito sul server. Se il valore ricevuto non coincide, $secure_link resta vuoto; se il token è corretto ma scaduto, assume il valore 0; quando entrambe le verifiche hanno successo vale 1.
Il meccanismo messo a punto da Bernat restituisce di conseguenza 401 Unauthorized per un token non valido e 410 Gone quando il collegamento ha superato la scadenza.
Prima di inoltrare la richiesta al servizio locale elimina inoltre l’header Authorization: una precauzione sensata, perché il token utilizzato per entrare nel tunnel non deve raggiungere l’applicazione pubblicata.
Come recuperare automaticamente la porta scelta da OpenSSH
Resta una difficoltà pratica: OpenSSH comunica la porta dinamica al client, ma il comando eseguito sul server non riceve quel numero attraverso una comoda variabile d’ambiente. Per produrre automaticamente l’URL occorre quindi individuarlo in altro modo.
Bernat risale la catena dei processi fino alle istanze sshd-session associate alla connessione e usa ss per trovare i socket TCP in ascolto appartenenti a quei processi.
Lo script messo a punto raccoglie quindi le porte trovate, calcola per ciascuna il token con OpenSSL, stampa l’URL completo e mantiene aperta la sessione. Nella configurazione SSH del client si può associare a un host simbolico una RemoteCommand che avvia automaticamente lo script. Il risultato finale diventa molto vicino all’esperienza dei servizi dedicati: ssh -R 0:localhost:8080 http-over-ssh, seguito immediatamente dall’indirizzo HTTPS da condividere.
Una soluzione minimale, non un sostituto universale di ngrok
Il vantaggio principale del progetto non consiste nel dimostrare che ngrok, Cloudflare Tunnel, frp, localtunnel o sish siano inutili.
Sono strumenti che risolvono molti problemi aggiuntivi: gestione degli utenti, osservabilità, policy, multiplexing, autenticazione, controllo degli accessi, integrazione con reti private e in alcuni casi terminazione TLS automatica.
La soluzione che combina SSH e nginx è interessante per il motivo opposto: mostra quanto si possa ottenere riutilizzando componenti Unix consolidati. Non richiede un daemon dedicato sul computer dello sviluppatore oltre al client OpenSSH; il traffico tra macchina locale e server viaggia nel normale canale SSH e l’infrastruttura Web resta nelle mani di chi amministra il server.
Per team piccoli, laboratori, ambienti di test e condivisioni temporanee può essere un approccio molto efficace. In produzione, o quando i tunnel appartengono a utenti diversi e trasportano dati sensibili, servono invece controlli più rigorosi e una gestione delle credenziali meno artigianale.
Resta una bella dimostrazione tecnica: con OpenSSH, nginx, un dominio wildcard e un certificato TLS si può costruire un servizio HTTP tunneling self-hosted con pochissimi componenti. Soprattutto, si comprende bene cosa fanno dietro le quinte molti strumenti che trasformano un URL locale come localhost:8080 in una risorsa raggiungibile da Internet.