# 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:` 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 ```bash 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): ```bash 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 ```bash docker compose up -d ``` ### 4. Run a Client On any remote host where your service is running: **Using a mounted key file:** ```bash 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:** ```bash 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: ```yaml # /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 ```bash # 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