Skip to content
Go back

stunnel vs spiped

By SumGuy 9 min read
stunnel vs spiped

Your old monitoring stack speaks plaintext and you know it

Redis on port 6379. MySQL replication over TCP. That Graphite agent you set up three years ago and haven’t touched since. Every single one of them shipping bytes in the clear, hoping nobody on the wire is paying attention.

You’re not going to rewrite them. You’re not going to rip out the stack. What you’re going to do is wrap a thin layer of encryption around the pipe and call it done.

Two tools have handled this job quietly and reliably for years: stunnel and spiped. Neither requires you to restructure your application. Neither needs you to spin up a VPN. They sit between your services and the network, and they encrypt the traffic. That’s the whole job.

They do it very differently, though — and picking the wrong one means you’re either managing certificates on a service that should be a five-minute job, or you’re shipping a tool with no certificate infrastructure into a situation that really needs one.


The crypto underneath

This part matters more than most people think.

stunnel is a TLS wrapper. Full stop. You get TLS 1.2 and 1.3, X.509 certificates, SNI support, OCSP stapling, client certificate authentication, PSK modes if you want them, the whole enchilada. The cryptography is OpenSSL’s problem and OpenSSL has been audited by everyone.

The tradeoff is that TLS brings its knobs with it. Cipher suites, certificate chains, trust stores, hostname verification — there’s a lot of surface area if you misconfigure something. Done right, it’s rock solid. Done carelessly, you’ve got a self-signed cert with verify = 0 and you’ve defeated the point.

spiped (written by Colin Percival of Tarsnap fame) takes a completely different angle. No X.509. No PKI. No certificate lifecycle. You give both ends the same 32-byte symmetric key file, and that’s it. Under the hood it uses HMAC-SHA256 with an authenticated Diffie-Hellman handshake for key negotiation, AES-256-CTR for encryption, and HMAC-SHA256 for authentication. The implementation is small enough to read in an afternoon, and the attack surface is proportionally small.

The tradeoff is that your security model is “this key is secret.” Full stop. There’s no certificate to inspect, no CA to verify, no SNI. If you’re connecting two trusted machines with no third parties in the picture, that’s a feature. If you’re fronting a service that external clients need to connect to with standard TLS tooling, that’s a showstopper.


Setup: stunnel

stunnel has been around since 1998 and it shows in the config file format — it’s readable, if a little old-fashioned.

Here’s the canonical “wrap Redis with TLS using a Let’s Encrypt cert” setup. On the server side:

/etc/stunnel/redis-server.conf
[redis-server]
accept = 6380
connect = 127.0.0.1:6379
cert = /etc/letsencrypt/live/redis.yourdomain.com/fullchain.pem
key = /etc/letsencrypt/live/redis.yourdomain.com/privkey.pem
; require clients to present a cert (optional but recommended)
; verify = 2
; CAfile = /etc/stunnel/client-ca.pem

On the client side (another host running stunnel to unwrap the TLS):

/etc/stunnel/redis-client.conf
[redis-client]
client = yes
accept = 127.0.0.1:6379
connect = redis.yourdomain.com:6380
CAfile = /etc/ssl/certs/ca-certificates.crt
checkHost = redis.yourdomain.com

Your application on the client box points at 127.0.0.1:6379 and has no idea TLS is involved. The stunnel on the server unwraps the connection and hands it to the real Redis. Clean.

Firing it up:

Terminal window
# server
stunnel /etc/stunnel/redis-server.conf
# or via systemd (recommended)
systemctl enable --now stunnel@redis-server

Let’s Encrypt cert renewal is handled by certbot. stunnel needs a HUP after renewal to reload the cert — drop this in your certbot renewal hook:

/etc/letsencrypt/renewal-hooks/post/stunnel-reload.sh
#!/bin/bash
systemctl reload stunnel@redis-server

That’s the operational overhead of the TLS approach in a nutshell: the cert renews every 90 days and you have a hook to reload it. Not bad. Not zero.


Setup: spiped

spiped’s author has a gift for tools that fit in your head completely. The entire configuration lives on the command line.

Generate a key (do this once, copy it to both machines):

Terminal window
dd if=/dev/urandom bs=32 count=1 > /etc/spiped/replication.key
chmod 600 /etc/spiped/replication.key

Server side (decrypt incoming connections and forward to MySQL on 3306):

Terminal window
spiped -d \
-s '[0.0.0.0]:3307' \
-t '127.0.0.1:3306' \
-k /etc/spiped/replication.key

Client side (encrypt outgoing connections heading to the server):

Terminal window
spiped -e \
-s '127.0.0.1:3306' \
-t 'db-primary.internal:3307' \
-k /etc/spiped/replication.key

Your replica’s MySQL config points at 127.0.0.1:3306 and the traffic arrives at the primary encrypted. The key file on both ends is the only secret you’re managing.

For production, wrap it in a systemd unit so it survives reboots:

/etc/systemd/system/spiped-replication.service
[Unit]
Description=spiped MySQL replication tunnel
After=network.target
[Service]
ExecStart=/usr/bin/spiped -F -d \
-s [0.0.0.0]:3307 \
-t 127.0.0.1:3306 \
-k /etc/spiped/replication.key
Restart=on-failure
User=spiped
[Install]
WantedBy=multi-user.target

The -F flag keeps it in the foreground so systemd can manage the lifecycle. That’s genuinely it. There’s no cert, no CA, no renewal hook. The key file either exists or it doesn’t.


MITM resistance, compared honestly

Both tools, configured correctly, are resistant to man-in-the-middle attacks. But “configured correctly” does different amounts of work in each case.

With stunnel, MITM resistance comes from certificate validation. If you set checkHost on the client and point at a real CA, an attacker can’t intercept without having the private key for a cert that chains to your trust store. Let’s Encrypt makes this free. But you have to turn on the validation. verify = 0 is a footgun that exists in a lot of copy-pasted configs. If someone’s tutorial told you to disable cert verification “for testing” and you never turned it back on, you have a nice-looking encrypted tunnel that authenticates nobody.

With spiped, MITM resistance comes from the pre-shared key. If the key is secret, an attacker can’t impersonate either end. There’s nothing to disable and no footgun equivalent — either both sides have the key or the handshake fails. The failure mode is key compromise, not configuration drift.

Practically: spiped’s security model has fewer ways to accidentally misconfigure yourself into insecurity. stunnel’s security model is more flexible and scales to situations spiped can’t cover, but it requires you to actually use the cert validation features.


Key/cert lifecycle — this is where ops teams lose sleep

Certificates expire. stunnel doesn’t expire. This combination has surprised more than a few engineers at 3 AM.

A Let’s Encrypt cert lives 90 days. Certbot handles renewal, but stunnel needs to reload the cert file after renewal — it doesn’t notice the file changed on disk. Forget the reload hook and you get a certificate expiry incident on a Redis port. Your application logs will tell you “connection refused” or “TLS handshake failed” and you’ll spend 20 minutes ruling out networking before checking the cert date.

There are ways to mitigate this — put stunnel behind a cert management system you trust, use short-lived certs with automated rotation, or use stunnel’s PSK mode to avoid certs entirely if you don’t need the full X.509 model. But for most home-lab setups you’re running certbot + a renewal hook and hoping the hook doesn’t silently fail.

spiped’s key is static until you rotate it manually. That’s both the upside and the downside. No surprise expirations. But if the key leaks — through a backup, a misconfigured secret manager, a compromised host — you need to rotate it on every machine simultaneously and restart spiped everywhere it’s deployed. With two hosts that’s a five-minute job. With twenty it’s a changelog entry and a migration plan.


Performance overhead

Honest answer: negligible for almost everything you’ll throw at either of these on a home lab or small infrastructure setup.

stunnel on a modern CPU doing TLS 1.3 with AES-NI will saturate a gigabit interface long before the crypto becomes the bottleneck. spiped similarly — the AES-CTR + HMAC-SHA256 is fast hardware-accelerated work.

Where spiped has a slight theoretical edge is in connection overhead. TLS handshakes involve more round trips than spiped’s simpler HKDF-based handshake. For long-lived connections (a persistent Redis connection, a MySQL replication stream) this is completely irrelevant. For workloads with many short-lived connections you might notice the difference in theory. In practice you’re probably not running that workload through either of these tools.

Don’t pick based on performance. Pick based on operational model.


Where each one actually fails

stunnel’s failure modes:

spiped’s failure modes:


Verdict

Use stunnel when:

Use spiped when:

And if you’re starting fresh and have the flexibility: consider WireGuard first. A WireGuard tunnel between two hosts gives you everything spiped gives you plus routing, plus multi-host support, plus tooling the whole community understands. stunnel and spiped shine when you can’t or don’t want to bring up a VPN — when you just need a single service’s traffic encrypted with minimal operational overhead.

For the Redis case: stunnel with Let’s Encrypt if it’s reachable from the internet, spiped if it’s two of your own boxes. Both are good choices. Neither requires you to rewrite anything.

Your old monitoring stack is still speaking plaintext. Now you know what to wrap it with.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
LazyLibrarian + Readarr: Automating Your Book Library
Next Post
OpenRouter vs LiteLLM

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts