Skip to main content

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)

  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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ssh_tunnel_gateway-0.4.3.tar.gz (21.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ssh_tunnel_gateway-0.4.3-py3-none-any.whl (22.9 kB view details)

Uploaded Python 3

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

Hashes for ssh_tunnel_gateway-0.4.3.tar.gz
Algorithm Hash digest
SHA256 2e70011ad7ab39c5ce02506c46cade4a465314c76c67430ac66695a991a1b3d4
MD5 37d499d7de02d30c5be3b70b0dc0cb92
BLAKE2b-256 a36a3b6fd0969d915fa2e1010e83860fe6063ab97147c344e745a8033e902fdf

See more details on using hashes here.

File details

Details for the file ssh_tunnel_gateway-0.4.3-py3-none-any.whl.

File metadata

File hashes

Hashes for ssh_tunnel_gateway-0.4.3-py3-none-any.whl
Algorithm Hash digest
SHA256 5ba5d0e01bd685f41aa8b6834d71e88ae2cfa02ba069502b3d5175fe6097b0d0
MD5 0e17a465d6c4dfa293ed59ffd52eb1ad
BLAKE2b-256 97513b550047b68a5be7eac53f470c7e3e8efef9f4db46449f4b8b2a0b5216fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page