Self-hosted service
tg123/sshpiper avatar
tg123/sshpiper

sshpiper: a reverse proxy for sshd where the plugin decides the route and the authentication

The missing reverse proxy for ssh scp

1,335 stars171 forksGoMIT

At a glance

What is it?
sshpiper sits between an ssh client and an ssh server, and a plugin decides where each connection goes and how authentication is presented upstream. The v1 line ships eight plugins, a native end to end suite for Windows and macOS, and an admin API that can list and kill live sessions.
Who is it for?
Adopt sshpiper when you need one entry point for many ssh servers, want the upstream to see a different authentication method than the client used, or need an audit trail of live sessions rather than log files. Do not adopt it if you need the Docker and Kubernetes plugins on macOS or Windows, since the release config marks those as Linux-only, or if you need the simplemath challenge plugin, which is not shipped.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three moving parts: downstream, upstream and the plugin in between

The README defines the vocabulary before anything else, and the definitions are the design. The `downstream` is the client side, typically an ssh client. The `upstream` is the server side, typically an ssh server. The `plugin` handles the routing from downstream to upstream, and it is also responsible for mapping authentication methods to the upstream server.

That last clause is the interesting one and it is the reason sshpiper is more than a TCP forwarder. The README's example is that the downstream may use password authentication while the upstream server receives public key authentication mapped by sshpiper. A conventional jump host would give the client a shell on the bastion; sshpiper gives the client a pipe to the target, and the plugin decides what credentials the target is presented with.

The diagram in the README shows two clients, Bob and Alice, connecting to a single sshpiper process and being routed by name to two different upstream machines. The pipe-by-name step is what makes the target server unaware of the proxy, which is the point of a transparent proxy design.

A third term, `additional challenge`, covers plugins that do not only route but also add a challenge to the SSH authentication for the upstream server, for instance asking the downstream for two-factor authentication provided by the plugin. The topics listed for the repository include two-factor authentication, auditing, recording and bastion use, so this is the direction the project has been taken.

The plugin chain is the configuration language

The daemon's command line shows the shape of the thing: the sshpiperd binary is given a plugin path, and plugins can be chained. The README's simple demo runs the `fixed` plugin against a target address, and the challenge example chains `simplemath` in front of `fixed`.

The plugin set in v1 is a set of routing strategies. `fixed` always goes to one target. `workingdir` remaps the username based on a directory, and the test table says it also rejects users that do not exist. `yaml` does the same remapping with a regular expression and rejects unmatched users. `username-router` parses the target and port, and the upstream user, out of the username the client typed, and rejects malformed usernames. `lua` does routing and username remapping through a script. `failtoban` chains with `fixed` and bans an address after failed authentication, with loopback allowlisting. `metrics` chains with `fixed` and exposes an HTTP connection gauge plus error counters. `revtunnel` handles public key registration, password-authenticated reverse forwarding, a native file session store and revocation.

The `fixed` and `failtoban` and `metrics` plugins are all described as chaining with `fixed`, which tells you `fixed` is the primitive that the others wrap rather than a peer of them.

The plugin contract lives in `libplugin/`, the plugin implementations in `plugin/`, and the daemon and the admin API in `cmd/`. The dependency list backs up the plugin set: `github.com/yuin/gopher-lua` for the Lua plugin, `github.com/pquerna/otp` for one-time passwords, `github.com/tg123/go-htpasswd` for password files, `github.com/tg123/remotesigner` for remote signing, and `github.com/prometheus/client_golang` for the metrics plugin.

Building v1 produces a daemon and a plugin set from one tree

The quick start is a source build, and the README is explicit about which tag to use, since there is also a legacy `v0` branch kept in the repository. The build steps are:

bash
git clone https://github.com/tg123/sshpiper
cd sshpiper

mkdir out
go build -tags full -o out ./...
(cd cmd/sshpiperd && go build -o ../../out/ .)

The `full` build tag is what pulls in the whole plugin set rather than a reduced one, and the second command builds the daemon into the same `out` directory, which is why the demo commands later refer to `./out/sshpiperd` and `./out/fixed` side by side. The `go.mod` requires Go 1.27.0.

The container build tells you more about the shape of the project than the quick start does. The `Dockerfile` has a web builder stage on a Node 24 slim image that compiles the admin web console with `npm ci` and `npm run build`, a Go builder on `golang:1.27-bookworm` with `CGO_ENABLED=0`, and it builds `sshpiperd`, `sshpiperd-admin`, `sshpiperd-webadmin` and the whole `plugin/` tree with `go build -o /sshpiperd/plugins -tags "${BUILDTAGS}" ./plugin/... ./e2e/testplugin/...`. The build tags are passed in as a build argument, which is how the image decides which plugins to ship.

The release configuration lives in `.goreleaser.yaml`, and the end to end suite reads that same file to decide which plugins to build for the current operating system, failing if a released plugin has no native test scenario.

The demo: a dummy sshd, the fixed plugin, one ssh command

The README's demo is a three step sequence that you can follow without a second machine. The dummy upstream is a container running an OpenSSH server with a fixed user and password:

bash
docker run -d -e USER_NAME=user -e USER_PASSWORD=pass -e PASSWORD_ACCESS=true -p 127.0.0.1:5522:2222 lscr.io/linuxserver/openssh-server

The daemon is then started with the `fixed` plugin pointed at that address, with tracing enabled so you can watch the decisions:

bash
./out/sshpiperd -i /tmp/sshpiperkey --server-key-generate-mode notexist --log-level=trace ./out/fixed --target 127.0.0.1:5522

The client side is a plain ssh invocation to the proxy's listening port:

bash
ssh 127.0.0.1 -l user -p 2222

The username travels in the ssh login name, which is what makes the `username-router` plugin possible and what `workingdir` and `yaml` remap. The `-i` flag points at the proxy's own host key, and the `--server-key-generate-mode notexist` option says the proxy should not generate a key file if it is missing, so you have to create `/tmp/sshpiperkey` yourself.

The additional challenge example chains two plugins, with the challenge plugin in front of the routing plugin:

bash
./out/sshpiperd -i /tmp/sshpiperkey --server-key-generate-mode notexist --log-level=trace ./out/simplemath -- ./out/fixed --target 127.0.0.1:5522

One caveat on that last one. The plugin table in the README ends with the statement that `simplemath` is not shipped, so it is a concept you can read in the source but not a binary you can pull from a release.

Eight plugins ship, and two of them are Linux-only

The native test table in the README is the most precise description of the shipped plugin set available. Windows and macOS currently ship the same eight plugins: `fixed`, `workingdir`, `yaml`, `username-router`, `lua`, `failtoban`, `metrics` and `revtunnel`. The table then lists what each one is checked for.

Reading that column is a good way to understand the intended semantics. `fixed` is verified for password rejection, binary stdin and stdout, stderr, exit status and reconnects, which are the properties an ssh proxy can break if it is not careful. `username-router` is verified for target and port and upstream user being parsed from the downstream username, and for rejection of malformed usernames. `failtoban` is verified for chaining with `fixed`, for banning after failed authentication, and for loopback allowlisting, which is a safety valve so that a misconfiguration cannot lock you out of your own proxy.

Two things are explicitly not available everywhere. The Docker and Kubernetes plugins are Linux-only in the release configuration, and `simplemath` is not shipped at all. The Kubernetes support is visible in the dependency list, which includes `k8s.io/api`, `k8s.io/apimachinery`, `k8s.io/client-go` and `k8s.io/code-generator` at v0.37.1, alongside `github.com/moby/moby/client` for the Docker plugin.

The `examples/` directory holds runnable demonstrations for the harder cases: username routing, Lua public key git routing, a reverse tunnel, a honeypot on failure, and the web admin console. The honeypot example is worth a look when you are deciding how a failed login should behave.

The admin API can list, watch and kill live sessions

The v1 line adds an admin surface, and it is designed to be off unless you ask for it. The `sshpiperd` process can expose a small admin gRPC API that lets external tools list live SSH sessions, view their terminal output in real time, and kill them. It is enabled by passing a non-zero `--admin-grpc-port`, and by default the API requires TLS, so a server certificate and key have to be supplied.

Two companion binaries exist for this. `sshpiperd-admin` is the command line counterpart to `sshpiperd-webadmin`, and the README calls the whole group a web admin console, with `examples/webadmin/` holding a working deployment. The `libadmin/` directory in the repository root holds the admin library, and the web console itself is a Node application built in the first stage of the Dockerfile and embedded into the binary's static assets.

This is the feature that turns sshpiper from a routing utility into an auditable access layer. A session list with live terminal output and a kill button is closer to what a bastion host is supposed to offer, and the repository topics include auditing and recording for that reason. The caveat is that the visible README text describes the gRPC API in a paragraph that is cut off partway through the TLS flag list, so the exact option names beyond `--admin-grpc-port` and `--admin-grpc-tls-cert` are not readable in the documentation available here.

Native end to end runs on Windows and macOS, but not on macOS arm64

The end to end suite is unusual in how thoroughly it is described. On Windows, the README says to run a PowerShell script from the repository root, with Windows Go and Docker Compose installed in the default WSL2 distribution and its Docker daemon running:

powershell
.\e2e\windows\run.ps1

On macOS, with native Go and a running Docker daemon with Compose, the runner is a shell script:

bash
bash e2e/macos/run.sh

Both use the shared `e2e/native` suite, and the README explains the arrangement in detail. Only the upstream SSH server runs in Docker, from `e2e/docker-compose.yml`; the Go runner, the daemon and the plugins stay native to the host operating system. Each runner starts an isolated Compose project, runs native `go test`, and collects logs and removes the containers and volumes even on failure. The daemon and plugins are run from paths containing spaces on purpose, which is the kind of detail that only matters if someone has already been bitten by it.

There are two deliberate exemptions. The permission based file routing tests use `--no-check-perm` only on Windows, because Windows mode bits cannot express Unix owner-only permissions, while macOS keeps the checks enabled. And the macOS CI job uses Intel, on `macos-15-intel`, because hosted Apple Silicon runners do not support the nested virtualization that Colima needs, so native macOS arm64 is not exercised in CI at all.

To run the suite yourself against an upstream that is already up, the README gives the environment variable and the command:

bash
SSHPIPERD_E2E_UPSTREAM=host:port
go test -v -count=1 -tags e2e -timeout 10m ./e2e/native

The plugin and daemon versions are in step: v1.6.1 was released on 2026-08-06 and the last push was on 2026-10-14, on the master branch.

Where sshpiper is the wrong choice

The clearest case is a target platform outside Linux, Windows and macOS on x86-64 or arm64. The release configuration is explicit that the Docker and Kubernetes plugins are Linux-only, so a team that manages its ssh targets through Kubernetes on macOS laptops cannot use the plugin that connects to the cluster.

The second case is a requirement for a full bastion session. sshpiper proxies connections to upstream servers; the admin API lists and kills live sessions and shows terminal output. It is not described as a login-audit database with a query interface, and the repository topics mention recording, but the visible documentation does not describe a recording format, a retention policy or a replay tool. If your compliance requirement is a tamper-evident record of what was typed, that requirement has to be checked against the project rather than assumed.

The third case is a team that wants one binary with no plugin chain. The configuration surface is a plugin on the command line, and the routing rules that are not `fixed` live in a working directory layout, a YAML regular expression, a username format, a Lua script or a Prometheus metrics endpoint. That is flexible, and it also means each rule is code you have to review, test and keep alive.

The fourth is stability of the line itself. The repository holds a legacy `v0` branch and the current line is `v1`, and the README states plainly that this is the v1 version, with the older one to be found in the other tree. Migrating from v0 is a project of its own.

Editorial conclusion

Adopt sshpiper when you need one entry point for many ssh servers, want the upstream to see a different authentication method than the client used, or need an audit trail of live sessions rather than log files. Do not adopt it if you need the Docker and Kubernetes plugins on macOS or Windows, since the release config marks those as Linux-only, or if you need the simplemath challenge plugin, which is not shipped. Before you install, decide which plugin matches your routing rule, read the plugin table in the README to see what each one is tested for, and check whether the admin gRPC API is something you want enabled at all, since it is off by default and expects TLS.

Frequently asked questions

What does sshpiper do that a normal ssh jump host does not?

It proxies the ssh protocol to upstream servers and lets a plugin decide the route and the authentication mapping. The README notes that the downstream may use password authentication while the upstream server receives public key authentication mapped by sshpiper, and some plugins add an extra challenge such as two-factor authentication.

How do I build and run sshpiper v1?

Clone the repository, run `go build -tags full -o out ./...` and build the daemon with `go build -o ../../out/ .` from `cmd/sshpiperd`, then start `./out/sshpiperd` with a plugin such as `./out/fixed --target host:port`. The `full` build tag is what includes the whole plugin set.

Which plugins does sshpiper ship?

Windows and macOS ship the same eight: `fixed`, `workingdir`, `yaml`, `username-router`, `lua`, `failtoban`, `metrics` and `revtunnel`. The Docker and Kubernetes plugins are Linux-only in the release configuration, and `simplemath` is not shipped.

Can I see and kill live SSH sessions with sshpiper?

Yes. The daemon can expose an admin gRPC API that lists live sessions, shows their terminal output in real time and kills them. It is off by default, enabled with a non-zero `--admin-grpc-port`, and requires a TLS server certificate and key by default.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. tg123/sshpiper on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tg123-sshpiper.svg)](https://hysenlabs.com/projects/tg123-sshpiper)