Marvin Pascale

14 Ottobre 2026

Reverse SSH tunnel

Indice navigabile

Reverse SSH tunnel tra homelab e VPS, con variante pubblica sulla porta 8080

La copertina illustra la variante pubblica su 0.0.0.0:8080. Qui partiamo dal listener privato su 127.0.0.1:8080.

Avete Grafana nell’homelab, una VPS su Internet e il desiderio abbastanza ragionevole di vedere le dashboard anche fuori casa. Poi arriva il dettaglio: la connessione domestica è dietro NAT, magari anche quello dell’operatore.

Potremmo iniziare una trattativa diplomatica con il router. Oppure lasciare che sia l’homelab ad aprire una connessione verso la VPS.

Un reverse SSH tunnel permette di raggiungere un servizio di casa attraverso una connessione SSH aperta dall’homelab verso la VPS. La parte interessante arriva dopo: farlo ripartire quando cade la rete, eseguirlo con l’utente giusto e capire cosa consente SELinux. Perché “funziona finché tengo aperto il terminale” è ancora una fase del progetto.

Come gira il traffico

Il client SSH parte dalla macchina di casa. Sulla VPS viene aperta una porta; le connessioni ricevute lì attraversano il tunnel e raggiungono Grafana dal lato domestico.

Useremo Grafana su 127.0.0.1:3000 della macchina che esegue SSH e la porta 8080 sulla VPS. Se Grafana vive altrove, anche in un container, la destinazione va adattata al punto da cui parte il client. Parliamo di traffico TCP.

La porta remota ascolterà su 127.0.0.1. Per raggiungerla dall’esterno potremo mettere un reverse proxy sulla VPS, con HTTPS e autenticazione. Nell’esempio il proxy gira direttamente sulla VPS: se è in un container con rete separata, il suo 127.0.0.1 non è quello dell’host e il collegamento al tunnel va progettato di conseguenza. Il tunnel cifra il collegamento tra i due host; il tratto tra browser e VPS va protetto a sua volta.

Mani in pasta

Gli esempi sono per macchine che amministriamo e richiedono che la rete domestica consenta connessioni SSH in uscita verso la VPS.

Prepariamo due account dedicati, senza privilegi amministrativi: grafana-tunnel sulla macchina domestica e tunnel-vps sulla VPS.

Nell’esempio, la home dell’utente locale è /home/grafana-tunnel. Servono una chiave dedicata, la chiave pubblica autorizzata sulla VPS e un file known_hosts con la chiave del server verificata attraverso un canale fidato. Raccoglierla dalla rete non ne dimostra l’autenticità.

La directory .ssh deve appartenere all’utente locale, con permessi 700; per la chiave privata usiamo 600. L’autenticazione deve funzionare senza richieste interattive. Una chiave con passphrase richiede un agente disponibile al servizio anche dopo il riavvio: quello del nostro terminale non basta.

Sulla VPS autorizziamo il forwarding remoto per questo account, limitando il listener. Inseriamo il blocco dedicato in fondo alla configurazione di sshd, così le direttive successive non finiscono involontariamente nello stesso Match. Se usiamo un file incluso, controlliamo anche l’ordine degli Include: il contesto Match non termina solo perché termina il file:

Match User tunnel-vps
    AllowTcpForwarding remote
    GatewayPorts no
    PermitListen 127.0.0.1:8080
    MaxSessions 0

PermitListen limita il punto di ascolto, non l’accesso alla shell. MaxSessions 0 impedisce sessioni shell, comandi e sottosistemi come SFTP, lasciando disponibile il forwarding usato dal tunnel. Non impedisce al client di scegliere un altro servizio domestico da inoltrare: PermitListen controlla il listener sulla VPS, non la destinazione sul client.

Controlliamo anche le altre restrizioni del server e della chiave: DisableForwarding yes, per esempio, impedisce comunque il forwarding. Prima di ricaricare il servizio SSH della distribuzione, verifichiamo la configurazione con privilegi amministrativi:

sudo sshd -t

Un account dedicato deve comunque poter autenticarsi: shell mancante o non eseguibile, blocco dell’account e regole PAM possono impedirlo anche con -N. Limitiamo account e chiave allo scopo necessario; niente chiave amministrativa riciclata.

Creiamo /home/grafana-tunnel/.ssh/config:

Host grafana-relay
    HostName vps.example.invalid
    User tunnel-vps
    IdentityFile /home/grafana-tunnel/.ssh/id_ed25519
    IdentitiesOnly yes
    BatchMode yes
    StrictHostKeyChecking yes
    UserKnownHostsFile /home/grafana-tunnel/.ssh/known_hosts
    ServerAliveInterval 30
    ServerAliveCountMax 3
    ExitOnForwardFailure yes
    RemoteForward 127.0.0.1:8080 127.0.0.1:3000

Sostituiamo il nome dimostrativo della VPS, poi avviamo il client con lo stesso utente che userà il servizio:

sudo -u grafana-tunnel /usr/bin/ssh -F /home/grafana-tunnel/.ssh/config -N -T grafana-relay

-N evita un comando remoto, -T evita il terminale. Il processo resta in primo piano.

I messaggi ServerAlive aiutano a rilevare una connessione SSH irraggiungibile. ExitOnForwardFailure fa fallire l’avvio se il forwarding non viene predisposto, per esempio perché la porta è occupata. Non controlla Grafana: il tunnel può essere vivo mentre l’applicazione è ferma. SSH non è il medico di famiglia dei container.

Il turno di systemd

Creiamo /etc/systemd/system/grafana-tunnel.service:

[Unit]
Description=Reverse SSH tunnel per Grafana
Wants=network-online.target
After=network-online.target

[Service]
Type=simple
User=grafana-tunnel
ExecStart=/usr/bin/ssh -F /home/grafana-tunnel/.ssh/config -N -T grafana-relay
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

L’impostazione User= è essenziale: una unit di sistema senza questa impostazione parte normalmente come root.

systemd segue il processo in primo piano e ne tenta il riavvio dopo dieci secondi quando termina, salvo arresti espliciti e limiti ai riavvii. Non aggiungiamo -f: il processo deve restare sotto il controllo di systemd.

network-online.target ordina l’avvio rispetto alla disponibilità della rete secondo la configurazione locale; non garantisce che la VPS sia raggiungibile e non sorveglia la connessione dopo l’avvio. A rilevare le interruzioni successive sono i messaggi ServerAlive; quando SSH termina, systemd può riavviarlo.

Terminiamo la prova manuale prima di avviare il servizio, altrimenti i due client si contenderanno la porta remota.

sudo systemd-analyze verify /etc/systemd/system/grafana-tunnel.service
sudo systemctl daemon-reload
sudo systemctl enable --now grafana-tunnel.service
sudo journalctl -u grafana-tunnel.service -b

Uno stato “active” descrive il processo, non certifica che la dashboard risponda.

SELinux: prima la diagnosi

Con SELinux in modalità enforcing, un comando funzionante dal terminale può incontrare permessi diversi quando parte come servizio. Controlliamo contesti e dinieghi:

ps -eZ
ls -Z /usr/bin/ssh /home/grafana-tunnel/.ssh
sudo ausearch -m AVC,USER_AVC -ts recent

La correzione dipende da distribuzione, policy installata e dinieghi effettivi. Disabilitare SELinux non è una diagnosi particolarmente raffinata. Se le etichette sono errate, ripristiniamo quelle previste dalla policy; non generiamo permessi alla cieca.

Una possibile architettura usa un piccolo wrapper compilato: systemd esegue /usr/local/libexec/grafana-tunnel, etichettato grafana_t_exec_t. Grazie alle regole del modulo locale, il processo del wrapper entra nel dominio grafana_t. Il wrapper richiama SSH tramite execv, conservando gli argomenti -F, -N e -T e l’alias grafana-relay. Anche la unit deve mantenere User=grafana-tunnel.

Nella policy Fedora che fornisce ssh_domtrans_ssh, applicare questa interfaccia a grafana_t abilita la transizione verso ssh_t. ssh_exec_t è il tipo del file eseguibile. Sono due ruoli diversi: un wrapper con un dominio dedicato non significa che SSH continui a girare in quel dominio.

Per un modulo locale manteniamo coerenti il nome del modulo e quelli dei file .te, .fc e .pp, definiamo il contesto completo dell’eseguibile, applichiamo le etichette e verifichiamo la transizione reale. Dichiarare i tipi non dimostra che il servizio abbia tutti i permessi necessari. È un’opzione da validare sulla macchina interessata, non un requisito universale dei tunnel SSH.

Le ultime verifiche

Sulla VPS controlliamo il listener:

ss -ltn

La porta 8080 deve ascoltare su 127.0.0.1. Dalla VPS proviamo la risposta HTTP di Grafana:

curl --fail --show-error http://127.0.0.1:8080/api/health

Controlliamo separatamente reverse proxy e accesso esterno previsto. Il loopback evita l’esposizione diretta su Internet, ma gli altri processi e utenti della VPS possono raggiungerlo: l’autenticazione di Grafana resta necessaria.

GatewayPorts merita attenzione: no, il valore predefinito, limita il forwarding al loopback; yes forza il bind wildcard; clientspecified lascia scegliere l’indirizzo al client. Chiedere 127.0.0.1 non sostituisce il controllo del server e del listener effettivo.

La variante 0.0.0.0:8080 della copertina va scelta consapevolmente e richiede una configurazione SSH compatibile, oltre a controlli adeguati sulla rete e sull’autenticazione. Non è una scorciatoia da adottare perché “così finalmente si vede”.

Infine proviamo riavvio e riconnessione in una finestra controllata, verificando processo, porta e applicazione. Gli esempi descrivono una configurazione; le prove restano da eseguire nel vostro ambiente.

A quel punto il tunnel avrà un responsabile anche alle tre di notte. Possibilmente systemd, così voi potete dormire.


Le opinioni in quanto tali sono opinabili e nulla ti vieta di approfondire l’argomento.

Risorse: