Back to home

Reaching a Raspberry Pi behind CGNAT with a reverse SSH tunnel

Written 29 October 2026 · about 6 minutes to read

A friend of mine was so excited about his Raspberry Pi and turning it into a small home server, littles did he know that you almost always get to put in a CGNAT pool in Turkey which restricts you having open ports and stuff for remote connections. We decided to use my Oracle Cloud VPS with a public IP as a relay for his remote connections. I'm explaning how I made his Pi reachable from anywhere through a small server I own with no port forwarding on the home router.

The problem

When youre in a CGNAT pool, many households share one public IP address, so you can make outbound connections but can't accept inbound ones. You need something with a public address that is reachable and ok with inbound connections. There are many services offer solutions to this problem but many are paid, limited, or depend on a third party.

The plan

You can't reach the Pi but Pi can reach to your server. So Pi opens an SSH connection to my VPS, asks it to listen on a port then forwards everything coming from there back to it's own ssh port. The person connecting logs into the VPS first, then hops through that listener to the Pi.

laptop  →  VPS (public IP)  ←  Pi (behind CGNAT)
            127.0.0.1:2222        connects out and keeps
            forwards to Pi:22     the tunnel open

Two details keep this safe; the tunnel listens only on the VPS’s localhost, so it isn’t exposed to the internet, and each account on the VPS is locked to the one thing it needs to do, so a leaked key can’t give anyone a shell on the server.

Steps

  1. On the Pi, create a dedicated key for the tunnel, with no passphrase so the service can start unattended.
    ssh-keygen -t ed25519 -f ~/.ssh/tunnel_key -N ""
    cat ~/.ssh/tunnel_key.pub
  2. On the VPS, create a user for the tunnel and give it the Pi’s public key. adduser doesn’t create ~/.ssh, so make it yourself.
    sudo adduser --disabled-password tunnel
    sudo mkdir -p /home/tunnel/.ssh
    sudo tee /home/tunnel/.ssh/authorized_keys > /dev/null <<'EOF'
    restrict,port-forwarding,permitlisten="127.0.0.1:2222" ssh-ed25519 AAAA... pi-tunnel
    EOF
    sudo chown -R tunnel:tunnel /home/tunnel/.ssh
    sudo chmod 700 /home/tunnel/.ssh
    sudo chmod 600 /home/tunnel/.ssh/authorized_keys
    Replace AAAA... pi-tunnel with the real contents of tunnel_key.pub, all on one line. restrict disables shells and everything else, port-forwarding turns forwarding back on, and permitlisten allows only that single listening port.
  3. On the Pi, install autossh, which restarts the SSH connection whenever it drops.
    sudo apt install autossh
  4. Still on the Pi, run the tunnel as a systemd service so it starts at boot and restarts on failure. Put this in /etc/systemd/system/reverse-tunnel.service, after changing the user and key path to match the Pi’s username.
    [Unit]
    Description=Reverse SSH tunnel to VPS
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    User=example-user
    Environment="AUTOSSH_GATETIME=0"
    ExecStart=/usr/bin/autossh -M 0 -N \
      -o "ServerAliveInterval=30" \
      -o "ServerAliveCountMax=3" \
      -o "ExitOnForwardFailure=yes" \
      -o "StrictHostKeyChecking=accept-new" \
      -i /home/example-user/.ssh/tunnel_key \
      -R 127.0.0.1:2222:localhost:22 \
      tunnel@203.0.113.10
    Restart=always
    RestartSec=10
    
    [Install]
    WantedBy=multi-user.target
    The -R line is the whole thing: listen on port 2222 on the VPS and send it to port 22 on the Pi. ExitOnForwardFailure makes the service not run without a working tunnel and makes it fail.
  5. Start the service and check that it stays up.
    sudo systemctl daemon-reload
    sudo systemctl enable --now reverse-tunnel
    sudo systemctl status reverse-tunnel
  6. On the VPS, confirm the listener exists.
    ss -tln | grep 2222
    You should see a line for 127.0.0.1:2222. If you don’t, the Pi’s side probably isn’t connected yet.
  7. The person who will connect needs their own key. On their laptop, generate one and share the public half.
    ssh-keygen -t ed25519
    cat ~/.ssh/id_ed25519.pub
  8. On the VPS, create a jump account for that person. It can only forward to the tunnel port and can’t open a shell.
    sudo adduser --disabled-password jumpuser
    sudo mkdir -p /home/jumpuser/.ssh
    sudo tee /home/jumpuser/.ssh/authorized_keys > /dev/null <<'EOF'
    restrict,port-forwarding,permitopen="127.0.0.1:2222" ssh-ed25519 AAAA... laptop
    EOF
    sudo chown -R jumpuser:jumpuser /home/jumpuser/.ssh
    sudo chmod 700 /home/jumpuser/.ssh
    sudo chmod 600 /home/jumpuser/.ssh/authorized_keys
    Again, replace the AAAA... laptop section with your own information.
  9. On the Pi, add the laptop’s public key so the person can log into the Pi itself with a key instead of a password.
    echo 'ssh-ed25519 AAAA... laptop' >> ~/.ssh/authorized_keys
  10. On the laptop, add a shortcut to ~/.ssh/config. ProxyJump makes SSH go through the VPS automatically.
    Host mypi
        HostName 127.0.0.1
        Port 2222
        User example-user
        ProxyJump jumpuser@203.0.113.10
    Now ssh mypi reaches the Pi from anywhere.

IP addresses, keys and stuff here are placeholders. Don't forget to replace 203.0.113.10 with your own VPS address, and you can pick any free port above 1024 in place of 2222 if you want to. 2222 is cool because SSH 22 heheh

I know we could have used Tailscale or something similar but it was quite fun configuring this and now my VPS has some real use case too...