SSH tunnel server + agent with reverse port forwarding
Project description
ssh-tunnel-gateway
Single Python package that ships both CLIs:
ssh-tunnel-server(control plane + port allocation)ssh-tunnel-agent(reverse SSH tunnel on private host)
This project uses a public gateway as a bastion to reach private/internal servers over standard SSH.
Install
Install on both gateway and agent hosts:
pip install ssh-tunnel-gateway
SKILLS.md
Session skill guide for coding/release agents is in SKILLS.md.
Quick Start (Recommended)
- Start gateway server:
API_KEY="change-me" ssh-tunnel-server
- Start agent:
ssh-tunnel-agent --api-key change-me --endpoint http://<gateway_host>:12000
- Read allocated
port_bfrom agent session file:
cat ~/.ssh-tunnel/session.json
python3 -c 'import json, os; print(json.load(open(os.path.expanduser("~/.ssh-tunnel/session.json")))["port_b"])'
- Add client SSH config (
~/.ssh/config):
Host GWServer
HostName <gateway_public_ip_or_dns>
User <gateway_ssh_user>
Port 22
Host AgentServer
HostName localhost
User <agent_ssh_user>
Port <port_b>
ProxyJump GWServer
- Connect:
ssh AgentServer
Control Plane Modes
Standard mode
Agent calls HTTP API directly:
ssh-tunnel-agent --api-key change-me --endpoint http://<gateway_host>:12000
Gateway requirements:
22/tcpfor SSH data plane12000/tcpfor HTTP control plane
--over-ssh mode
Agent reaches control plane through SSH local forwarding (no public 12000 required):
ssh-tunnel-agent --api-key change-me --endpoint http://127.0.0.1:12000 --over-ssh GWServer
SSH alias example:
Host GWServer
HostName <gateway_public_ip_or_dns>
User <gateway_ssh_user>
Port 22
Important behavior:
- Alias match is strict against SSH config (
~/.ssh/configby default, orSSH_CONFIG_PATH). - If alias is missing, agent falls back to standard endpoint mode.
- In
--over-sshmode,API_URLhost/IP is ignored; only the API port is used. - Agent prefers binding the same local API port first; if unavailable, it picks a free local port.
Systemd Deployment
Use -d for systemd (run once and let systemd restart on failure).
Server unit (/etc/systemd/system/ssh-tunnel-server.service):
[Unit]
Description=ssh-tunnel-gateway server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=gw-tunnel
WorkingDirectory=/home/gw-tunnel
EnvironmentFile=/etc/ssh-tunnel/server.env
ExecStart=/usr/local/bin/ssh-tunnel-server -d
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Server env (/etc/ssh-tunnel/server.env):
# Required:
API_KEY=change-me
# Optional (use instead of API_KEY for multiple keys):
# API_KEYS=key1,key2,key3
# Optional overrides:
# STATE_DIR=/home/gw-tunnel/.ssh-tunnel/server
# SERVER_HOST=0.0.0.0
# SERVER_PORT=12000
# SSH_USER=gw-tunnel
# SSH_JUMP_USER=li
# SSH_PUBLIC_HOST=gateway.example.com
# AUTHORIZED_KEYS_PATH=/home/gw-tunnel/.ssh/authorized_keys
# AGENT_REVERSE_BIND_HOST=0.0.0.0
Agent unit (/etc/systemd/system/ssh-tunnel-agent.service):
[Unit]
Description=ssh-tunnel-gateway agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=li
WorkingDirectory=/home/li
EnvironmentFile=/etc/ssh-tunnel/agent.env
ExecStart=/usr/local/bin/ssh-tunnel-agent -d
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
Agent env (/etc/ssh-tunnel/agent.env):
# Required:
API_KEY=change-me
API_URL=http://<gateway_host>:12000
# Optional overrides:
# STATE_DIR=/home/li/.ssh-tunnel
# SSH_CONFIG_PATH=/home/li/.ssh/config
# SSH_HOST=<gateway_host>
# SSH_PORT=22
# LOCAL_TARGET_HOST=127.0.0.1
# LOCAL_TARGET_PORT=22
# REVERSE_BIND_HOST=0.0.0.0
# Optional control-plane over SSH (if enabled, API_URL host is ignored and port is used):
# OVER_SSH=GWServer
# API_URL=http://127.0.0.1:12000
Enable services:
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-server
sudo systemctl enable --now ssh-tunnel-agent
Systemd note:
- Use absolute paths in env files (
/home/<user>/...) instead of~.
Operational Behavior
- Server API authentication supports
API_KEYorAPI_KEYS(comma-separated). - API endpoint is
POST /with JSONaction: register: allocate or reuseport_b, return encrypted key material.heartbeat: refresh lease.- Agent keeps a stable local
agent_id(UUID) unless explicitly overridden. - Agent writes session state to
${STATE_DIR}/session.json(default~/.ssh-tunnel/session.json). - Server keeps
agent_id -> port_bmapping and reuses the same port when possible. - If reused port is busy, server tries reclaiming it; if reclaim fails, server allocates a new one.
- Lease timeout is
7days by default (LEASE_TTL_DAYS); expired agents are cleaned from state andauthorized_keys. - Restarting
ssh-tunnel-serverdoes not restart existing tunnel processes insshd; active tunnels stay up while their SSH sessions stay up.
SSHD Requirements (Gateway)
For default public reverse bind (AGENT_REVERSE_BIND_HOST=0.0.0.0):
AllowTcpForwarding remote
GatewayPorts clientspecified
For loopback-only reverse bind (AGENT_REVERSE_BIND_HOST=127.0.0.1):
AllowTcpForwarding remoteis still required.
Security Guidance
- Use a dedicated SSH tunnel user on gateway (for example
gw-tunnel). - Block shell/TTY for that user and allow only remote port forwarding.
- Keep client jump access (
SSH_JUMP_USER) separate from tunnel login (SSH_USER) when needed.
Example sshd_config hardening:
Match User gw-tunnel
PasswordAuthentication no
PubkeyAuthentication yes
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
PermitUserRC no
AllowTcpForwarding remote
Agent Skills (From SKILLS.md)
Scope
- Single package:
ssh-tunnel-gateway - Two CLIs:
ssh-tunnel-server,ssh-tunnel-agent - Transport model: SSH data plane (
22) + HTTP control plane (12000) or--over-ssh
Design Intent
- Keep SSH native (
ProxyJump, standard ssh config, standard sshd settings). - Keep control plane minimal (
POST /withaction=register|heartbeat). - Keep agent identity stable (cached
agent_id) and reuseport_bwhen possible. - Prefer explicit and operationally safe defaults.
Runtime Defaults
- Agent state directory default:
~/.ssh-tunnel - Agent session file default:
~/.ssh-tunnel/session.json - Agent key file default:
~/.ssh-tunnel/agent.pem - Agent id file default:
~/.ssh-tunnel/agent_id - Lease cleanup default:
7days (LEASE_TTL_DAYS)
--over-ssh Rules
--over-ssh <alias>must match an alias in SSH config (~/.ssh/configorSSH_CONFIG_PATH).- If alias is missing, fallback to standard endpoint mode.
- In
--over-sshmode, ignoreAPI_URLhost and use only its port. - Use SSH alias directly as destination (do not force
user@alias).
Systemd Rules
- Use
-dfor both CLIs under systemd. - Prefer absolute paths in env files.
- Set
User=explicitly so home expansion resolves as expected. - Keep optional env vars commented in example env files.
Logging Expectations
- Agent logs register result with
agent_id,port_b,ssh_user,jump_user,jump_host, andconnect_hint. - Server logs register/heartbeat with client IP and key operational fields.
- Foreground mode should provide enough info to debug without inspecting code.
Release Checklist
- Update
VERSION. - Verify README reflects current behavior and flags.
- Build:
make build. - Verify CLI versions:
ssh-tunnel-server --version,ssh-tunnel-agent --version. - Upload:
make upload. - If PyPI says file exists, bump version and rebuild.
Documentation Style
- Put usage and copy/paste commands before deep details.
- Keep server and agent examples separate and explicit.
- Document required env vars as active lines and optional ones as comments.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ssh_tunnel_gateway-0.4.3.tar.gz.
File metadata
- Download URL: ssh_tunnel_gateway-0.4.3.tar.gz
- Upload date:
- Size: 21.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e70011ad7ab39c5ce02506c46cade4a465314c76c67430ac66695a991a1b3d4
|
|
| MD5 |
37d499d7de02d30c5be3b70b0dc0cb92
|
|
| BLAKE2b-256 |
a36a3b6fd0969d915fa2e1010e83860fe6063ab97147c344e745a8033e902fdf
|
File details
Details for the file ssh_tunnel_gateway-0.4.3-py3-none-any.whl.
File metadata
- Download URL: ssh_tunnel_gateway-0.4.3-py3-none-any.whl
- Upload date:
- Size: 22.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5ba5d0e01bd685f41aa8b6834d71e88ae2cfa02ba069502b3d5175fe6097b0d0
|
|
| MD5 |
0e17a465d6c4dfa293ed59ffd52eb1ad
|
|
| BLAKE2b-256 |
97513b550047b68a5be7eac53f470c7e3e8efef9f4db46449f4b8b2a0b5216fd
|