spiped Secure Pipe Daemon on Ubuntu 24.04 on Azure User Guide
Overview
This guide covers the deployment and configuration of spiped on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. spiped, pronounced "ess pipe dee", is a secure pipe daemon: it creates a symmetrically encrypted and authenticated pipe between two socket addresses, so you can connect to one address and transparently reach another across an untrusted network. It is similar in spirit to an SSH port forward, but it is a purpose built daemon rather than a side effect of a shell session, and it uses a pre shared symmetric key instead of SSH credentials.
The image builds spiped 1.6.4 from the official signed upstream release tarball (www.tarsnap.com/spiped/spiped-1.6.4.tgz), with the tarball verified against the upstream published SHA-256 during the build, so the exact version is reproducible and there is no third party package repository on the running VM. Both daemons run under systemd as the unprivileged spiped user. Unattended security upgrades keep the base OS patched.
A pipe has two ends, and this image ships both. That is what makes it provable the moment it boots, with no second machine to set up first:
-
spiped.serviceis the server side. It accepts encrypted connections on TCP 8022 and forwards the decrypted stream to the service you are protecting. -
spiped-client.serviceis the client side. It accepts plaintext connections on 127.0.0.1:9022 and forwards them encrypted to the server side.
As shipped the client side loops back to the server side on the same VM, so connecting to 127.0.0.1:9022 traverses the complete encrypt then decrypt path and reaches the protected service. In a real deployment you keep the server side here and run the client side on your own machine; the shipped client is both a working end to end proof and the template you copy for that remote endpoint.
Secure by design, with nothing baked in. spiped uses a symmetric key, so whoever holds the key can both open and impersonate the pipe. A key shipped inside the image would therefore be a shared secret on every customer VM and would defeat the product entirely. This image never ships key material. On the very first boot of every VM a fresh 256 bit key is generated from the kernel random source, unique to that instance, and recorded in /root/spiped-credentials.txt (mode 0600, root only). Both daemons are held behind a bootstrap gate until that has happened, so neither can ever start with missing or shared key material.
The key is a credential gate, not just encryption. spiped authenticates during its handshake, so a peer that cannot prove it holds this VM's key is dropped before any byte reaches the protected service. A plaintext client, or a client with the wrong key, never gets through at all.
What is included:
-
spiped 1.6.4 built from the pinned, checksum verified upstream source, with both
spipedand thespipeone shot client, run under systemd as the unprivilegedspipeduser -
An encrypted pipe endpoint on TCP 8022 and a local plaintext endpoint on 127.0.0.1:9022, wired together and to this VM's SSH service as a working demonstration you repoint at your own service
-
A per instance 256 bit key generated on first boot, documented with a fingerprint and a transportable copy in
/root/spiped-credentials.txt(0600) -
A hardened baseline: the plaintext endpoint bound to loopback only, an unprivileged runtime user, and
CAP_NET_BIND_SERVICEgranted so you can move the encrypted endpoint to a privileged port such as 443 without editing the unit -
Everything driven from one short, commented file at
/etc/spiped/spiped.conf, so you retarget the pipe without touching systemd units
Key facts
| Item | Value |
|---|---|
| Platform | Ubuntu 24.04 LTS on Azure |
| Default administrative user | azureuser |
| Encrypted pipe endpoint | TCP 8022 (all interfaces) |
| Local plaintext endpoint | TCP 9022 (loopback only) |
| Key file | /etc/spiped/spiped.key (0640 root:spiped) |
| Credentials file | /root/spiped-credentials.txt (0600 root:root) |
| Configuration | /etc/spiped/spiped.conf |
Prerequisites
-
Active Azure subscription, SSH public key, VNet and subnet in target region
-
Subscription to the spiped listing on Azure Marketplace
-
Network Security Group rules allowing TCP 22 for administration, and TCP 8022 from the clients that will use the pipe
-
A way to copy a small secret to the machine that will run the client side, over a channel you already trust
Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM). Encryption throughput scales with CPU, so busy pipes should use Standard_D2s_v5 or larger.
Step 1: Deploy from the Azure Portal
Search spiped in Marketplace, select the cloudimg publisher, and click Create. Configure the Network Security Group to allow TCP 8022 from your client networks, and TCP 22 for administration. Leave the plaintext port 9022 closed; it is bound to loopback and is not meant to be reachable from the network.
Step 2: Deploy from the Azure CLI
RG="pipe-prod"; LOCATION="eastus"; VM_NAME="pipe1"
IMAGE_URN="cloudimg1647283583153:spiped-ubuntu-24-04:default:latest"
SSH_KEY="$(cat ~/.ssh/id_rsa.pub)"
az group create --name "$RG" --location "$LOCATION"
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn cloudimg1647283583153:spiped-ubuntu-24-04:default:latest
az vm create \
--resource-group "$RG" --name "$VM_NAME" \
--image "$IMAGE_URN" \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values "$SSH_KEY" \
--public-ip-sku Standard
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 8022 --priority 1001
az vm open-port --resource-group "$RG" --name "$VM_NAME" --port 22 --priority 1002
Step 3: First boot
On first boot the image generates this VM's 256 bit key, writes it to /etc/spiped/spiped.key, records its fingerprint and a transportable copy in /root/spiped-credentials.txt, and only then releases the two spiped units to start. This completes within a few seconds. SSH in as azureuser and read the details:
sudo cat /root/spiped-credentials.txt
The file names the key file, its SHA-256 fingerprint, a base64 copy you can carry to the client machine, and the two endpoints. Keep it root only; it is the one place the key exists in a form you can copy.
Step 4: Confirm both ends of the pipe are running
Both units run under systemd as the unprivileged spiped user. ss confirms the encrypted endpoint is listening on all interfaces and the plaintext endpoint is bound to loopback only.
systemctl is-active spiped.service
systemctl is-active spiped-client.service
ps -o user= -C spiped | sort -u | tr -d ' '
ss -tlnp | grep -E ':8022 |:9022 '
Expected output:
active
active
spiped
LISTEN 0 10 127.0.0.1:9022 0.0.0.0:* users:(("spiped",pid=3406,fd=3))
LISTEN 0 10 0.0.0.0:8022 0.0.0.0:* users:(("spiped",pid=3405,fd=3))

You can also confirm the build:
timeout 10 spiped -v </dev/null 2>&1
spiped 1.6.4
Note the </dev/null. Both spiped and spipe read standard input as part of normal operation, so redirect it whenever you are only probing the binary.
Step 5: Prove the encrypted round trip
This is the demonstration that matters, and it runs entirely on this one VM. A connection to the local plaintext port is encrypted by the client side, carried to the server side, decrypted, and handed to the protected service. The service in the shipped configuration is this VM's own SSH daemon, because it is always present, so its greeting arriving back through the pipe is proof the whole path works.
echo -n 'through the pipe : '; timeout 8 bash -c 'exec 3<>/dev/tcp/127.0.0.1/9022; head -1 <&3' 2>/dev/null || echo 'no response'
Expected output:
through the pipe : SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.18
That greeting was produced by the service at the far end of the pipe. A real byte stream survived encryption and decryption, so the pipe is genuinely carrying traffic rather than merely holding a port open.

Step 6: The pipe requires this VM's key
The encrypted endpoint on 8022 authenticates its peer during the spiped handshake. A client that cannot prove it holds this VM's key is dropped before it reaches the protected service. Prove all three cases. These commands run on the VM against loopback; from a remote client, replace 127.0.0.1 with the VM address.
WRONGKEY=$(mktemp); dd if=/dev/urandom bs=32 count=1 of="$WRONGKEY" status=none
echo -n 'plaintext client : '; timeout 8 bash -c 'exec 3<>/dev/tcp/127.0.0.1/8022; head -c 64 <&3' 2>/dev/null | grep -aq 'SSH-2.0' && echo 'reached the protected service' || echo 'never reached the protected service'
echo -n 'wrong key : '; { sleep 3; } | timeout 10 spipe -t '[127.0.0.1]:8022' -k "$WRONGKEY" 2>/dev/null | head -1 | grep . || echo 'never reached the protected service'
echo -n 'this VM key : '; { sleep 3; } | timeout 10 sudo spipe -t '[127.0.0.1]:8022' -k /etc/spiped/spiped.key 2>/dev/null | head -1 | grep . || echo 'never reached the protected service'
rm -f "$WRONGKEY"
Expected output:
plaintext client : never reached the protected service
wrong key : never reached the protected service
this VM key : SSH-2.0-OpenSSH_9.6p1 Ubuntu-3ubuntu13.18
The first case is worth understanding. A plaintext client does receive a few bytes, because the spiped server opens its handshake by sending a random value. It never gets any further: without the key it cannot compute the authentication code the server expects, so the connection is dropped and the protected service is never touched.

spipe is the one shot client shipped alongside the daemon. It speaks the same protocol but pipes standard input and output instead of listening on a port, which makes it convenient for exactly this kind of check.
Step 7: Wire up the second endpoint on your own machine
In production the server side stays on this VM and the client side runs wherever your traffic originates. Install spiped on that machine, recreate the key file from the base64 copy in the credentials file, and run the client side pointing at this VM.
First, on the VM, print the two values you need:
sudo grep -E '^KEY_SHA256=|^KEY_B64=' /root/spiped-credentials.txt
Then, on your own machine, recreate the key and confirm it matches. Replace <vm-ip> with this VM's address, and paste the base64 value from the previous command.
echo '<paste the KEY_B64 value here>' | base64 -d > spiped.key
chmod 0600 spiped.key
sha256sum spiped.key
spiped -e -s '[127.0.0.1]:8022' -t '[<vm-ip>]:8022' -k spiped.key &
ssh -p 8022 azureuser@127.0.0.1
The fingerprint printed by sha256sum must match the KEY_SHA256 value from the VM. The last two commands start the client side and then connect to 127.0.0.1:8022 on your own machine, which reaches the protected service on the VM with everything between the two hosts encrypted and authenticated.
Transfer the key over a channel you already trust, such as an existing SSH session or a secrets manager. Anyone who obtains it can both use and impersonate the pipe.
Step 8: Review the configuration
spiped takes all of its settings as command line arguments. This image supplies them from one short file so you never have to edit a systemd unit.
grep -vE '^\s*#|^\s*$' /etc/spiped/spiped.conf
Expected output:
SPIPED_SERVER_SOURCE=[0.0.0.0]:8022
SPIPED_SERVER_TARGET=[127.0.0.1]:22
SPIPED_CLIENT_SOURCE=[127.0.0.1]:9022
SPIPED_CLIENT_TARGET=[127.0.0.1]:8022
SPIPED_KEY=/etc/spiped/spiped.key
SPIPED_MAX_CONNS=100
SPIPED_TIMEOUT=5
SPIPED_SERVER_TARGET is the service you are protecting. SPIPED_CLIENT_TARGET is where the client side sends encrypted traffic. Addresses use spiped's own format: [ip.v4.ad.dr]:port, host.name:port, or an absolute path to a UNIX socket.
Step 9: Point the pipe at your own service
To protect something other than SSH, change SPIPED_SERVER_TARGET to its address and restart the server side. The backend can be on this VM or anywhere else your VM can reach. For example, to protect a PostgreSQL server elsewhere in your virtual network, replacing <private-ip> with that server's address:
sudo sed -i 's|^SPIPED_SERVER_TARGET=.*|SPIPED_SERVER_TARGET=[<private-ip>]:5432|' /etc/spiped/spiped.conf
sudo systemctl restart spiped
Clients then run the client side locally and connect to their own loopback port as if PostgreSQL were running there. Nothing on either side needs to know it is talking through an encrypted pipe, which is the point of the design: the database keeps speaking its own plaintext protocol, and the untrusted network in between only ever carries authenticated ciphertext.
To run several pipes on one VM, copy /etc/systemd/system/spiped.service to a second unit with its own source port and target, and give it its own EnvironmentFile.
Step 10: Confirm no key material ships in the image
No default, shared, or baked in key exists. The credentials file is 0600 root:root, the key file is 0640 root:spiped so only root and the service account can read it, and the directory itself is 0750.
sudo stat -c 'file: %n perms: %a owner: %U:%G' /root/spiped-credentials.txt /etc/spiped/spiped.key /etc/spiped
sudo stat -c '%s bytes' /etc/spiped/spiped.key
sudo sed -E 's/^(KEY_SHA256=.{12}).*/\1********************/; s/^(KEY_B64=.{10}).*/\1**************************/' /root/spiped-credentials.txt | grep -E '^KEY_FILE=|^KEY_SHA256=|^KEY_B64='
Expected output:
file: /root/spiped-credentials.txt perms: 600 owner: root:root
file: /etc/spiped/spiped.key perms: 640 owner: root:spiped
file: /etc/spiped perms: 750 owner: root:spiped
32 bytes
KEY_FILE=/etc/spiped/spiped.key
KEY_SHA256=a79dbcb3c8c5********************
KEY_B64=HQZAO+M2uo**************************
The key is exactly 32 bytes, which is the 256 bits of entropy upstream requires, and it was read from the kernel random source on this instance's first boot. The fingerprint above will differ on your VM, because your key is yours alone.

Server Components
| Component | Version | Purpose |
|---|---|---|
| spiped | 1.6.4 | Secure pipe daemon, both server and client sides |
| spipe | 1.6.4 | One shot spiped protocol client for scripts and checks |
| OpenSSL | 3.0.13 | Cryptographic primitives spiped links against |
| systemd | 255 | Service supervision for both pipe ends and first boot |
| Ubuntu | 24.04 LTS | Base operating system |
Filesystem Layout
| Path | Size | Purpose |
|---|---|---|
/ |
29 GB | Root filesystem |
/boot |
881 MB | Operating system kernel files |
/boot/efi |
105 MB | UEFI boot partition (Gen2 Hyper V) |
/mnt |
varies | Azure temporary resource disk |
Key directories
| Path | Purpose |
|---|---|
/usr/local/bin/spiped |
The spiped daemon binary |
/usr/local/bin/spipe |
The spipe one shot client binary |
/etc/spiped/ |
Configuration and key material (0750 root:spiped) |
/etc/spiped/spiped.conf |
Endpoint and tuning settings for both units |
/etc/spiped/spiped.key |
This instance 256 bit key (0640 root:spiped) |
/root/spiped-credentials.txt |
Key fingerprint and transportable copy (0600 root:root) |
/usr/local/share/doc/spiped/ |
Upstream COPYRIGHT and README shipped with the binaries |
/usr/local/share/man/man1/ |
spiped.1 and spipe.1 manual pages |
Managing the service
systemctl status spiped.service --no-pager
systemctl status spiped-client.service --no-pager
Restart the server side after changing SPIPED_SERVER_SOURCE, SPIPED_SERVER_TARGET, or the key. Restart the client side after changing SPIPED_CLIENT_SOURCE or SPIPED_CLIENT_TARGET. spiped handles SIGTERM gracefully: it stops accepting new connections and exits once existing ones have finished, so a restart does not cut live sessions short.
If you do not need the loopback demonstration once your own client endpoint is in place, run sudo systemctl disable --now spiped-client.service to stop the client side and keep it stopped across reboots. The server side, which is the half that actually protects your service, keeps running untouched. Re enable it later with sudo systemctl enable --now spiped-client.service.
Scripts and Log Files
| Path | Purpose |
|---|---|
/usr/local/sbin/spiped-firstboot.sh |
Generates this instance key on first boot |
/var/log/spiped-firstboot.log |
First boot key generation log |
journalctl -u spiped.service |
Server side daemon log |
journalctl -u spiped-client.service |
Client side daemon log |
journalctl -u spiped-firstboot.service |
First boot service log |
On Startup
spiped-firstboot.service runs once, on the first boot of a new VM. It generates the per instance key, writes the credentials file, and then creates the marker /var/lib/cloudimg/spiped-bootstrap-ready. Both spiped units carry ConditionPathExists on that marker, so they cannot start until the key exists. After that first boot the service marks itself complete with a sentinel at /var/lib/cloudimg/spiped-firstboot.done and does not run again, and the two daemons start normally on every subsequent boot.
systemctl is-active spiped-firstboot.service
sudo journalctl -u spiped-firstboot.service --no-pager -n 15
Troubleshooting
The pipe returns nothing at all. Check both units are active and that the backend named in SPIPED_SERVER_TARGET is actually listening. A pipe to a dead backend completes its handshake and then has nothing to forward.
systemctl is-active spiped.service spiped-client.service
sudo ss -tlnp | grep -E ':8022 |:9022 |:22 '
sudo journalctl -u spiped.service --no-pager -n 20
A remote client cannot connect. Confirm TCP 8022 is open in the Network Security Group for that client's network, and that the client is using this VM's address rather than loopback. Port 9022 is bound to loopback deliberately and will never answer from off the VM.
A remote client connects but is dropped. The two ends are not holding the same key. Compare fingerprints: sha256sum of the client's key file must equal KEY_SHA256 in /root/spiped-credentials.txt on the VM. A key copied through an editor or a chat window is often corrupted by trailing whitespace or newline translation, which is why the credentials file carries a base64 copy and a fingerprint to check it against.
A unit refuses to start after a first boot problem. Both units are gated on the bootstrap marker. If key generation failed, the marker is absent and the daemons stay stopped by design rather than starting without a key.
ls -l /var/lib/cloudimg/spiped-bootstrap-ready /var/lib/cloudimg/spiped-firstboot.done
sudo cat /var/log/spiped-firstboot.log
Connections are slower or larger than expected. spiped sends data in fixed 1024 byte packets and pads anything smaller, so interactive traffic uses more bandwidth than the raw byte count suggests. This is a deliberate design choice that limits what an observer can infer from packet sizes. It does not hide timing, so spiped protects the content of an interactive session but not its typing rhythm.
Security Recommendations
-
Restrict the Network Security Group. Allow TCP 8022 only from the client networks that need the pipe, and TCP 22 for administration only.
-
Treat the key as the credential it is. It is symmetric, so anyone holding it can both open the pipe and impersonate the server. Transfer it only over a channel you already trust and never commit it to source control.
-
Rotate the key by replacing the file on both ends at the same time. Write 32 fresh bytes from the kernel random source, distribute the new file, and restart the daemons at both ends. There is no in band rotation; both ends must change together.
-
Keep the plaintext endpoint on loopback. Exposing port 9022 to the network would defeat the pipe by publishing the very traffic it exists to protect.
-
Do not reuse one key across unrelated pipes. Upstream also asks that a single key file is not used for more than 2^64 connections, which is far beyond normal use but worth knowing.
-
Prefer perfect forward secrecy. The shipped configuration uses spiped's default full handshake, which provides it. Do not add the fast handshake option unless you have measured a genuine CPU problem and accept the loss.
-
Keep the OS patched. Unattended security upgrades remain enabled on the running VM.
Support and Licensing
spiped is developed by Colin Percival and Tarsnap Backup Inc. and distributed under the BSD 2 Clause License. The one external file it carries, libcperciva/external/queue/queue.h, is the BSD licensed queue implementation from the University of California Regents. This cloudimg image builds the unmodified upstream spiped 1.6.4 source, and the upstream COPYRIGHT file ships in the image at /usr/local/share/doc/spiped/COPYRIGHT. cloudimg provides the packaging, the secure default configuration, the per instance key automation, security patching, and 24/7 support with a guaranteed 24 hour response SLA.
cloudimg is not affiliated with or endorsed by the spiped project or Tarsnap Backup Inc. spiped and Tarsnap are marks of their respective owners and are used here only to identify the software.
Deploy on Azure
Find spiped Secure Pipe Daemon on Ubuntu 24.04 LTS on the Azure Marketplace, published by cloudimg. Deploy from the Portal or the Azure CLI as shown above.
Need Help?
Email support@cloudimg.co.uk for deployment help, configuration questions, or licensing enquiries.