sshpiper is a free, open source networking & connectivity project written in Go and released under MIT. It has 1,331 GitHub stars, 170 forks and 25 open issues, and was last pushed 6 hours ago. On this registry it ranks #55 of 69 tracked projects in Networking & Connectivity, with 5 head-to-head comparisons available.

What is sshpiper?

sshpiper is a Go reverse proxy for sshd that routes SSH, SCP and port-forwarding sessions from a downstream client to upstream SSH servers, built for operators who want a bastion layer they control sitting in front of existing hosts.

What it is

sshpiper sits between a downstream client, typically an SSH client, and an upstream server, typically an SSH server. A plugin handles the routing from downstream to upstream and is also responsible for mapping authentication methods to the upstream server: the downstream may authenticate with a password while the upstream receives public key authentication mapped by sshpiper. The same plugins can add an additional challenge during SSH authentication, for example asking the downstream for two-factor authentication. Every protocol running on top of SSH is supported, including SSH, SCP and port forwarding, so a client connecting with ssh -l bob is piped through to Bob's machine while ssh -l alice reaches Alice's machine. The project is written in Go and is in the SSH and OpenSSH ecosystem; the end-to-end suite runs against the real OpenSSH host-password service defined in e2e/docker-compose.yml.

The concrete problem is that a front door for an SSH estate and the hosts behind it otherwise have to agree on everything. Without such a proxy, each client connects directly to each upstream host, every upstream server has to be reachable and configured for whatever authentication the client offers, and adding a second factor generally means changing the servers themselves. sshpiper replaces those direct client-to-upstream connections with a single proxied hop: routing decisions move into plugins, username remapping happens centrally, and authentication is mapped or extended in front of sshd rather than inside it. This is version v1; a legacy v0 branch is maintained separately.

Key capabilities

  • Routes sessions with plugins that ship in the release config, including fixed, workingdir, yaml, username-router, lua, failtoban, metrics and revtunnel.
  • Remaps usernames by directory with the workingdir plugin, which also rejects missing users, or by regular expression with the yaml plugin, which rejects unmatched users.
  • Parses the target host, port and upstream user from the downstream username with the username-router plugin, and rejects malformed usernames.
  • Runs script-based routing, username remapping and rejection through the lua plugin.
  • Chains with fixed in the failtoban plugin to ban clients after failed authentication, with loopback allowlisting.
  • Exposes an HTTP connection gauge alongside authentication and pipe-creation error counters through the metrics plugin, also chained with fixed.
  • Supports public-key registration, password-authenticated reverse forwarding, a native file session store and revocation with the revtunnel plugin.

Who uses it and how

  • Operators fronting several upstream machines behind one address, where routing by downstream username decides which host a session lands on.
  • Teams that need two-factor authentication without touching upstream configuration, using the plugin additional-challenge mechanism; the topic list points to Google Authenticator and Azure AD as the second-factor sources.
  • Environments with auditing and session recording requirements, reflected in the auditing and recording topics.
  • Windows and macOS operators run the shared e2e/native suite through e2e/windows/run.ps1 and e2e/macos/run.sh; all eight shipped plugins are exercised on both platforms, and the daemon and plugins run from paths containing spaces.
  • Linux deployments can additionally use the Docker and Kubernetes plugins, which are Linux-only in the release config.

Getting started

Build from source by cloning https://github.com/tg123/sshpiper, then running go build -tags full -o out ./... and building cmd/sshpiperd, or pull the published Docker image farmer1992/sshpiperd.

How it compares

sshpiper positions itself as the proxy layer in front of OpenSSH sshd, so it complements rather than replaces the SSH server an operator already runs. This registry names no comparable reverse proxy for SSH, and no list of paid products it displaces was supplied, so it stands alone here.

When to use it — and when not to

A self-hoster must run the daemon and the upstream SSH servers it fronts, either as a Go build or as the farmer1992/sshpiperd container. It is the wrong pick if Docker or Kubernetes plugin support is needed on Windows or macOS, since those plugins are Linux-only, or if simplemath is required, as it is not shipped. File-based routing tests use --no-check-perm on Windows because its mode bits cannot express Unix owner-only permissions, so permission guarantees are weaker there than on Linux and macOS.

project readme (upstream, from github) — read inline

sshpiper 🖇

E2E Go Report Card Docker Image

sshpiper is the reverse proxy for sshd. all protocols, including ssh, scp, port forwarding, running on top of ssh are supported.

Note: this is v1 version, checkout legacy v0 here

Overview and Terminology

  • downstream: the client side, typically an ssh client.
  • upstream: the server side, typically an ssh server.
  • plugin: handles the routing from downstream to upstream. The plugin is also responsible for mapping authentication methods to the upstream server. For example, the downstream may use password authentication, but the upstream server may receive public key authentication mapped by sshpiper.
  • additional challenge: some plugins will not only perform routing but also add additional challenges to SSH authentication for the upstream server. For example, the downstream may be asked for two-factor authentication provided by the plugin.
+---------+                      +------------------+          +-----------------+
|         |                      |                  |          |                 |
|   Bob   +----ssh -l bob----+   |   sshpiper    +------------->   Bob' machine  |
|         |                  |   |               |  |          |                 |
+---------+                  |   |               |  |          +-----------------+
                             +---> pipe-by-name--+  |                             
+---------+                  |   |               |  |          +-----------------+
|         |                  |   |               |  |          |                 |
|  Alice  +----ssh -l alice--+   |               +------------->  Alice' machine |
|         |                      |                  |          |                 |
+---------+                      +------------------+          +-----------------+


 downstream                         sshpiper                        upstream                     

Quick start

Build

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

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

Native Windows and macOS E2E tests

On Windows, run from the repository root in PowerShell with Windows Go and Docker Compose installed in the default WSL2 distribution (with its Docker daemon running):

.\e2e\windows\run.ps1

On macOS, use native Go and a running Docker daemon with Compose (for example, Docker Desktop or Colima), then run from the repository root:

bash e2e/macos/run.sh

Both runners use the shared e2e/native suite. It reads .goreleaser.yaml to build every plugin shipped for the current OS and fails if any release plugin lacks a native E2E scenario. The daemon and plugins run from paths containing spaces. Windows and macOS currently ship the same eight plugins:

Plugin Native E2E behavior
fixed Password rejection, binary stdin/stdout, stderr, exit status, reconnects
workingdir Directory-based username remapping and rejection of missing users
yaml Regex username remapping and rejection of unmatched users
username-router Target/port and upstream user parsed from the downstream username; malformed username rejection
lua Script-based routing, username remapping, and rejection
failtoban Chaining with fixed, banning after failed authentication, and loopback allowlisting
metrics Chaining with fixed, HTTP connection gauge lifecycle, authentication and pipe-creation error counters
revtunnel Public-key registration, password-authenticated reverse forwarding, native file session store, and revocation

On Windows, the suite also checks plugin termination when the daemon is forcibly killed, using a helper that deliberately survives stdio closure. File-based routing tests use --no-check-perm only on Windows, whose mode bits cannot express Unix owner-only permissions; macOS keeps permission checks enabled. Docker and Kubernetes plugins are Linux-only in the release config; simplemath is not shipped. On macOS, a pipe-watching supervisor terminates each daemon's process group, including its plugins, even if the parent test process times out or is interrupted.

The upstream is the real OpenSSH host-password service from e2e/docker-compose.yml, with e2e/docker-compose.native.yml publishing an ephemeral loopback port. Each runner starts an isolated Compose project (through WSL2 on Windows), runs native go test, and collects logs and removes the containers and volumes even on failure. Only the upstream SSH server runs in Docker; the Go runner, daemon, and plugins stay native to the host OS.

The E2E workflow provisions WSL2 and Docker on windows-latest and Colima on macos-15-intel, alongside the existing Linux Docker Compose suite. The macOS job uses Intel because hosted Apple Silicon runners do not support the nested virtualization Colima needs; native macOS arm64 is not exercised in CI. To run Go tests directly against an already-started Compose host-password service, set SSHPIPERD_E2E_UPSTREAM to its published host:port, then run go test -v -count=1 -tags e2e -timeout 10m ./e2e/native.

Run simple demo

start dummy sshd server

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

start sshpiperd with fixed plugin targeting the dummy sshd server

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

test ssh connection (password: pass)

ssh 127.0.0.1 -l user -p 2222

➕ math before login?

Here illustrates the example of additional challenge before the fixed plugin.

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

More examples

For Docker Compose demos (including username routing and Lua publickey git routing), see examples/.

Admin UI (sshpiperd-webadmin)

sshpiperd can optionally expose a small admin gRPC API that lets external tools list live SSH sessions, view their terminal output in real time, and kill them. This API is off by default and is enabled by passing a non-zero --admin-grpc-port. By default the API requires TLS, so a server cert/key must be supplied:

./out/sshpiperd --admin-grpc-port 8222 \
  --admin-grpc-tls-cert server.crt \
  --admin-grpc-tls-key server.key \
  ... <plugin> ...

For mutual TLS (recommended for production), also pass --admin-grpc-tls-cacert ca.crt so clients must present a certificate signed by ca.crt. To opt out of TLS entirely on a trusted network, pass --admin-grpc-insecure.

A separate binary, sshpiperd-webadmin, aggregates one or more sshpiperd admin endpoints and serves a browser dashboard plus a JSON HTTP API:

./out/sshpiperd-webadmin \
  --sshpiperd 127.0.0.1:8222 \
  --sshpiperd piper-2.internal:8222 \
  --tls-cacert ca.crt --tls-cert client.crt --tls-key client.key \
  -l 127.0.0.1 -p 8080

Then open . Endpoints can also be supplied via the SSHPIPERD_WEBADMIN_ENDPOINTS env var (comma-separated). Pass --insecure on the webadmin side when sshpiperd is started with --admin-grpc-insecure. The same client library (libadmin/) is structured so a future CLI tool (sshpiperd-admin) can reuse the discovery + aggregator code.

Plugins

icons

  • 🔀: routing plugin
  • 🔒: additional challenge plugin
  • 📈: metrics plugin

Plugin list

  • workingdir 🔀: /home-like directory to managed upstreams routing by sshpiperd.
  • yaml 🔀: config routing with a single yaml file.
  • lua 🔀🔒: scriptable plugin for custom routing and auth challenges.
  • docker 🔀: pipe into docker containers.
  • kubernetes 🔀: manage pipes via Kubernetes CRD.
  • azdevicecode 🔒: ask user to enter azure device code before login
  • fixed 🔀: fixed targeting the dummy sshd server
  • username-router 🔀: route based on username, the username format is target+username, where target is the target host and username is the username to use for that target.
  • simplemath 🔒: ask for very simple math question before login, demo purpose
  • githubapp 🔀: login ssh with your github account
  • restful by @11notes 🔀🔒: The rest plugin for sshpiperd is a simple plugin that allows you to use a restful backend for authentication and challenge.
  • failtoban 🔒: ban ip after failed login attempts
  • openpubkey🔀🔒: integrate with openpubkey
  • metrics 📈: serve prometheus metrics on open connections and auth errors

Screen recording

asciicast

recording the screen in asciicast format

To use it, start sshpiperd with --screen-recording-format asciicast and --screen-recording-dir /path/to/recordingdir

Example:

```
ssh user_name@
... do some commands
exit

asciinema play /path/to/recordingdir//shell-channel-0.cast

```

typescript

recording the screen in typescript format (not the lang). The format is compatible with scriptreplay(1)

To use it, start sshpiperd with --screen-recording-format typescript and --screen-recording-dir /path/to/recordingdir

Example:

```
ssh user_name@127.0.0.1 -p 2222
... do some commands
exit


$ cd /path/to/recordingdir/
$ ls *.timing *.typescript
1472847798.timing 1472847798.typescript

$ scriptreplay -t 1472847798.timing 1472847798.typescript # will replay the ssh session
```

Public key authentication when using sshpiper (Private key remapping)

During SSH publickey auth, RFC 4252 Section 7, ssh client sign session_id and some other data using private key into a signature sig. This is for server to verify that the connection is from the client not the man in the middle.

However, sshpiper actually holds two ssh connection, and it is doing what the man in the middle does. the two ssh connections' session_id will never be the same, because they are hash of the shared secret. RFC 4253 Section 7.2.

To support publickey auth, sshpiper routing plugin must provide a new private key for the upstream to sign the session_id. This new private key is called mapping key.

How this work

+------------+        +------------------------+                       
|            |        |                        |                       
|   client   |        |   sshpiper             |                       
|   PK_X     +-------->      |                 |                       
|            |        |      v                 |                       
|            |        |   Check Permission     |                       
+------------+        |      |                 |                       
                      |      |                 |                       
                      |      |                 |     +----------------+
                      |      v                 |     |                |
                      |   sign again           |     |   server       |
                      |   using PK_Y  +-------------->   check PK_Y   |
                      |                        |     |                |
                      |                        |     |                |
                      +------------------------+     +----------------+

Ports to other platforms

Migrating from v0

What's the major change in v1

  • low level sshpiper api is fully redesigned to support more routing protocols.
  • plugins system totally redesigned to be more flexible and extensible.
    • plugins are now separated from main process and no longer a single big binary, this allow user to write their own plugins without touching sshpiperd code.
  • grpc is first class now, the plugins are built on top of it

For plugins already in v1, you need change params to new params. However, not all plugins are migrated to v1 yet, they are being migrated gradually. you can still use the old plugins in v0 branch

Contributing

see CONTRIBUTING.md

License

MIT

Frequently asked questions

Is sshpiper free to use?

sshpiper is open source under the MIT licence. There is no licence fee and no seat count — you can self-host it or, where the project offers one, pay a vendor for a managed version instead.

What does sshpiper do?

The missing reverse proxy for ssh scp

What is sshpiper written in?

sshpiper is primarily written in Go. Its source is publicly available at https://github.com/tg123/sshpiper, and it has 1,331 GitHub stars.