170 lines
5.5 KiB
Markdown
170 lines
5.5 KiB
Markdown
# 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
|
|
|
|
```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
|