Commit:
2569c845dff126f9a4106edf17c1888532397463Source updated: · Edit this page
Deployment and graceful upgrades¶
Ansible deployment, configuration, fingerprint seeding, and zero-downtime process upgrades.
Installation¶
Download static Linux binaries from the
GitHub releases page, build
from source with Go 1.27.1 or newer, or use the container image published at
ghcr.io/kilo666mj/sshgate.
The container runs as UID 65532. A safe first run keeps the host's sshd on
port 22 and exposes SSHGate on port 2222:
mkdir -p sshgate-data
sudo chown 65532:65532 sshgate-data
docker run --rm --network host \
-v "$PWD/sshgate-data:/var/lib/sshgate" \
ghcr.io/kilo666mj/sshgate:latest \
serve --allow-unknown --route '[::]:2222=127.0.0.1:22'
Host networking lets the container reach an sshd bound to the host's
loopback interface. With ordinary container networking, 127.0.0.1 means the
container itself.
Ansible Deployment¶
The repo includes an Ansible playbook that builds sshgate locally, installs it
under /usr/local/bin, creates a dedicated sshgate system user, and manages a
systemd service:
cd ansible
cp inventory.example inventory
cp group_vars/sshgate.yml.example group_vars/sshgate.yml
# Edit inventory and group_vars/sshgate.yml for your deployment.
ansible-galaxy collection install ansible.posix
ansible-playbook --syntax-check playbook.yml
ansible-playbook playbook.yml
The real inventory and group variables files are ignored so deployment-specific
host names, fingerprints, and settings are not committed accidentally. Override
variables in group_vars/sshgate.yml, the inventory, or with -e as needed:
sshgate_binary: /usr/local/bin/sshgate
sshgate_data_dir: /var/lib/sshgate
sshgate_config_dir: /etc/sshgate
sshgate_routes:
- "[::]:2222=127.0.0.1:22"
sshgate_allow_unknown: false
sshgate_max_fingerprints: 100000
sshgate_metrics_listen: "127.0.0.1:9108"
sshgate_configure_local_prometheus: true
sshgate_prometheus_config: /etc/prometheus/prometheus.yml
sshgate_prometheus_target: "127.0.0.1:9108"
sshgate_control_plane: {}
sshgate_approved_fingerprints: []
sshgate_goarch: amd64
The playbook installs the binary at /usr/local/bin/sshgate, stores runtime state
under /var/lib/sshgate, and installs config at /etc/sshgate/config.json.
Only the data directory is writable by the sshgate user. The default route
listens on port 2222, so the service does not need privileged bind
capabilities. It forwards to 127.0.0.1:22; adjust the backend if your real
sshd listens somewhere else. The playbook cross-compiles a Linux binary
locally with CGO_ENABLED=0, deriving GOARCH from the target host's
architecture unless sshgate_goarch is overridden.
The Ansible default is deny-first: unknown fingerprints are recorded as blocked
and are not forwarded. Set sshgate_allow_unknown: true temporarily during
enrollment if you want new clients to pass through before approval.
The Ansible service exposes Prometheus metrics at
http://127.0.0.1:9108/metrics by default. Set sshgate_metrics_listen to an
empty string to disable it, or to another address when a remote Prometheus
server must scrape the service. Secure any non-loopback listener with firewall
rules or a private monitoring network. Ansible writes the address into
config.json so graceful upgrades can apply changes without replacing the
process command line or dropping live SSH sessions.
When /etc/prometheus/prometheus.yml exists, the playbook also adds a managed
sshgate scrape job, validates the complete configuration with promtool, and
reloads Prometheus. Set sshgate_configure_local_prometheus: false when another
configuration manager owns that file. sshgate_prometheus_config and
sshgate_prometheus_target override the config path and scrape target.
You can also seed approved fingerprints during deployment:
sshgate_approved_fingerprints:
- fingerprint: "0123456789abcdef0123456789abcdef"
label: "Alice laptop"
- fingerprint: "fedcba9876543210fedcba9876543210"
label: "CI deploy key"
Seeding is additive. The playbook approves the listed fingerprints with
--register, preserving existing database entries and leaving fingerprints not
listed in inventory unchanged.
Graceful Upgrades¶
Because sshgate sits inline in the SSH data path, a plain process restart tears
down every live SSH session it is proxying — unlike restarting sshd itself,
whose existing sessions run in separate child processes and survive.
To get the same "restart without dropping connections" behavior, sshgate uses
tableflip. On SIGHUP it re-execs the
binary on disk (picking up a newly installed version), passes the listening
sockets to the new process, and keeps the old process alive to finish serving
its existing connections. New connections go to the new process immediately, so
none are refused during the handoff.
# Trigger a zero-downtime upgrade of a running instance
kill -HUP <pid>
# or, under systemd:
systemctl reload sshgate
The old process stops accepting, stops its background database writers, and waits
for its remaining sessions to close before exiting. --drain-timeout bounds that
wait (default 1h, 0 waits indefinitely) so departing processes cannot pile up
after repeated upgrades while a long-lived session stays open — analogous to
nginx's worker_shutdown_timeout. During the drain window you may briefly see two
sshgate processes, which is expected.
The systemd unit is Type=notify with NotifyAccess=all: after an upgrade the
new server reports its PID to systemd via sd_notify, so systemctl keeps
tracking the process that is actually serving. The Ansible deployment reloads
(rather than restarts) on binary and config changes, so a deploy does not drop
active sessions — including Ansible's own connection when it runs through
sshgate.
Note that fingerprint approve/block changes never require a restart at all: each
new connection reads its verdict from the database live. Only binary upgrades and
--route/config changes need the process to reload.