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
downstream: the client side, typically an ssh client.upstream: the server side, typically an ssh server.plugin: handles the routing fromdownstreamtoupstream. Thepluginis 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 bysshpiper.additional challenge: somepluginswill not only perform routing but also add additional challenges to SSH authentication for theupstreamserver. For example, thedownstreammay be asked for two-factor authentication provided by theplugin.
+---------+ +------------------+ +-----------------+
| | | | | |
| Bob +----ssh -l bob----+ | sshpiper +-------------> Bob' machine |
| | | | | | | |
+---------+ | | | | +-----------------+
+---> pipe-by-name--+ |
+---------+ | | | | +-----------------+
| | | | | | | |
| Alice +----ssh -l alice--+ | +-------------> Alice' machine |
| | | | | |
+---------+ +------------------+ +-----------------+
downstream sshpiper upstream
git clone https://github.com/tg123/sshpiper
cd sshpiper
mkdir out
go build -tags full -o out ./...
(cd cmd/sshpiperd && go build -o ../../out/ .)
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.ps1On 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.shBoth 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.
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
./out/sshpiperd -i /tmp/sshpiperkey --server-key-generate-mode notexist --log-level=trace ./out/fixed --target 127.0.0.1:5522
ssh 127.0.0.1 -l user -p 2222
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
For Docker Compose demos (including username routing and Lua publickey git routing), see examples/.
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 http://127.0.0.1:8080. 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.
- π: 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, wheretargetis the target host andusernameis 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
recording the screen in asciicast format https://docs.asciinema.org/manual/asciicast/v2/
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/<conn_guid>/shell-channel-0.cast
```
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/<conn_guid>
$ ls *.timing *.typescript
1472847798.timing 1472847798.typescript
$ scriptreplay -t 1472847798.timing 1472847798.typescript # will replay the ssh session
```
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 |
| | | |
| | | |
+------------------------+ +----------------+
- 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
sshpiperdcode.
- plugins are now separated from main process and no longer a single big binary, this allow user to write their own plugins without touching
grpcis 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
see CONTRIBUTING.md
MIT