Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save gwpl/44a114bcd3c33b2b79dc3815a02812ee to your computer and use it in GitHub Desktop.

Select an option

Save gwpl/44a114bcd3c33b2b79dc3815a02812ee to your computer and use it in GitHub Desktop.
SSH Reverse Tunnel — Quick Setup Guide
title SSH Reverse Tunnel — Quick Setup Guide
geometry a4paper,top=2cm,left=1cm,right=1cm,bottom=1.5cm
gist https://gist.github.com/gwpl/44a114bcd3c33b2b79dc3815a02812ee

SSH Reverse Tunnel — Quick Setup Guide

Control remote computers (behind NAT) from your master machine, where remote computers initiate the connection.

Variable Reference

All variables follow the WHAT_on_WHERE naming pattern — the suffix tells you which machine the value lives on or describes.

Machines

Variable Description Example
master Your trusted controlling machine your main workstation
remote Machine(s) you want to control (behind NAT) laptops, servers
vpn Relay server with public IP (Appendices only) a VPS or cloud instance

Credentials & Addresses

# -- master (your controlling machine) --
MASTER_IP=192.168.7.123          # master's IP, reachable by remotes
SSHD_PORT_on_MASTER=3333         # sshd listening port on master
USER_on_MASTER=alice              # your username on master

# -- remote (machines you control) --
USER_on_REMOTE=remoteuser         # username on the remote machine

# -- vpn (relay server, Appendices only) --
VPN_IP=203.0.113.50               # VPN's public IP
SSHD_PORT_on_VPN=22               # sshd listening port on VPN
USER_on_VPN=tunnel                # username on VPN

Tunnel Ports (reserved range: 2220–2229)

Port Lives on Tunnels to Used in
2220 master or VPN Remote A → localhost:22 Direct, Appendix A, B
2221 master or VPN Remote B → localhost:22 Appendix B
2222 master or VPN Remote C → localhost:22 Appendix B
... ... ... up to 2229
2210 VPN master → SSHD_PORT_on_MASTER Appendix A only

How It Works

Remote (behind NAT)                              Master (reachable)
       |                                                  |
       |--- ssh -R 2220:localhost:22 master -N --------->  |  (tunnel established)
       |                                                  |
       |<--- ssh -p 2220 USER_on_REMOTE@localhost -------|  (you control remote)

There are two independent SSH sessions — session 1 is the tunnel (raw TCP pipe), session 2 is your actual shell connection that travels through it. The remote's sshd handles session 2 normally — it doesn't know it arrived via a tunnel. That's why the remote must have sshd running.

ssh -R Syntax

ssh -R P1:L:P2 S
  • P1 — port to open on S (the server you're connecting to)
  • L:P2 — where the caller forwards incoming connections to (resolved from the caller's side)
  • S — the server to connect to

L can be localhost, an IP, or a hostname — anything the caller can resolve and reach. Examples:

  • ssh -R 2220:localhost:22 S — tunnel to caller's own sshd
  • ssh -R 2220:10.0.0.50:22 S — tunnel to a third machine on caller's LAN
  • ssh -R 2220:example.com:80 S — tunnel to a website (raw TCP, not an HTTP proxy)

Setup Checklist

1. Remote: ensure sshd is running

sudo systemctl enable --now ssh

2. Remote: generate key (if none exists)

ssh-keygen -t ed25519
cat ~/.ssh/id_ed25519.pub   # copy this

3. Master: add restricted key to ~/.ssh/authorized_keys

Paste the remote's public key with this prefix (single line):

restrict,port-forwarding,permitlisten="localhost:2220",permitopen="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...key... USER_on_REMOTE@remote-host

What each option does:

  • restrict — disables everything (shell, pty, agent, X11, forwarding)
  • port-forwarding — re-enables forwarding (needed for the tunnel)
  • permitlisten="localhost:2220" — only allows reverse tunnel on port 2220
  • permitopen="255.255.255.255:9" — blocks local forwarding (-L)
  • command="/bin/false" — no shell execution possible

Optional: add from="SUBNET/MASK" to restrict source IPs.

4. Master: verify sshd_config has GatewayPorts no

grep -i gatewayports /etc/ssh/sshd_config

If missing or set to yes, add GatewayPorts no and restart sshd.

5. Master: add your public keys to the remote

Copy master user's public keys to the remote's ~/.ssh/authorized_keys:

cat ~/.ssh/*.pub   # on master, copy these to the remote

Running It

On the remote — open the tunnel:

ssh -R 2220:localhost:22 ${USER_on_MASTER}@${MASTER_IP} -p ${SSHD_PORT_on_MASTER} \
    -o ServerAliveInterval=120 -N

On master — connect through the tunnel:

ssh -p 2220 ${USER_on_REMOTE}@localhost

SSH config shortcut (on master, in ~/.ssh/config):

Host remote-a
    HostName localhost
    Port 2220
    User USER_on_REMOTE

Then just: ssh remote-a

Persistent Tunnel (Optional)

Install autossh on the remote for auto-reconnection:

sudo apt install autossh
autossh -M 0 -fN -R 2220:localhost:22 \
    ${USER_on_MASTER}@${MASTER_IP} -p ${SSHD_PORT_on_MASTER} \
    -o ServerAliveInterval=120

Troubleshooting

  • Permission denied — check that the correct key fingerprint is in master's authorized_keys: ssh-keygen -l -f ~/.ssh/authorized_keys
  • Tunnel not workingpermitlisten requires port-forwarding to also be set (it's silently ignored otherwise)
  • Connection drops — increase ServerAliveInterval or use autossh
  • Debug — add -vvv to the ssh command on the remote to see what's happening

Understanding permitlisten and permitopen

These two authorized_keys options control opposite directions of SSH port forwarding. Since restrict,port-forwarding re-enables both -R and -L (there is no per-direction flag in OpenSSH), you must use these options to lock down whichever direction is not needed.

permitlisten — controls -R (reverse/remote forwarding)

Whitelists which [host:]port the key holder can ask the server to listen on.

When a client runs ssh -R 2220:localhost:22 server, it asks the server to start listening on port 2220. permitlisten restricts which listen addresses are allowed.

  • permitlisten="localhost:2220" — client can only -R on port 2220
  • permitlisten not specified + port-forwarding enabled → client can -R on any port (security gap!)

Blocking all -R: Use permitlisten="255.255.255.255:9" — an unreachable address that parses as valid but never matches a real request.

permitopen — controls -L (local forwarding) and ProxyJump

Whitelists which destinations the server will open connections to on behalf of the client. This also controls direct-tcpip channels used by ProxyJump (-J).

  • permitopen="localhost:2220" — server can only connect to localhost:2220 (enables ProxyJump to that tunnel port)
  • permitopen="255.255.255.255:9"effectively blocks all -L and ProxyJump (unreachable destination)

Critical: authorized_keys vs sshd_config asymmetry

Syntax sshd_config authorized_keys
PermitListen none Valid (blocks all -R) INVALID — parse error, key rejected
permitlisten="" N/A INVALID — parse error, key rejected
permitlisten="localhost:0" N/A INVALID — port 0 fails validation
permitlisten="255.255.255.255:9" N/A Valid — effective block

This asymmetry exists because sshd_config parsing (servconf.c) explicitly handles the none keyword, but authorized_keys parsing (auth-options.c) validates every value strictly as host:port via a2port(). Source: OpenSSH 10.2p1 auth-options.c:handle_permit().

How restrict,port-forwarding interacts with these options

restrict                                → all forwarding OFF
restrict,port-forwarding                → both -L and -R ON (no restrictions!)
restrict,port-forwarding,permitlisten=  → -R scoped, -L unrestricted
restrict,port-forwarding,permitopen=    → -L scoped, -R unrestricted
restrict,port-forwarding,permitlisten=X,permitopen=Y → both scoped (secure)

Rule of thumb: when using restrict,port-forwarding, always specify both permitlisten and permitopen to lock down the unused direction with the 255.255.255.255:9 trick.

Per-key patterns used in this guide

Key belongs to Needs -R Needs -L/ProxyJump permitlisten permitopen
Remote on master (direct) Yes (tunnel) No "localhost:2220" "255.255.255.255:9"
Master on VPN (Appendix A) Yes (expose sshd) No "localhost:2210" "255.255.255.255:9"
Remote on VPN (Appendix A) No Yes (ProxyJump) "255.255.255.255:9" "localhost:2210"
Remote on VPN (Appendix B) Yes (tunnel) No "localhost:222X" "255.255.255.255:9"
Master on VPN (Appendix B) No Yes (ProxyJump) "255.255.255.255:9" "localhost:2220",...

Appendix A: VPN Relay — Both Machines Behind NAT

When both master and the remote are behind NAT, use a VPN server (with a public IP) as a relay. Two chained reverse tunnels route through it.

Scenario

Neither master nor the remote has a reachable IP. A VPN server with a public IP acts as the meeting point.

ASCII Diagram — Overview

Remote                    VPN Server                    Master
(behind NAT)            (public IP)               (behind NAT)
     ?        <---         ?          <---            ?
  no public IP        relay point              no public IP

ASCII Diagram — Connection Flow

Remote                    VPN Server                    Master
(behind NAT)            (public IP)               (behind NAT)
     |                       |                          |
     |                       |<--- ssh -R 2210 ---------|  (1) master exposes sshd on VPN
     |                       |     :localhost:SSHD_PORT  |
     |                       |      _on_MASTER           |
     |                       |                          |
     |--- ssh -J VPN ------->|--- localhost:2210 ------->|  (2) remote reaches master
     |  -R 2220:localhost:22 |    (through tunnel)       |      via ProxyJump, sets up
     |                       |                          |      reverse tunnel on master
     |                       |                          |
     |<========= ssh -p 2220 USER_on_REMOTE@localhost ==|  (3) master controls remote
     |  (travels back through both tunnels)             |

Mermaid Diagram — Connection Flow

sequenceDiagram
    participant M as Master<br/>(behind NAT)
    participant V as VPN Server<br/>(public IP)
    participant R as Remote<br/>(behind NAT)

    Note over M,V: Step 1: Master exposes sshd on VPN
    M->>V: ssh -R 2210:localhost:SSHD_PORT_on_MASTER
    Note over V: VPN now listens on<br/>localhost:2210 → master sshd

    Note over R,M: Step 2: Remote reaches master via ProxyJump
    R->>V: ssh -J USER_on_VPN@VPN (ProxyJump)
    V->>M: relay via localhost:2210 (tunnel from Step 1)
    Note over M: -R 2220:localhost:22 negotiated<br/>master now listens on localhost:2220

    Note over M,R: Step 3: Master controls remote
    M->>M: ssh -p 2220 USER_on_REMOTE@localhost
    M-->>V: through Step 2 tunnel
    V-->>R: through ProxyJump relay
    Note over R: lands on remote's sshd (port 22)
Loading

Mermaid Diagram — Architecture

graph LR
    subgraph "Behind NAT"
        M["Master<br/>sshd :SSHD_PORT_on_MASTER"]
    end

    subgraph "Public Internet"
        V["VPN Server<br/>sshd :SSHD_PORT_on_VPN<br/>localhost:2210 (tunnel to master)"]
    end

    subgraph "Behind NAT "
        R["Remote<br/>sshd :22"]
    end

    M -->|"1. ssh -R 2210:localhost:SSHD_PORT_on_MASTER"| V
    R -->|"2. ssh -J VPN -R 2220:localhost:22"| V
    V -->|"2. relayed to localhost:2210"| M
    M -.->|"3. ssh -p 2220 USER_on_REMOTE@localhost"| R

    style M fill:#4a9eff,color:#fff
    style V fill:#ff9f43,color:#fff
    style R fill:#26de81,color:#fff
Loading

Three SSH Sessions In Play

Session Initiated by Path Purpose
1 Master master → VPN Reverse tunnel exposing master's sshd on VPN:2210
2 Remote remote → VPN → master ProxyJump + reverse tunnel opening master:2220
3 Master master → localhost:2220 → tunnel → remote:22 Your actual shell on the remote

Setup

Step 1: Master exposes its sshd on VPN

master$ ssh -R 2210:localhost:${SSHD_PORT_on_MASTER} \
    ${USER_on_VPN}@${VPN_IP} -p ${SSHD_PORT_on_VPN} \
    -o ServerAliveInterval=120 -N

This opens localhost:2210 on VPN, tunneling back to master's sshd. With GatewayPorts no on VPN (recommended), port 2210 is only reachable from VPN's localhost — not from the internet.

Step 2: Remote connects to master via ProxyJump, with reverse tunnel

remote$ ssh -J ${USER_on_VPN}@${VPN_IP}:${SSHD_PORT_on_VPN} \
    -R 2220:localhost:22 \
    ${USER_on_MASTER}@localhost -p 2210 \
    -o ServerAliveInterval=120 -N

What happens:

  1. SSH connects to VPN (the jump host) — remote authenticates to VPN
  2. From VPN, it connects to localhost:2210 — which tunnels to master's sshd
  3. Remote authenticates to master's sshd
  4. -R 2220:localhost:22 is negotiated with master's sshd — port 2220 opens on master

Step 3: On master — same as the direct setup

master$ ssh -p 2220 ${USER_on_REMOTE}@localhost

Or: ssh remote-a (if configured in ~/.ssh/config)

Security: Restrict Keys at Every Hop

On VPN — master's key (tunnel-only, exposes sshd)

restrict,port-forwarding,permitlisten="localhost:2210",permitopen="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...master-key...

On VPN — remote's key (ProxyJump relay only)

restrict,port-forwarding,permitopen="localhost:2210",permitlisten="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...remote-key...

The remote needs permitopen="localhost:2210" so the ProxyJump can connect through to master's tunnel. permitlisten="255.255.255.255:9" blocks any -R attempts — the remote should not open ports on VPN.

On master — remote's key (same as direct setup)

restrict,port-forwarding,permitlisten="localhost:2220",permitopen="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...remote-key...

VPN sshd_config hardening

GatewayPorts no              # tunnels bind to localhost only
AllowTcpForwarding yes       # need both -R (master's tunnel) and direct-tcpip (remote's ProxyJump)

Note: AllowTcpForwarding yes is required here because the remote's ProxyJump uses direct-tcpip (same mechanism as -L). Per-key permitopen and permitlisten restrictions ensure each key can only use its intended direction.

Persistent Tunnels with autossh

On master (keep sshd exposed on VPN):

autossh -M 0 -fN -R 2210:localhost:${SSHD_PORT_on_MASTER} \
    ${USER_on_VPN}@${VPN_IP} -p ${SSHD_PORT_on_VPN} \
    -o ServerAliveInterval=120

On remote (keep reverse tunnel to master):

autossh -M 0 -fN \
    -J ${USER_on_VPN}@${VPN_IP}:${SSHD_PORT_on_VPN} \
    -R 2220:localhost:22 \
    ${USER_on_MASTER}@localhost -p 2210 \
    -o ServerAliveInterval=120

Key Insight

The ProxyJump (-J) is what makes this work without GatewayPorts yes. The jump goes through VPN to reach master's tunnel endpoint on localhost — so master's sshd port never needs to be exposed on VPN's public interface. The entire chain stays on localhost bindings.


Appendix B: VPN Hub — Remotes Always Connected, Master Connects On-Demand

A simpler architecture where remotes maintain persistent reverse tunnels to VPN, and master connects whenever it wants via ProxyJump. Master does not need to be online or maintain any tunnel — it just SSHes through VPN on demand.

Why This Is Better For Many Use Cases

  • Master can be offline — no persistent tunnel from master needed
  • Simpler setup — remotes only talk to VPN, never to master
  • Scales — add more remotes by assigning each a unique port on VPN
  • VPN is the gatekeeper — only users with SSH access to VPN can reach the remotes
  • No ports exposed to internetGatewayPorts no keeps everything on VPN's localhost

Architecture

ASCII Diagram — Overview

    Remote A                                        Remote B
   (behind NAT)                                   (behind NAT)
        |                                              |
        |--- ssh -R 2220:localhost:22 ---.    .--- ssh -R 2221:localhost:22 ---|
        |     (autossh, persistent)       \  /    (autossh, persistent)        |
        |                                  \/                                  |
        |                                  /\                                  |
        |                     +-----------/--\-----------+                     |
        |                     |  VPN Server (public IP)  |                     |
        |                     |   localhost:2220 -> A:22  |                     |
        |                     |   localhost:2221 -> B:22  |                     |
        |                     +------------|-------------+                     |
        |                                  |                                   |
        |                      Master (behind NAT)                             |
        |                       connects on demand                             |
        |                     ssh -J VPN user@localhost -p 2220                |
        |                                  |                                   |
        |<---------------------------------+                                   |
                        (ProxyJump through VPN)

Mermaid Diagram — Architecture

graph TB
    subgraph "Behind NAT — Remotes"
        A["Remote A<br/>sshd :22<br/>autossh → VPN :2220"]
        B["Remote B<br/>sshd :22<br/>autossh → VPN :2221"]
        C["Remote C<br/>sshd :22<br/>autossh → VPN :2222"]
    end

    subgraph "Public Internet"
        V["VPN Server<br/>sshd :SSHD_PORT_on_VPN<br/>localhost:2220 → A<br/>localhost:2221 → B<br/>localhost:2222 → C"]
    end

    subgraph "Behind NAT — Master"
        K["Master<br/>connects when needed<br/>no persistent tunnel"]
    end

    A -->|"ssh -R 2220:localhost:22 (persistent)"| V
    B -->|"ssh -R 2221:localhost:22 (persistent)"| V
    C -->|"ssh -R 2222:localhost:22 (persistent)"| V
    K -.->|"ssh -J VPN user@localhost -p 222X"| V

    style K fill:#4a9eff,color:#fff
    style V fill:#ff9f43,color:#fff
    style A fill:#26de81,color:#fff
    style B fill:#26de81,color:#fff
    style C fill:#26de81,color:#fff
Loading

Mermaid Diagram — Connection Sequence

sequenceDiagram
    participant A as Remote A<br/>(behind NAT)
    participant B as Remote B<br/>(behind NAT)
    participant V as VPN Server<br/>(public IP)
    participant M as Master<br/>(behind NAT)

    Note over A,V: Remotes maintain persistent tunnels (autossh)
    A->>V: ssh -R 2220:localhost:22 (persistent)
    B->>V: ssh -R 2221:localhost:22 (persistent)
    Note over V: VPN localhost:2220 → A:22<br/>VPN localhost:2221 → B:22

    Note over M: Master comes online, wants to control Remote A
    M->>V: ssh -J USER_on_VPN@VPN (ProxyJump)
    V->>V: connects to localhost:2220
    V-->>A: tunneled to A's sshd :22
    Note over M,A: Master now has shell on Remote A

    Note over M: Later, wants Remote B
    M->>V: ssh -J USER_on_VPN@VPN (ProxyJump)
    V->>V: connects to localhost:2221
    V-->>B: tunneled to B's sshd :22
    Note over M,B: Master now has shell on Remote B
Loading

Two SSH Sessions (simpler than Appendix A)

Session Initiated by Path Purpose
1 Remote remote → VPN Persistent reverse tunnel (autossh)
2 Master master → VPN → localhost:PORT → remote:22 On-demand shell via ProxyJump

Compare with Appendix A which needs three sessions. This is simpler because master doesn't maintain its own tunnel.

Setup

Port Allocation

Assign each remote a unique port on VPN from the 2220–2229 range:

Remote Port on VPN Tunnel
Remote A 2220 localhost:2220 → A:22
Remote B 2221 localhost:2221 → B:22
Remote C 2222 localhost:2222 → C:22
... ... up to 2229

Step 1: Each remote — persistent reverse tunnel to VPN

On each remote, set up autossh:

# Remote A
autossh -M 0 -fN -R 2220:localhost:22 \
    ${USER_on_VPN}@${VPN_IP} -p ${SSHD_PORT_on_VPN} \
    -o ServerAliveInterval=120

# Remote B
autossh -M 0 -fN -R 2221:localhost:22 \
    ${USER_on_VPN}@${VPN_IP} -p ${SSHD_PORT_on_VPN} \
    -o ServerAliveInterval=120

Each remote needs sshd running (sudo systemctl enable --now ssh).

Step 2: Master — connect on demand via ProxyJump

master$ ssh -J ${USER_on_VPN}@${VPN_IP}:${SSHD_PORT_on_VPN} \
    ${USER_on_REMOTE}@localhost -p 2220

That's it. One command, two hops: master → VPN → remote.

Step 3: Master — clean SSH config for easy access

In ~/.ssh/config on master:

# VPN jump host (shared by all remote entries)
Host vpn-jump
    HostName VPN_IP
    Port SSHD_PORT_on_VPN
    User USER_on_VPN

# Individual remotes
Host remote-a
    HostName localhost
    Port 2220
    User USER_on_REMOTE_A
    ProxyJump vpn-jump

Host remote-b
    HostName localhost
    Port 2221
    User USER_on_REMOTE_B
    ProxyJump vpn-jump

Host remote-c
    HostName localhost
    Port 2222
    User USER_on_REMOTE_C
    ProxyJump vpn-jump

Then just:

master$ ssh remote-a    # → VPN → Remote A
master$ ssh remote-b    # → VPN → Remote B
master$ ssh remote-c    # → VPN → Remote C

Security: Restrict Keys at Every Hop

On VPN — each remote's key (tunnel-only)

Each remote gets its own restricted entry, locked to its assigned port:

restrict,port-forwarding,permitlisten="localhost:2220",permitopen="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...remote-a-key...
restrict,port-forwarding,permitlisten="localhost:2221",permitopen="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...remote-b-key...

Each remote can only open its assigned port — no shell, no other forwarding.

On VPN — master's key (ProxyJump relay only)

Master needs permitopen to connect through to the remote tunnel ports, and permitlisten="255.255.255.255:9" to block any -R attempts (master should not open ports on VPN):

restrict,port-forwarding,permitopen="localhost:2220",permitopen="localhost:2221",permitopen="localhost:2222",permitlisten="255.255.255.255:9",command="/bin/false" ssh-ed25519 AAAA...master-key...

This means master can ProxyJump to any registered remote port, but cannot open reverse tunnels on VPN or get a shell. Without the permitlisten block, port-forwarding would allow -R on any port — see the "Understanding permitlisten and permitopen" section for details.

On each remote — master's key (full access)

On each remote's ~/.ssh/authorized_keys, add master's public key without restrictions (or with whatever access level you want):

ssh-ed25519 AAAA...master-key... USER_on_MASTER@master-host

Master is trusted — it gets full shell access on the remotes.

VPN sshd_config hardening

GatewayPorts no              # tunnel ports bind to localhost only
AllowTcpForwarding yes       # need both -R (remotes) and direct-tcpip (master ProxyJump)

Note: we need AllowTcpForwarding yes (not just remote) because master's ProxyJump uses direct-tcpip which is the same mechanism as local forwarding (-L). The per-key permitopen restrictions ensure master can only connect to registered remote tunnel ports.

Making Remote Tunnels Survive Reboots

Option A: systemd service (recommended)

Create /etc/systemd/system/ssh-tunnel-vpn.service on each remote:

[Unit]
Description=Reverse SSH tunnel to VPN
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/autossh -M 0 -N \
    -R 2220:localhost:22 \
    USER_on_VPN@VPN_IP -p SSHD_PORT_on_VPN \
    -o ServerAliveInterval=120 \
    -o ExitOnForwardFailure=yes
Restart=always
RestartSec=10
User=tunnel_user

[Install]
WantedBy=multi-user.target
sudo systemctl enable --now ssh-tunnel-vpn

Option B: crontab

@reboot autossh -M 0 -fN -R 2220:localhost:22 USER_on_VPN@VPN_IP -p SSHD_PORT_on_VPN -o ServerAliveInterval=120

Comparison: Appendix A vs Appendix B

Aspect A: Double Reverse Tunnel B: VPN Hub (this)
Master must be online Yes (persistent tunnel) No (connects on demand)
Sessions needed 3 2
Remote connects to VPN then master (ProxyJump) VPN only
Remote knows about master Yes (has master's key) No (only knows VPN)
Scales to many remotes Awkward (each needs master tunnel) Clean (assign port per remote)
Master goes offline Remotes lose tunnel to master No impact — remotes stay connected to VPN
Best for Both machines always on Fleet of remotes, master connects as needed

Key Insight

This is essentially a poor man's bastion host / jump server. The VPN acts as a rendezvous point where remotes park their sshd via reverse tunnels, and authorized users (master) hop through on demand. The remotes never expose any port to the internet — they only make outbound connections to VPN. The VPN's GatewayPorts no + per-key permitlisten ensures tunnel ports are only reachable from VPN's localhost, and only users whose keys have the right permitopen entries can reach them.

References

SRC = 2026-03-14--ssh-reverse-tunnel-quick-setup-guide.md
PDF = $(SRC:.md=.pdf)
.PHONY: pdf open clean
pdf: $(PDF)
open: $(PDF)
xdg-open $<
PREAMBLE = preamble.tex
$(PDF): $(SRC) $(PREAMBLE)
pandoc -F mermaid-filter -H $(PREAMBLE) --toc --toc-depth=2 -o $@ $<
clean:
rm -f $(PDF)
\usepackage{fancyhdr}
\setlength{\headheight}{14pt}
\setlength{\headsep}{10pt}
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{\small SSH Reverse Tunnel --- Quick Setup Guide}
\fancyfoot[R]{\small\thepage}
\fancypagestyle{plain}{
\fancyhf{}
\fancyhead[L]{\small SSH Reverse Tunnel --- Quick Setup Guide}
\fancyfoot[R]{\small\thepage}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment