Skip to main content

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.

  1. Start gateway server:
API_KEY="change-me" ssh-tunnel-server
  1. Start agent:
ssh-tunnel-agent --api-key change-me --endpoint http://<gateway_host>:12000
  1. Read allocated port_b from 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"])'
  1. 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
  1. 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/tcp for SSH data plane
  • 12000/tcp for 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/config by default, or SSH_CONFIG_PATH).
  • If alias is missing, agent falls back to standard endpoint mode.
  • In --over-ssh mode, API_URL host/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_KEY or API_KEYS (comma-separated).
  • API endpoint is POST / with JSON action:
  • register: allocate or reuse port_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_b mapping 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 7 days by default (LEASE_TTL_DAYS); expired agents are cleaned from state and authorized_keys.
  • Restarting ssh-tunnel-server does not restart existing tunnel processes in sshd; 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 remote is 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 / with action=register|heartbeat).
  • Keep agent identity stable (cached agent_id) and reuse port_b when 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: 7 days (LEASE_TTL_DAYS)

--over-ssh Rules

  • --over-ssh <alias> must match an alias in SSH config (~/.ssh/config or SSH_CONFIG_PATH).
  • If alias is missing, fallback to standard endpoint mode.
  • In --over-ssh mode, ignore API_URL host and use only its port.
  • Use SSH alias directly as destination (do not force user@alias).

Systemd Rules

  • Use -d for 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, and connect_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

  1. Update VERSION.
  2. Verify README reflects current behavior and flags.
  3. Build: make build.
  4. Verify CLI versions: ssh-tunnel-server --version, ssh-tunnel-agent --version.
  5. Upload: make upload.
  6. 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)

Source distribution for ssh-tunnel-gateway 0.4.3
File Size Uploaded
ssh_tunnel_gateway-0.4.3.tar.gz 21.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ssh-tunnel-gateway 0.4.3
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.4.3 This release

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

1 release file

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page