SSHPiper 1.6.1 on AWS User Guide
Overview
This guide covers launching and verifying SSHPiper 1.6.1 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg 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 other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.
SSHPiper is an SSH reverse proxy. Its daemon, sshpiperd, accepts SSH connections on port 2222 and passes each one through 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: ssh -p 2222 alice@gateway lands on Alice's server, ssh -p 2222 build@gateway 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.
What cloudimg adds
- Current release, current toolchain. This image ships SSHPiper 1.6.1, compiled by cloudimg from the unmodified upstream source with Go 1.27.3. The upstream 1.6.1 binaries were built with Go 1.26.5 and 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/cloudimg-module-raise.diff). The build is checksum-pinned end to end and reproducible; 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
sshpiperdhost key, the upstream keysshpiperduses to log in to your upstream servers, a demo client key and the demo upstream's host key, so no two instances share a key. Logins are public-key only. - A working demo pipe. First boot routes the user
demoto a small demo upstream on the instance itself (a second SSH server bound to127.0.0.1:2200only, which runs no commands), so you can see a real pipe working before you add your own servers. The upstream's host key is pinned:sshpiperdrefuses an upstream whose key does not match. - Hardened gateway.
sshpiperdruns as an unprivilegedsshpiperuser that owns and can write no file, with no Linux capabilities, a read-only system and a restricted set of system calls. It refuses SSH port forwarding (ssh -L,-D,-R) by default. - No route to instance metadata. Both
sshpiperdand the demo upstream are blocked from the EC2 instance metadata service at the system level, and the demo upstream allows no forwarding, so a client of the pipe cannot reach the metadata service through it.
Prerequisites
- An AWS account and 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.
Step 1: Launch the instance
Option A: AWS Marketplace console
- Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
- Once 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 (
m5.largerecommended; the software charge is $0.04 per vCPU per hour) and your key pair. - Security group: the listing's default group opens 22 and 2222. Restrict port 22 to your own IP address, and 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 (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, and 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: the AMI already makes IMDSv2 required. The upstream servers' security groups must allow SSH from this instance's security group.
Step 2: Connect and read the instance details
Connect 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, readable only by root. It holds fingerprints and the connect command, not key material:
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://www.cloudimg.co.uk/guides/sshpiper-1-6-1-on-ubuntu-24-04-aws/
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 differ: every instance has its own keys.
Step 3: Confirm SSHPiper is running
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.
Step 4: Run the self-test
The self-test connects through sshpiperd on the instance itself with the demo key, checks that the host key on port 2222 is this instance's key, that the pipe lands on the demo upstream, and that an unknown key, users without a pipe and password logins are refused, and that 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
Step 5: Try the demo pipe
On the instance, pin sshpiperd's host key 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.
cloudimg-sshpiper-demo: you are demobackend on the demo upstream sshd (127.0.0.1:2200 on ip-172-31-88-77), reached through sshpiperd
cloudimg-sshpiper-demo: this upstream sees the connection from 127.0.0.1 (sshpiperd), not from your client
cloudimg-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; it 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. Once you have a copy, remove the server's copy of the demo key:
sudo shred -u /root/sshpiper-demo-key
Step 6: Route a user to your own SSH server
Each pipe maps one downstream login name to an upstream server. For a user alice whose real server is 10.0.1.20:
- Authorise the gateway on the upstream server.
sshpiperdlogs in to upstream servers with this instance's upstream key. Print its public half:
bash
sudo cat /etc/sshpiper/upstream/id_ed25519.pub
and add that line to ~/.ssh/authorized_keys of the account 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.pubto 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
Check the fingerprint ssh-keyscan returned against the one the upstream server's administrator gives you (ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on that server).
- Add the pipe to
/etc/sshpiper/sshpiperd.yaml(sudo nano /etc/sshpiper/sshpiperd.yaml), underpipes::
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
sshpiperdand 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, readable by the sshpiper group and not by others (root:sshpiper, mode 0640): sshpiperd refuses to start otherwise (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 yaml router's full syntax (several users per pipe, regular-expression user names, user CA keys) is described in /usr/local/share/doc/sshpiper/yaml-plugin-README.md.
Step 7: Remove the demo
Once your own pipes work, remove the demo. First delete the demo pipe (the - from: entry for username: "demo" and its to: block) from /etc/sshpiper/sshpiperd.yaml and restart sshpiperd, then 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.
Step 8: Logs and client addresses
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 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, and keep the upstream servers' SSH reachable only from the gateway.
How the secure start works
sshpiper.service and sshpiper-demo-backend.service only start once first boot has written /var/lib/cloudimg/sshpiper-ready. Before each start of sshpiperd, a preflight run as root checks that the host key exists and that the router configuration and every key, authorized_keys and known_hosts file it names is a regular file owned by root, readable by the sshpiper group and not by others, and that nothing under /etc/sshpiper is writable by anyone but root. (This replaces upstream's owner-only permission check, which sshpiperd runs with --no-check-perm so that sshpiperd never owns its own configuration.) If a check fails, sshpiperd does not start. Run it against the current configuration with:
sudo /usr/local/sbin/sshpiper-preflight && echo preflight OK
preflight OK
The demo upstream's preflight additionally refuses to start it if its configuration ever 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 with an empty ExecStart= line followed by the ExecStart= line that systemctl cat sshpiper shows, minus the two flags, then 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 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.
- Give each user their own pipe and key, and pin every upstream server's host key with
known_hosts. Remove/root/sshpiper-demo-keyonce copied, and remove the demo (Step 7) before production use. - Protect
/etc/sshpiper/upstream/id_ed25519: every upstream server that authorises it accepts logins from the gateway. Authorise it only for the accounts each pipe needs. - Keep the instance patched. Ubuntu security updates install automatically every day; SSHPiper itself is updated by launching a newer version from the listing.
- Known advisories in modules that are part of the source but not compiled into this build (no fix needed for this image, 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 licence 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.