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.
Metadata
Release files for ssh-tunnel-gateway 0.4.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ssh_tunnel_gateway-0.4.3.tar.gz | 21.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ssh_tunnel_gateway-0.4.3-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.0 kB
Release files / ssh_tunnel_gateway-0.4.3.tar.gz
| Download URL | ssh_tunnel_gateway-0.4.3.tar.gz |
|---|---|
| Size | 21.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2e70011ad7ab39c5ce02506c46cade4a465314c76c67430ac66695a991a1b3d4
|
|
BLAKE2b-256 checksum How to use checksums |
a36a3b6fd0969d915fa2e1010e83860fe6063ab97147c344e745a8033e902fdf
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.14
|
Release files / ssh_tunnel_gateway-0.4.3-py3-none-any.whl
| Download URL | ssh_tunnel_gateway-0.4.3-py3-none-any.whl |
|---|---|
| Size | 22.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5ba5d0e01bd685f41aa8b6834d71e88ae2cfa02ba069502b3d5175fe6097b0d0
|
|
BLAKE2b-256 checksum How to use checksums |
97513b550047b68a5be7eac53f470c7e3e8efef9f4db46449f4b8b2a0b5216fd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.11.14
|