Deployment guide

SSHPiper 1.6.1

Overview

SSHPiper is an SSH reverse proxy. Its daemon, sshpiperd, accepts SSH connections on port 2222 and passes each one to an upstream SSH server, chosen from the login name the client uses. You publish one SSH endpoint and keep your real SSH servers on private addresses.

For example, ssh -p 2222 alice@gateway lands on Alice's server and ssh -p 2222 build@gateway lands on the build server, each with its own keys. The routing table is a YAML file (/etc/sshpiper/sshpiperd.yaml). SSHPiper is a headless service with no web console.

This guide covers launching and verifying SSHPiper 1.6.1 on Ubuntu 24.04 from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace. You pick it as the software version when you subscribe and launch. The listing's guide hub describes the other VPN, proxy and remote access servers.

What SecureAnyCloud adds

  • Current release, current toolchain. This image ships SSHPiper 1.6.1, compiled by SecureAnyCloud from the unmodified upstream source with Go 1.27.3. The upstream 1.6.1 binaries were built with Go 1.26.5 and with older gRPC and SSH library versions that have since received security fixes. This build raises exactly those modules, listed in /usr/local/share/doc/sshpiper/sac-module-raise.diff. The build is checksum-pinned end to end and reproducible, and the record is in /usr/local/share/doc/sshpiper/BUILD-PROVENANCE.txt.
  • No default key. The image ships with no key at all. Each instance's first boot generates its own sshpiperd host key, the upstream key that sshpiperd uses to log in to your upstream servers, a demo client key and the demo upstream's host key. No two instances share a key. Logins are public-key only.
  • A working demo pipe. First boot routes the user demo to a small demo upstream on the instance itself. The demo upstream is a second SSH server bound to 127.0.0.1:2200 only, and it runs no commands. You can see a real pipe working before you add your own servers. The upstream's host key is pinned, so sshpiperd refuses an upstream whose key does not match.
  • Hardened gateway. sshpiperd runs as an unprivileged sshpiper user that owns and can write no file. It runs with no Linux capabilities, a read-only system and a restricted set of system calls. By default it refuses SSH port forwarding (ssh -L, -D, -R).
  • No route to instance metadata. Both sshpiperd and the demo upstream are blocked from the EC2 instance metadata service at the system level. The demo upstream also allows no forwarding, so a client of the pipe cannot reach the metadata service through it.

Prerequisites

  • An AWS account with permission to launch EC2 instances
  • An EC2 key pair in the Region you launch in
  • An OpenSSH client on each machine that connects through the gateway (macOS, Linux and Windows 10/11 include one)
  • For production use: the SSH servers you want to publish, reachable from the instance (usually on private addresses in the same VPC)

Recommended instance type: m5.large. A gateway for a small team runs comfortably on t3.small or larger.

Connecting to your instance

Version Operating system SSH user

SSHPiper 1.6.1 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Port Protocol Purpose Who should reach it

22 TCP SSH administration of the instance (the system SSH server) Your administrators

2222 TCP sshpiperd: clients connect here and are passed to an upstream server The people and machines that use the gateway

The demo upstream listens on 127.0.0.1:2200 only and is not reachable from the network.

1

Launch the instance

Option A: AWS Marketplace console

  • Open the SecureAnyCloud Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace. Choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  • When the subscription is active, choose Launch your software.
  • Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose SSHPiper 1.6.1 on Ubuntu 24.04, then choose your Region.
  • Pick an instance type and your key pair. m5.large is recommended, and the software charge is $0.04 per vCPU per hour.
  • Set the security group. The listing's default group opens ports 22 and 2222. Restrict port 22 to your own IP address, and restrict port 2222 to the networks your users connect from.
  • Choose a subnet in the VPC where your upstream SSH servers live. Leave the metadata defaults, because the image already requires IMDSv2. Then choose Launch.

Option B: AWS CLI

Run this on your own workstation. It creates a security group, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page. Replace the VPC, subnet, key pair and CIDR ranges with your own.

SG=$(aws ec2 create-security-group --group-name sshpiper \
  --description "SSHPiper gateway" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 2222 --cidr <your-client-cidr>
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=sshpiper}]'

You do not need --metadata-options, because the AMI already requires IMDSv2. The upstream servers' security groups must allow SSH from this instance's security group.

2

Connect and read the instance details

Connect over SSH as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It generates the four keys, starts the demo upstream and sshpiperd, and checks that the demo pipe works.

The details are in /root/sshpiper-credentials.txt, which only root can read. The file holds fingerprints and the connect command, not key material. Print it with sudo:

sudo cat /root/sshpiper-credentials.txt
# SSHPiper 1.6.1 - written by sshpiper-firstboot on this instance. No key material is stored in this file.
# Demo client key (private half): /root/sshpiper-demo-key (0600 root). Copy it to your client machine, then shred it here.
# Try the demo pipe:
ssh -p 2222 -i <demo key> demo@203.0.113.10 -> lands on demobackend@127.0.0.1:2200
# Your own upstreams: /etc/sshpiper/sshpiperd.yaml (authorise /etc/sshpiper/upstream/id_ed25519.pub on each upstream host). Self-test:
sudo sshpiper-selftest
# Guide: https://secureanycloud.com/stacks/sshpiper-1-6-1/ SSHPIPER_HOST=203.0.113.10 SSHPIPER_PORT=2222 SSHPIPER_DEMO_USER=demo SSHPIPER_DEMO_KEY_FILE=/root/sshpiper-demo-key SSHPIPER_DEMO_KEY_FINGERPRINT=SHA256:Jq3vT8nW1cY6pL0aR5dK2mX9sH4fB7gE3uZ1oN8iQw4 SSHPIPER_HOST_KEY_FINGERPRINT=SHA256:or4eT9kU2pV6bN1xS7tM3cL8dG0hJ5aY2fW4iQ7zRq0 SSHPIPER_UPSTREAM_PUBLIC_KEY_FILE=/etc/sshpiper/upstream/id_ed25519.pub SSHPIPER_UPSTREAM_KEY_FINGERPRINT=SHA256:Pz6wQ1eR8tY3uI5oA9sD2fG7hJ4kL0zX6cV1bN3mWe2 SSHPIPER_DEMO_UPSTREAM=demobackend@127.0.0.1:2200 SSHPIPER_DEMO_UPSTREAM_HOST_KEY_FINGERPRINT=SHA256:Lm2nB5vC8xZ1aS4dF7gH0jK3qW6eR9tY2uI5oP8lKj1

Your values will differ, because every instance has its own keys.

3

Confirm SSHPiper is running

Check that the three services are active and see which ports are listening:

systemctl is-active sshpiper sshpiper-demo-backend sshpiper-firstboot
sudo ss -Htlnp | awk '{print $4, $6}'
active active active 127.0.0.1:2200 users:(("sshd",pid=9539,fd=3)) 127.0.0.53%lo:53 users:(("systemd-resolve",pid=404,fd=15)) 0.0.0.0:22 users:(("sshd",pid=891,fd=3),("systemd",pid=1,fd=175)) 127.0.0.54:53 users:(("systemd-resolve",pid=404,fd=17)) [::]:22 users:(("sshd",pid=891,fd=4),("systemd",pid=1,fd=176)) *:2222 users:(("sshpiperd",pid=9585,fd=3))

sshpiperd owns port 2222. The system SSH server keeps port 22 for administration. The demo upstream listens on loopback only, and nothing else listens on the network.

4

Run the self-test

Run the self-test on the instance. It connects through sshpiperd with the demo key and checks that:

  • the host key on port 2222 is this instance's key
  • the pipe lands on the demo upstream
  • an unknown key, users without a pipe and password logins are all refused
  • ssh -W through the pipe reaches neither the metadata service nor any other address
sudo sshpiper-selftest
[sshpiper-selftest] host key pinned; demo -> sshpiperd:2222 -> demobackend@127.0.0.1:2200 (the upstream sees 127.0.0.1) [sshpiper-selftest] an unauthorized key is refused [sshpiper-selftest] users without a pipe (root, demobackend, ubuntu, nosuchpipe) are refused [sshpiper-selftest] password authentication is refused (offered: publickey) [sshpiper-selftest] ssh -W through the pipe reaches neither the metadata service nor any other address (forwarding refused) SELFTEST_OK
5

Try the demo pipe

On the instance, pin the host key of sshpiperd and log in as demo with the demo key:

printf '[127.0.0.1]:2222 %s\n' "$(sudo cut -d' ' -f1,2 /etc/sshpiper/ssh_host_ed25519_key.pub)" > /tmp/sshpiper_known_hosts
sudo ssh -p 2222 -i /root/sshpiper-demo-key -o UserKnownHostsFile=/tmp/sshpiper_known_hosts demo@127.0.0.1
Pseudo-terminal will not be allocated because stdin is not a terminal. sac-sshpiper-demo: you are demobackend on the demo upstream sshd (127.0.0.1:2200 on ip-172-31-88-77), reached through sshpiperd sac-sshpiper-demo: this upstream sees the connection from 127.0.0.1 (sshpiperd), not from your client sac-sshpiper-demo: it runs no commands; route sshpiperd to your own hosts in /etc/sshpiper/sshpiperd.yaml

You logged in as demo and arrived as demobackend on the upstream. sshpiperd authenticated you with the demo key, then logged in to the upstream with its own upstream key. The demo upstream only prints these lines and runs no command you send.

To try it from your workstation, copy the demo key off the instance. Run this on your workstation:

umask 077 ssh -i ~/.ssh/my-key-pair.pem ubuntu@<SSHPIPER_HOST> 'sudo cat /root/sshpiper-demo-key' > ~/.ssh/sshpiper-demo-key ssh -p 2222 -i ~/.ssh/sshpiper-demo-key demo@<SSHPIPER_HOST>

The first time you connect, SSH shows the gateway's host key fingerprint. Accept it only if it equals SSHPIPER_HOST_KEY_FINGERPRINT in the credentials file. After you have your copy, remove the server's copy of the demo key:

sudo shred -u /root/sshpiper-demo-key
6

Route a user to your own SSH server

Each pipe maps one downstream login name to an upstream server. The example below routes the user alice to her real server at 10.0.1.20.

  • Authorize the gateway on the upstream server. sshpiperd logs in to upstream servers with this instance's upstream key. Print its public half:

bash sudo cat /etc/sshpiper/upstream/id_ed25519.pub

Add that line to ~/.ssh/authorized_keys of the account that sshpiperd should log in as on 10.0.1.20 (here alice).

  • Pin the upstream server's host key and install Alice's public key on the gateway. Copy her id_ed25519.pub to the instance first:

bash sudo install -d -o root -g sshpiper -m 0750 /etc/sshpiper/users/ ssh-keyscan -p 22 -t ed25519 10.0.1.20 | sudo tee /etc/sshpiper/users/10.0.1.20.known_hosts sudo install -o root -g sshpiper -m 0640 alice.pub /etc/sshpiper/users/alice.pub sudo chown root:sshpiper /etc/sshpiper/users/10.0.1.20.known_hosts && sudo chmod 0640 /etc/sshpiper/users/10.0.1.20.known_hosts

Compare the fingerprint that ssh-keyscan returned with the one the upstream server's administrator gives you. The administrator can print it with ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on that server.

  • Add the pipe to /etc/sshpiper/sshpiperd.yaml under pipes:. Open the file with sudo nano /etc/sshpiper/sshpiperd.yaml.

yaml - from: - username: "alice" authorized_keys: /etc/sshpiper/users/alice.pub to: host: 10.0.1.20:22 username: "alice" private_key: /etc/sshpiper/upstream/id_ed25519 known_hosts: /etc/sshpiper/users/10.0.1.20.known_hosts

  • Restart sshpiperd, then connect as Alice from her machine:

bash sudo systemctl restart sshpiper ssh -p 2222 -i ~/.ssh/id_ed25519 alice@<SSHPIPER_HOST>

Every file the configuration names must be owned by root and readable by the sshpiper group but not by others (root:sshpiper, mode 0640). Otherwise sshpiperd refuses to start (see "How the secure start works").

The client chooses only the login name. The upstream address, account and key always come from this file, and a login name without a pipe is refused. The full syntax of the YAML router (several users per pipe, regular-expression user names, user CA keys) is described in /usr/local/share/doc/sshpiper/yaml-plugin-README.md.

7

Remove the demo

When your own pipes work, remove the demo. First delete the demo pipe from /etc/sshpiper/sshpiperd.yaml. That is the - from: entry for username: "demo" and its to: block. Then restart sshpiperd, stop the demo upstream and shred its keys:

sudo systemctl restart sshpiper
sudo systemctl disable --now sshpiper-demo-backend
sudo shred -u /root/sshpiper-demo-key /etc/sshpiper/demo/authorized_keys /etc/sshpiper/upstream/demo-backend.known_hosts

Keep /etc/sshpiper/upstream/id_ed25519. It is the key sshpiperd uses for your own upstream servers. Disabling the demo upstream does not affect sshpiperd.

8

Review logs and client addresses

List the most recent pipes that sshpiperd created:

sudo journalctl -u sshpiper --no-pager -o cat | grep 'pipe created' | tail -3
time=2026-10-31T20:43:04.376Z level=INFO msg="ssh connection pipe created" downstream_addr=198.51.100.7:44852 downstream_user=demo upstream_addr=127.0.0.1:2200 upstream_user=demobackend

sshpiperd logs every pipe with the real client address (downstream_addr) and the upstream it chose.

The upstream server, however, sees the connection come from the gateway, not from the client. Its own logs, fail2ban, AllowUsers user@address rules and other per-address controls all see this instance's private address. Apply per-client address controls on the gateway (its security group, or the from: side of a pipe) rather than on the upstream servers. Keep the upstream servers' SSH reachable only from the gateway.

How the secure start works

sshpiper.service and sshpiper-demo-backend.service start only after first boot has written /var/lib/sac/data/sshpiper-ready. Before each start of sshpiperd, a preflight run as root checks that the host key exists. It also checks that the router configuration and every key, authorized_keys and known_hosts file it names is a regular file owned by root and readable by the sshpiper group but not by others. It also checks that nothing under /etc/sshpiper is writable by anyone but root.

This replaces upstream's owner-only permission check. sshpiperd runs with --no-check-perm so that it never owns its own configuration. If a check fails, sshpiperd does not start. Run the preflight against the current configuration with:

sudo /usr/local/sbin/sshpiper-preflight && echo preflight OK
preflight OK

The demo upstream has its own preflight. It refuses to start the demo upstream if its configuration allows forwarding or listens anywhere but 127.0.0.1:2200.

Port forwarding

sshpiperd starts with --disable-local-forwarding --disable-remote-forwarding, so ssh -L, ssh -D, ssh -W and ssh -R through the gateway are refused, whatever the upstream server allows.

If your users need forwarding to or from their upstream servers, override the start command without those two flags. Run sudo systemctl edit sshpiper and add a [Service] section. In it, put an empty ExecStart= line, followed by the ExecStart= line that systemctl cat sshpiper shows, minus the two flags. Then run sudo systemctl restart sshpiper.

Forwarding then follows each upstream server's own SSH configuration. sshpiperd itself stays blocked from the instance metadata service either way.

Security recommendations

  • Restrict the ports. Limit port 2222 to the networks your users connect from, and port 22 to your administrators. Keep the upstream servers' SSH reachable only from the gateway.
  • Use one pipe and key per user. Pin every upstream server's host key with known_hosts. Remove /root/sshpiper-demo-key after you copy it, and remove the demo (Step 7) before you run the gateway in production.
  • Protect the upstream key. Every upstream server that authorizes /etc/sshpiper/upstream/id_ed25519 accepts logins from the gateway. Authorize it only for the accounts each pipe needs.
  • Keep the instance patched. Ubuntu security updates install automatically every day. To update SSHPiper itself, launch a newer version from the listing.
  • Known advisories. The following advisories affect modules that are part of the source but not compiled into this build, so this image needs no fix. They are recorded for transparency: GO-2026-5932 (golang.org/x/crypto/openpgp, no fix exists upstream) and GO-2026-6860 / GO-2026-6866 (golang.org/x/net/html).

Support and licensing

SSHPiper is free software under the MIT License. The license text, the build record and the module changes are in /usr/local/share/doc/sshpiper/. The source of this release is at https://github.com/tg123/sshpiper/tree/v1.6.1.

Deploy on AWS

Launch SSHPiper 1.6.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS