gatewaysshd is a daemon that provides a meeting place for all your SSH
tunnels. It is especially useful when you have many hard-to-reach machines
running behind firewalls and NAT, and you want to reach services on them over
SSH from anywhere in the world — without opening a single inbound port on
those machines.
Machines behind firewalls keep a persistent SSH connection to the gateway and remote-forward the ports they want to expose, giving each one a service name:
workstation$ ssh -T -N workstation@gateway -R ssh:22:localhost:22 -R web:80:localhost:80
The gateway does not actually open those ports on the server. Forwarded ports are a virtual concept: the gateway just remembers who advertised what, and connects the two ends internally when another client asks for the service. This relieves you of assigning and managing real port numbers on the server.
From anywhere else, a service is addressed as service.username, like a
hostname:
laptop$ ssh -T -N username@gateway -L 2222:ssh.workstation:22
laptop$ ssh -p 2222 localhost
Authentication is by SSH user certificates only: every client presents a certificate signed by your certificate authority, and the gateway records the users, their reported status, and their geolocation in postgres.
- Virtual port forwarding — expose services by name (
web.alice), no server-side port allocation, no inbound ports on the client machines. - Certificate-based authentication — a single CA public key controls who can connect; per-user permissions (port forwarding, administrator) are carried in the certificate and the database.
- Built-in shell —
ssh username@gatewaygives youstatus,listUsers,getUser,kickUser(admin),ping, andversioncommands. - Status reporting — clients can push arbitrary JSON status and screenshots that are stored per user and served over the HTTP API.
- HTTP API —
/api/user,/api/user/{id},/api/user/{id}/screenshotfor dashboards and monitoring. - SOCKS5 and HTTP proxies (optional) — expose a SOCKS5 or HTTP
forward-proxy port so any client can reach exposed services by name
(
service.username), the same reachability as anssh -Ddynamic forward. Disabled by default; see the security note below. - GeoIP — optionally resolves each user's location from a MaxMind database.
- Mesh peering — multiple gateway instances share one postgres database and form a mesh, so a user on one node can tunnel to a user on another node over the same SSH service port. Inter-node trust uses a separate peer certificate authority layered on top of each node's existing user CA, so existing single-node deployments keep their user-facing CA unchanged.
- Hardened crypto defaults — insecure key exchanges, ciphers, and MACs are disabled out of the box.
Prebuilt binaries for linux, macOS, and windows are on the releases page.
With docker (a distroless image, the binary is the entrypoint; amd64 and arm64 images are published to Docker Hub on every release):
$ docker pull ziyan/gatewaysshd
Or build the image yourself:
$ docker build -t ziyan/gatewaysshd .
Or build from source (Go 1.25+):
$ git clone https://github.com/ziyan/gatewaysshd.git
$ cd gatewaysshd
$ make build
-
Create a certificate authority and keys. Follow SSH.md for the full walkthrough — generate a CA key, a signed host certificate for the gateway, and a signed user certificate for each client.
-
Start postgres. The gateway stores users in a postgres database:
$ docker run -d --name gatewaysshd-db \ -e POSTGRES_DB=gatewaysshd \ -e POSTGRES_USER=gatewaysshd \ -e POSTGRES_PASSWORD=gatewaysshd \ -p 5432:5432 postgres -
Run the gateway:
$ gatewaysshd \ --listen-ssh :2020 \ --listen-http 127.0.0.1:2080 \ --ca-public-key id_rsa.ca.pub \ --host-private-key id_rsa.gateway \ --host-public-key id_rsa.gateway-cert.pub \ --geoip-database geoip.mmdbRun
gatewaysshd --helpfor the full list of flags, including postgres connection settings, idle timeout, and the debug endpoint. -
Connect a client:
$ ssh -i ~/.ssh/id_rsa.alice -p 2020 alice@gateway -R web:80:localhost:80
Connecting without a command drops you into a small shell:
$ ssh -p 2020 alice@gateway
Welcome to gatewaysshd version 0.4.0! Type "help" to get a list of available commands.
gatewaysshd> status
gatewaysshd> listUsers
gatewaysshd> getUser bob
gatewaysshd> exit
status, listUsers, and getUser require the permit-port-forwarding
certificate extension; kickUser additionally requires the user to be marked
as an administrator in the database.
Optionally, the gateway can expose forward proxies so clients that are not
SSH tunnels can still reach exposed services by name. This is the equivalent
of ssh -D: the proxy resolves service.username (locally or across the
mesh) and bridges to it.
$ gatewaysshd \
... \
--listen-socks 127.0.0.1:1080 \
--listen-http-proxy 127.0.0.1:8080
Then, for a service web exposed by user alice:
$ curl --socks5-hostname 127.0.0.1:1080 http://web.alice/
$ curl --proxy http://127.0.0.1:8080 http://web.alice/
The HTTP proxy supports both CONNECT (tunneling arbitrary TCP) and
absolute-form requests (plain HTTP forwarding).
Security: both proxies are unauthenticated — anyone who can reach the proxy port can reach any exposed service, bypassing SSH certificate auth. They are disabled by default; only enable them bound to a trusted network (e.g.
127.0.0.1or a private interface).
Several gateway instances can share a single postgres database and form a
mesh. When a user connects to any node, the node it landed on is recorded on
the user record. When another user asks to tunnel to service.username, the
node resolves which node that user is on and forwards the tunnel to that node
over an outbound peer connection — reusing the same SSH service port, no
extra listener.
Inter-node trust is a separate peer certificate authority, layered on top
of each node's existing user CA. Peer nodes authenticate as the reserved user
peer with a certificate signed by the peer CA (key id = node id); those
certificates are checked only against --peer-ca-public-key and never grant
user capabilities. Existing single-node deployments keep their user CA, host
key, and service port unchanged — mesh is enabled purely by adding node
flags:
$ gatewaysshd \
... \
--node-id node-us \
--node-address gateway-us.example.com:2020 \
--node-certificate id_rsa.node-us-cert.pub \
--peer-ca-public-key id_rsa.peer-ca.pub
Nodes without direct database access can reach the central postgres through a peer node's SSH service port instead of a separate connection:
$ gatewaysshd \
... \
--postgres-peer gateway-us.example.com:2020 \
--postgres-peer-host-public-key gateway-us.pub \
--node-certificate id_rsa.node-jp-cert.pub
$ make format # gofmt the tree
$ make build # build into build/gatewaysshd
$ make test # run tests (spins up a temporary postgres container)
$ make lint # golangci-lint
$ make docker # build the docker image
Integration tests need docker: make test launches a disposable postgres
container and points the test suite at it via GATEWAYSSHD_TEST_DATABASE_HOST.
Without that variable, database-backed tests skip and the rest of the suite
still runs.
- Commit messages follow conventional commits
(
feat: …,fix: …); the release bot derives the next version from them. - Every pull request description includes a changelog block (pre-filled by
the PR template). On each release, the bot collects the blocks of all
merged PRs into CHANGELOG.md. Apply the
skip-changeloglabel if your change has no user-visible effect. - Releases are fully automated: merging to
mastertriggers the release bot, which versions, updates the changelog, tags, and publishes binaries.