This moots the entire need for argo tunnels using ssh which is already basically perfect and traefik and labels n so on.
Go to file
Leopere f6b40d2432
Fix SSH to ingress: port 65522, auto-load companion cert
- keyutil.go / client ssh.go: if <key>-cert.pub exists next to
  the private key, load it automatically (mirrors openssh behavior)
- stack.production.yml: TRAEFIK_SSH_HOST uses port 65522

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 18:38:31 -05:00
cmd Rework for Swarm deploy on ingress.nixc.us 2026-02-08 18:24:13 -05:00
internal Fix SSH to ingress: port 65522, auto-load companion cert 2026-02-08 18:38:31 -05:00
.gitignore Add docker-compose.override.yml to gitignore 2026-02-08 18:27:03 -05:00
.woodpecker.yml Rework for Swarm deploy on ingress.nixc.us 2026-02-08 18:24:13 -05:00
Dockerfile Initial commit: reverse SSH tunnel server for Traefik 2026-02-08 18:16:41 -05:00
README.md Initial commit: reverse SSH tunnel server for Traefik 2026-02-08 18:16:41 -05:00
docker-compose.production.yml Add Woodpecker CI, production stack, and compose files 2026-02-08 18:18:54 -05:00
docker-compose.yml Add Woodpecker CI, production stack, and compose files 2026-02-08 18:18:54 -05:00
go.mod Initial commit: reverse SSH tunnel server for Traefik 2026-02-08 18:16:41 -05:00
go.sum Initial commit: reverse SSH tunnel server for Traefik 2026-02-08 18:16:41 -05:00
stack.production.yml Fix SSH to ingress: port 65522, auto-load companion cert 2026-02-08 18:38:31 -05:00

README.md

Reverse SSH Tunnel Server for Traefik

A lightweight Go system that lets remote Docker hosts expose HTTP services through a central Traefik reverse proxy via SSH tunnels. The server SSHs into the Traefik ingress host to dynamically manage route configuration.

Architecture

Remote Host                        Tunnel Server              Traefik Host (ingress.nixc.us)
+--------------+                   +---------------------+    +---------------------+
| tunnel-client| ---SSH tunnel---> | tunnel-server       |    | Traefik             |
| TUNNEL_KEY   |                   | allocates port,     |    | watches dynamic/    |
| TUNNEL_DOMAIN|                   | SSHs into Traefik   |--->| routes HTTPS to     |
| TUNNEL_PORT  |                   | host to write config |    | tunnel-server ports |
+--------------+                   +---------------------+    +---------------------+

Flow

  1. Client connects to tunnel-server via SSH using a private key
  2. Client sends domain metadata ({"domain":"myapp.example.com"}) over a custom channel
  3. Server allocates a port from the pool and sets up a reverse port forward
  4. Server SSHs into ingress.nixc.us and writes a Traefik dynamic config file
  5. Traefik detects the new config and routes HTTPS traffic to tunnel-server:<port>
  6. Traffic flows: Internet -> Traefik -> tunnel-server:port -> SSH tunnel -> client -> local service
  7. When the client disconnects, the config file is removed and the port is freed

Quick Start

1. Generate SSH Keys

mkdir -p keys

# Server host key (for the SSH server that clients connect to)
ssh-keygen -t ed25519 -f keys/host_key -N ""

# Client key (for tunnel clients)
ssh-keygen -t ed25519 -f keys/id_ed25519 -N ""

# Authorize the client
cat keys/id_ed25519.pub > keys/authorized_keys

# Deploy key for SSHing into the Traefik host
# (use an existing key that has root access to ingress.nixc.us,
#  or generate one and add its .pub to the Traefik host's authorized_keys)
cp ~/.ssh/ingress_deploy_key keys/traefik_deploy_key

2. Enable Traefik File Provider

On ingress.nixc.us, ensure Traefik has the file provider enabled. If Traefik is run via Docker stack with CLI args, add:

--providers.file.directory=/root/traefik/dynamic
--providers.file.watch=true

Or via SSH (non-interactive):

ssh root@ingress.nixc.us "mkdir -p /root/traefik/dynamic"

Then update your Traefik stack/service to mount the directory and add the CLI args.

3. Start the Tunnel Server

docker compose up -d

4. Run a Client

On any remote host where your service is running:

Using a mounted key file:

docker run -d \
  --name tunnel-client \
  -e TUNNEL_SERVER=ingress.nixc.us:2222 \
  -e TUNNEL_DOMAIN=myapp.example.com \
  -e TUNNEL_PORT=8080 \
  -e TUNNEL_KEY=/keys/id_ed25519 \
  -v /path/to/keys:/keys:ro \
  --network host \
  tunnel-client

Using raw PEM key content in an envvar:

docker run -d \
  --name tunnel-client \
  -e TUNNEL_SERVER=ingress.nixc.us:2222 \
  -e TUNNEL_DOMAIN=myapp.example.com \
  -e TUNNEL_PORT=8080 \
  -e "TUNNEL_KEY=$(cat /path/to/id_ed25519)" \
  --network host \
  tunnel-client

Environment Variables

Server

Variable Description Default
SSH_PORT SSH listen port for tunnel clients 2222
PORT_RANGE_START First allocatable tunnel port 10000
PORT_RANGE_END Last allocatable tunnel port 10100
SSH_HOST_KEY Path to SSH host private key /keys/host_key
AUTHORIZED_KEYS Path to authorized_keys file /keys/authorized_keys
TRAEFIK_SSH_HOST Traefik host to SSH into (required) -
TRAEFIK_SSH_USER SSH user on the Traefik host root
TRAEFIK_SSH_KEY SSH key for Traefik host (path or PEM) (required) -
TRAEFIK_CONFIG_DIR Remote path for dynamic configs /root/traefik/dynamic
TRAEFIK_ENTRYPOINT Traefik entrypoint name websecure
TRAEFIK_CERT_RESOLVER Traefik TLS cert resolver letsencryptresolver

Client

Variable Description Default
TUNNEL_SERVER Server host:port (required) -
TUNNEL_DOMAIN Public domain to expose (required) -
TUNNEL_PORT Local port of your service 8080
TUNNEL_KEY SSH private key — file path or raw PEM (required) /keys/id_ed25519

Generated Traefik Config

When a client connects requesting myapp.example.com, the server writes this to the Traefik host:

# /root/traefik/dynamic/tunnel-myapp-example-com.yml
http:
  routers:
    tunnel-myapp-example-com-router:
      rule: "Host(`myapp.example.com`)"
      entryPoints:
        - websecure
      tls:
        certResolver: letsencryptresolver
      service: tunnel-myapp-example-com-service
  services:
    tunnel-myapp-example-com-service:
      loadBalancer:
        servers:
          - url: "http://tunnel-server:10042"

Building

# Build both binaries locally
go build -o tunnel-server ./cmd/server/
go build -o tunnel-client ./cmd/client/

# Build Docker images
docker compose build                                # server image
docker build --target client -t tunnel-client .     # client image

Security Notes

  • Only clients whose public keys are in authorized_keys can connect
  • The server uses a stable host key for client verification
  • SSH tunnels encrypt all traffic between client and server
  • The server authenticates to the Traefik host with a separate deploy key
  • Traefik handles TLS termination with automatic Let's Encrypt certificates