Sn
Security Azure

SnapPass on Ubuntu 24.04 on Azure User Guide

| Product: SnapPass on Ubuntu 24.04 LTS on Azure

Overview

SnapPass is a self hosted one time secret service. You paste a password, an API key or any short piece of text, choose how long it may live, and SnapPass gives you back a link. The first person to open that link sees the secret once and SnapPass destroys it in the same request, so the link is dead from that moment. If nobody opens it, the secret expires on its own. It replaces the habit of pasting credentials into chat and email, where they stay in a searchable history indefinitely.

Each secret is encrypted under a key generated just for it. Only the ciphertext is stored in Redis, and the decryption key lives nowhere except inside the link you send, so the datastore on its own reveals nothing.

The cloudimg image is built for that promise rather than merely installing the software. Redis is configured in memory only so no secret material is written to disk, TLS is terminated from first boot because the link itself carries the decryption key, nginx never records a request address in its logs, and a first boot check performs a complete end to end exchange with a marked test value and refuses to finish booting if it finds it anywhere in the logs, the journal or the datastore directory.

What is included:

  • SnapPass pinned to upstream commit 464ca34e in a dedicated Python virtual environment at /opt/snappass, served by gunicorn on loopback port 5000
  • nginx terminating TLS on port 443 with a self signed certificate generated per instance, and port 80 redirecting to it
  • Redis as the only datastore, bound to loopback, with a per instance password and both RDB and AOF persistence disabled
  • Per instance sign in protecting the create surface, while retrieving a link needs no sign in
  • A credential and security round trip helper and a secret leak checker, both run as hard gates at first boot
  • Ubuntu 24.04 LTS fully patched at build time with unattended security upgrades enabled

SnapPass, nginx and Redis active, with the pinned upstream commit and the resolved dependency versions

Why this image pins a commit rather than a release tag

The last SnapPass release tag, v1.6.2, was cut in January 2024. It predates upstream's fix for host header injection in generated share links. Without that fix SnapPass builds the share link from the client supplied Host header, so anyone who can reach the create endpoint can make SnapPass mint a link pointing at a host they control. For a product whose entire value is secrecy that is not an acceptable state to ship, so this image pins the exact merge commit that carries the fix. The pin is recorded on the instance:

cat /opt/snappass/PROVENANCE.txt
SnapPass provenance for this cloudimg image
  upstream        : https://github.com/pinterest/snappass.git
  pinned commit   : 464ca34e8853c717a0611b818150dfa887b1d836
  upstream licence: MIT (see LICENSE.snappass)
  why this commit : it is the merge that carries upstream's host-header injection fix for
                    generated share links. Release tag v1.6.2 (Jan 2024) predates that fix.
  dependency set  : resolved at build time, frozen to requirements.lock.txt

Step 1: Deploy the virtual machine

Deploy the image from the Azure Marketplace. A Standard_B2s (2 vCPU, 4 GiB) is ample; SnapPass is a small Python process and Redis is capped at 128 MB.

In the network security group, allow inbound 443 from the networks that should reach the service, and 22 from your administration network. Port 80 only issues a redirect to 443, so open it if you want visitors typing a bare address to be redirected. Do not open 6379 — Redis is bound to loopback and there is nothing to reach.

Because retrieving a secret needs no sign in, anyone you send a link to must be able to reach port 443. If your recipients are outside your organisation, that means the service needs to be reachable from the internet; if they are not, restrict the source range to your own networks.

Step 2: First boot and your per instance sign in

At first boot the instance generates four things that exist nowhere in the image: a Flask secret key, a Redis password, a sign in password for the create surface, and a TLS key pair. Until they exist the instance serves nothing on a routable port at all.

Sign in over SSH and read the credentials file:

sudo cat /root/snappass-credentials.txt
# SnapPass - Per-VM Credentials (generated on first boot by snappass-firstboot.service)
# These credentials are unique to this VM. Store them somewhere safe.
SNAPPASS_URL=https://203.0.113.10/
SNAPPASS_WEB_USER=snappass
SNAPPASS_WEB_PASSWORD=************************ (unique to this VM)
SNAPPASS_REDIS_PASSWORD=************************************ (unique to this VM)

The per instance credentials file and the file permissions on every piece of secret material

Confirm the services are up and the instance is answering:

systemctl is-active snappass nginx redis-server
active
active
active
curl -ks -o /dev/null -w "healthz: HTTP %{http_code}\n" https://127.0.0.1/healthz
curl -s -o /dev/null -w "port 80: HTTP %{http_code} (redirect to TLS)\n" http://127.0.0.1/
healthz: HTTP 200
port 80: HTTP 301 (redirect to TLS)

The certificate is self signed and unique to this instance, so a browser warns on the first visit. That is expected; Step 6 shows how to install a certificate for a real hostname.

Step 3: Share your first secret

Open the address in SNAPPASS_URL in a browser and sign in with SNAPPASS_WEB_USER and SNAPPASS_WEB_PASSWORD. Paste the secret, choose how long it may live, and select Generate URL.

The SnapPass create form with a secret pasted in and an expiry selected

SnapPass returns the one time link. Send that link to your recipient by whatever channel you like. The link is the only thing that can decrypt the secret, and it needs no sign in, so the recipient does not need an account on your instance.

The one time link SnapPass minted, with a copy to clipboard button

When the recipient opens the link they get a confirmation page first, so a link preview crawler in a chat client cannot silently burn the secret. Nothing is consumed until they select Reveal secret.

The recipient's confirmation page warning that the secret can only be revealed once

Revealing shows the plaintext and destroys it in the same request.

The revealed secret, with confirmation that it has been permanently deleted

Opening the same link again shows nothing. This is the whole point of the product, and it is the behaviour the instance verifies for itself at first boot.

The same link opened a second time, showing Secret not found

Step 4: Prove the one time property yourself

The same flow is available over an API, which makes it easy to demonstrate. This block creates a secret through the gated create endpoint, retrieves it once, and retrieves it again:

W=$(sudo grep '^SNAPPASS_WEB_PASSWORD=' /root/snappass-credentials.txt | cut -d= -f2-)
TOKEN=$(curl -ks -u "snappass:$W" -H "Content-Type: application/json" -X POST https://127.0.0.1/api/v2/passwords -d '{"password":"demo-not-a-real-secret","ttl":3600}' | sed -n 's/.*"token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
echo "created a secret (token withheld)"
curl -ks -o /dev/null -w "  first retrieval:  HTTP %{http_code}\n" https://127.0.0.1/api/v2/passwords/$TOKEN
curl -ks -o /dev/null -w "  second retrieval: HTTP %{http_code}\n" https://127.0.0.1/api/v2/passwords/$TOKEN
created a secret (token withheld)
  first retrieval:  HTTP 200
  second retrieval: HTTP 404

The password is read into a shell variable from the root only credentials file rather than typed on the command line, and the token is never echoed, because a SnapPass token contains the secret's decryption key.

You can also confirm that the create surface is closed to anyone without the instance credential:

curl -ks -o /dev/null -w "unauthenticated GET /: HTTP %{http_code}\n" https://127.0.0.1/
W=$(sudo grep '^SNAPPASS_WEB_PASSWORD=' /root/snappass-credentials.txt | cut -d= -f2-)
curl -ks -o /dev/null -u "snappass:$W" -w "authenticated GET /: HTTP %{http_code}\n" https://127.0.0.1/
unauthenticated GET /: HTTP 401
authenticated GET /: HTTP 200

A helper on the instance runs the full set of checks in one go — one time retrieval, expiry, an exact plaintext match, no plaintext at rest, and the Redis exposure checks:

sudo bash /usr/local/sbin/snappass-cred-roundtrip.sh
OK: create gated by per-VM basic auth; secret retrieved once with an exact plaintext match;
OK: second retrieval 404 (one-time property holds); 3s TTL expired the secret;
OK: no plaintext at rest in Redis (ciphertext only); Redis requires the per-VM password,
OK: refuses the routable address, and has RDB+AOF persistence disabled with no files on disk.

The credential and security round trip proving the one time property, expiry and the at rest checks

Step 5: What the instance does to protect the secret

Nothing is written to disk. Redis holds live secrets in memory only. Both RDB snapshots and the append only file are disabled, so a secret cannot outlive its expiry in a file on the OS disk and cannot survive a reboot. This is a deliberate trade off: restarting the instance drops any secret that has not yet been retrieved, and the sender simply shares a new link.

redis-cli ping
R=$(sudo grep '^SNAPPASS_REDIS_PASSWORD=' /root/snappass-credentials.txt | cut -d= -f2-)
echo "save       = [$(redis-cli --no-auth-warning -a "$R" config get save 2>/dev/null | tail -1)]"
echo "appendonly = $(redis-cli --no-auth-warning -a "$R" config get appendonly 2>/dev/null | tail -1)"
sudo ls -A /var/lib/redis | wc -l | xargs -I{} echo "files under /var/lib/redis: {}"
NOAUTH Authentication required.
save       = []
appendonly = no
files under /var/lib/redis: 0

Only nginx is reachable. SnapPass itself and Redis are bound to loopback:

ss -tlnH | awk '{print $1, $4}' | sort -u
LISTEN 0.0.0.0:22
LISTEN 0.0.0.0:443
LISTEN 0.0.0.0:80
LISTEN 127.0.0.1:5000
LISTEN 127.0.0.1:6379
LISTEN 127.0.0.53%lo:53
LISTEN 127.0.0.54:53
LISTEN [::1]:6379
LISTEN [::]:22
LISTEN [::]:443
LISTEN [::]:80

The link never reaches a log. A SnapPass retrieval address contains the secret's decryption key, so nginx uses a log format that records no request address at all, logging is switched off entirely on every location that can carry a token, gunicorn's access log is never enabled, and a Referrer-Policy: no-referrer header stops a link inside a revealed secret leaking the address to a third party site. The instance proves this rather than asserting it: a checker performs a real end to end exchange with a marked test value and then searches the nginx logs, the system logs, the whole systemd journal, the Redis directory and the application tree for the plaintext, the token and the bare decryption key.

sudo bash /usr/local/sbin/snappass-secret-leak-check.sh
OK: an end-to-end secret exchange left no plaintext, no one-time token and no decryption
OK: key in nginx logs, system logs, the systemd journal, /var/lib/redis, or the app tree.

This runs as a hard gate at first boot, so an instance that could leak never finishes booting into a serving state.

Loopback only binds, Redis refusing an unauthenticated client, persistence disabled and the leak check passing

Step 6: Install a certificate for a real hostname

The instance ships with a self signed certificate carrying its own address, which is why browsers warn on the first visit. Once you point a DNS name at the instance, replace it.

Install a certificate for your hostname, for example with Let's Encrypt. Stop nginx first so the HTTP challenge can bind port 80, and put the issued certificate where nginx already expects it:

Replace <your-domain> and <your-email> below with your own values.

sudo apt-get update && sudo apt-get install -y certbot
sudo systemctl stop nginx
sudo certbot certonly --standalone -d secrets.<your-domain> --agree-tos -m <your-email> --non-interactive
sudo ln -sf /etc/letsencrypt/live/secrets.<your-domain>/fullchain.pem /etc/ssl/snappass/server.crt
sudo ln -sf /etc/letsencrypt/live/secrets.<your-domain>/privkey.pem /etc/ssl/snappass/server.key
sudo systemctl start nginx

Then point the share links at that name instead of the instance address. SnapPass builds every link from HOST_OVERRIDE, which is what makes it immune to a spoofed Host header, so this value is the single control over what your recipients see:

sudo sed -i 's/^HOST_OVERRIDE=.*/HOST_OVERRIDE=secrets.<your-domain>/' /etc/snappass/snappass.env
sudo systemctl restart snappass.service
curl -s -o /dev/null -w "still serving: HTTP %{http_code}\n" https://secrets.<your-domain>/healthz

Step 7: Change or remove the sign in on the create surface

The sign in protects only the create surface. Retrieving a link is deliberately open, because the token is itself the credential and your recipients are usually outside your organisation.

To change the password, write a new hash into the same file, substituting your own value for <new-password>:

sudo sh -c 'printf "%s:%s\n" snappass "$(openssl passwd -apr1 <new-password>)" > /etc/nginx/.snappass-htpasswd'
sudo chown root:www-data /etc/nginx/.snappass-htpasswd
sudo chmod 640 /etc/nginx/.snappass-htpasswd
sudo systemctl reload nginx

To add a second person, append another line to the same file rather than replacing it.

To remove the sign in entirely and let anyone who can reach the instance create secrets, comment out the two auth_basic lines in each of the three create locations in /etc/nginx/sites-available/cloudimg-snappass and reload nginx. Think carefully before doing this on an internet facing instance: an open create endpoint is an open invitation to use your machine as free anonymous hosting. Restricting the network security group source range is usually the better control.

Step 8: Day to day operation

Check the three services and when SnapPass last started:

systemctl is-active snappass.service nginx.service redis-server.service
systemctl show snappass.service -p ActiveState -p ActiveEnterTimestamp

Restarting Redis does not take SnapPass down; the units are ordered but not bound together, and the client reconnects. Restarting either service does, by design, drop any secret that has not yet been retrieved.

There is nothing to back up. That is not an omission: the only state this appliance holds is live secrets, and keeping a copy of those would defeat the product. Back up your instance's configuration if you like, but note that /etc/snappass/snappass.env, /etc/nginx/.snappass-htpasswd and /etc/ssl/snappass/server.key all contain secret material and must never be copied to a location that is less protected than they are.

The operating system is fully patched at build time and unattended security upgrades stay enabled, so the instance keeps taking security updates on its own.

Troubleshooting

A browser warns that the certificate is not trusted. Expected on a fresh instance: the certificate is self signed and unique to that instance. Step 6 replaces it.

Creating a secret returns HTTP 400. SnapPass refuses to build a link from an untrusted Host header. Confirm HOST_OVERRIDE in /etc/snappass/snappass.env matches the name or address your users actually reach the instance on, then restart the service.

A link says "Secret not found" sooner than expected. Either it was already revealed, its expiry elapsed, or the instance restarted. Because the datastore is in memory only, a reboot clears anything not yet retrieved.

SnapPass will not start. The service refuses to start without a per instance secret key and an authenticated datastore address, which is deliberate. Check that /etc/snappass/snappass.env exists and that first boot completed:

systemctl is-active snappass-firstboot.service
sudo tail -20 /var/log/snappass-firstboot.log

Support

cloudimg provides 24/7 technical support for this image by email at support@cloudimg.co.uk and by live chat, with a one hour average response time for critical issues.

SnapPass is an open source project of Pinterest, Inc., distributed under the MIT licence, and is referenced here only to identify the software this image packages. SnapPass and Pinterest are trademarks of Pinterest, Inc. cloudimg is not affiliated with, endorsed by, or sponsored by Pinterest, Inc.