Metadata-Version: 2.4
Name: ssh-tunnel-gateway
Version: 0.4.0
Summary: SSH tunnel server + agent with reverse port forwarding
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: fastapi>=0.110
Requires-Dist: uvicorn[standard]>=0.29
Requires-Dist: requests>=2.31
Requires-Dist: cryptography>=41

# ssh-tunnel-gateway

Single Python package that ships both the server and agent CLIs.

## Project Description

`ssh-tunnel-gateway` provides a gateway server (`ssh-tunnel-server`) and an agent (`ssh-tunnel-agent`) for reverse SSH access to hosts that are not directly reachable from clients.

The server manages control-plane registration over HTTP and allocates a tunnel port (`port_b`).  
The agent establishes and maintains the SSH reverse tunnel data plane to that allocated port.

## Design Philosophy

- Use a public gateway host as a bastion to reach private/internal servers that do not expose inbound SSH.
- Use standard SSH as the transport layer for mature security properties and operational reliability.
- Stay native to SSH workflows so teams can integrate with existing clients, keys, config files, and `ProxyJump`.
- Keep control-plane logic minimal: HTTP API only for registration, identity, and lease lifecycle.

## Usage (Start Here)

Server foreground:

```bash
API_KEY="change-me" ssh-tunnel-server
```

Server systemd mode:

```bash
API_KEY="change-me" ssh-tunnel-server -d
```

Agent foreground:

```bash
ssh-tunnel-agent --api-key change-me --endpoint http://server:12000
```

Agent foreground (control plane over SSH config host alias):

```bash
ssh-tunnel-agent --api-key change-me --endpoint http://127.0.0.1:12000 --over-ssh GWServer
```

Example SSH config alias:

```sshconfig
Host GWServer
    HostName <gateway_public_ip_or_dns>
    User <gateway_ssh_user>
    Port 22
```

Agent systemd mode:

```bash
ssh-tunnel-agent -d --api-key change-me --endpoint http://server:12000
```

Notes:
- Server auth supports either `API_KEY` (single key) or `API_KEYS` (comma-separated multiple keys).
- Restarting `ssh-tunnel-server` does not restart active SSH tunnels; existing tunnels stay up as long as the underlying SSH session to gateway `sshd` remains alive.
- `--over-ssh <ssh_config_host>` makes agent reach API through SSH local forwarding over port `22`, so gateway `12000` does not need to be publicly exposed.
- `--over-ssh <ssh_config_host>` is used only when that host alias exists in SSH config (`~/.ssh/config` by default, or `SSH_CONFIG_PATH`).
- `--over-ssh` alias matching is intentionally strict by design; if alias check does not pass, agent falls back to standard endpoint mode.
- In `--over-ssh` mode, `API_URL` host/IP is ignored for compatibility; agent uses only the `API_URL` port and forwards to gateway `127.0.0.1:<port>`.
- If the alias is not found, `--over-ssh` is ignored for both control-plane API and default tunnel host selection.
- In `--over-ssh` mode, agent prefers the same local endpoint port as `API_URL`; if that local port is unavailable, it falls back to a free local port.
- If `--agent-id` is not provided, agent generates a UUID once and caches it locally for future restarts.
- Agent writes current session info (including `port_b`) to `${STATE_DIR}/session.json` by default.
- Server reuses the same `port_b` for the same `agent_id` when possible.
- If the previous port is busy, server tries to reclaim (kill listener on that port) and reuse it.
- If reclaim fails, server rotates to a new free port.
- Default reverse bind host is `0.0.0.0` (public bind on gateway).
- Use `--reverse-bind-host 127.0.0.1` if you want loopback-only bind for strict bastion usage.
- If not set, agent follows the server-provided reverse bind host.
- In `-d` mode, register/startup failures exit immediately so systemd can restart the unit.

## Install

On the gateway server host:

```bash
pip install ssh-tunnel-gateway
```

On each agent host:

```bash
pip install ssh-tunnel-gateway
```

## Quick Start (ProxyJump)

1. On the gateway server host, start server:

```bash
API_KEY="change-me" ssh-tunnel-server
```

Or with multiple API keys:

```bash
API_KEYS="key1,key2,key3" ssh-tunnel-server
```

With default public reverse bind (`AGENT_REVERSE_BIND_HOST=0.0.0.0`), gateway `sshd_config` must include:
- `AllowTcpForwarding remote`
- `GatewayPorts clientspecified`

To avoid exposing gateway port `12000`, run server API on localhost:

```bash
API_KEY="change-me" SERVER_HOST=127.0.0.1 SERVER_PORT=12000 ssh-tunnel-server
```

2. On the agent host, start agent:

```bash
ssh-tunnel-agent --api-key change-me --endpoint http://<gateway_host>:12000
```

Or over SSH control tunnel (no public `12000`):

```bash
ssh-tunnel-agent --api-key change-me --endpoint http://127.0.0.1:12000 --over-ssh GWServer
```

3. Get `port_b` from the agent side:
- Foreground mode prints a log line with `port_b`.
- Foreground logs also print:
  - `ssh_user`: tunnel login user returned by server.
  - `jump_user`: ProxyJump user returned by server.
  - `agent_user_hint`: defaults to local `$USER` on the agent host.
- Session file is always written to `${STATE_DIR}/session.json` (default `./data/session.json`):

```bash
cat ./data/session.json
```

Get only `port_b`:

```bash
python3 -c 'import json; print(json.load(open("./data/session.json"))["port_b"])'
```

4. Add user SSH config (`~/.ssh/config`):

```sshconfig
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_from_agent_session_json>
    ProxyJump GWServer
```

5. Connect:

```bash
ssh AgentServer
```

## Systemd Usage

Server unit (`/etc/systemd/system/ssh-tunnel-server.service`):

```ini
[Unit]
Description=ssh-tunnel-gateway server
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=gw-tunnel
WorkingDirectory=/opt/ssh-tunnel
EnvironmentFile=/etc/ssh-tunnel/server.env
ExecStart=/usr/local/bin/ssh-tunnel-server -d
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Example server env (`/etc/ssh-tunnel/server.env`):

```bash
API_KEY=change-me
# Or multiple keys:
# API_KEYS=key1,key2,key3
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`):

```ini
[Unit]
Description=ssh-tunnel-gateway agent
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=root
WorkingDirectory=/opt/ssh-tunnel
EnvironmentFile=/etc/ssh-tunnel/agent.env
ExecStart=/usr/local/bin/ssh-tunnel-agent -d
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

Example agent env (`/etc/ssh-tunnel/agent.env`):

```bash
API_KEY=change-me
API_URL=http://<gateway_host>:12000
SSH_HOST=<gateway_host>
SSH_PORT=22
LOCAL_TARGET_HOST=127.0.0.1
LOCAL_TARGET_PORT=22
# Optional control over SSH config alias mode:
# OVER_SSH=GWServer
# API_URL=http://127.0.0.1:12000
```

Enable and start:

```bash
sudo systemctl daemon-reload
sudo systemctl enable --now ssh-tunnel-server
sudo systemctl enable --now ssh-tunnel-agent
```
