Cryptgeon on Ubuntu 24.04 on Azure User Guide
Overview
Cryptgeon is an open source service for sharing a secret once. You paste a password, an API key, a recovery phrase or a file, choose how many times it may be opened or how long it may live, and hand over a single link. The moment the recipient opens it, the note is destroyed.
The property that makes it different from a paste bin is that your own server cannot read what it stores. Each note is encrypted in the sender's browser, and the decryption key is placed in the fragment of the share link, after the #. Browsers never transmit the fragment, so the key never reaches your VM. What arrives is ciphertext and nothing else.
The cloudimg image builds Cryptgeon 2.9.3 from the pinned upstream source, runs it as a hardened systemd service behind nginx, and locks the in memory note store with a password unique to your VM that is generated on first boot.
What is included:
- Cryptgeon 2.9.3, built from upstream commit
40f3559, installed at/opt/cryptgeon - nginx on port
80as the only listener reachable from outside the VM - The application bound to
127.0.0.1:8000and Redis bound to127.0.0.1:6379, neither reachable off the VM - A 48 character Redis password generated on first boot, before anything opens a port
- No note persistence at all: the store runs in RAM with snapshots and append only logging disabled
- Note limits set to bounded, documented values: 4 MiB per note, up to 100 views, up to 360 minutes
cryptgeon.servicerunning as the unprivilegedcryptgeonuser- 24/7 cloudimg support
Prerequisites
An active Azure subscription, an SSH key, and a VNet with a subnet. Standard_B2s is the recommended size; Cryptgeon is light, and its memory use is bounded by the note store cap rather than by traffic.
Add an inbound network security group rule for TCP 80 (and 443 once you have installed a certificate) from wherever your users are. Do not open ports 8000 or 6379: they are bound to loopback and are not meant to be reachable.
HTTPS is required for real use. Cryptgeon encrypts in the browser using the Web Crypto API, and browsers only expose that API on a secure origin. Over plain
http://to an IP address the encryption cannot run, so the page will not create notes. Step 6 covers installing a certificate. Until you do, test only fromhttp://localhostthrough an SSH tunnel, which browsers treat as secure.
Step 1: Deploy and connect
Deploy the image from the Azure Marketplace, then connect over SSH as azureuser:
ssh azureuser@<vm-ip>
Confirm all three services came up, check the version the running binary reports, and confirm the listener layout:
sudo systemctl is-active cryptgeon redis-server nginx
curl -s http://127.0.0.1/api/status | jq '{version, max_size, max_views, max_expiration}'
ss -tln | grep -E ':(80|8000|6379) ' | awk '{print $1" "$4}'
cryptgeon.service carries a fail closed pre start gate, so seeing it active is itself the proof that the note store is password protected. You should see 2.9.3, a max_size of 4194304 bytes, and exactly one publicly bound listener on :80 with 8000 and 6379 on 127.0.0.1.

Step 2: Review the per VM secrets
Every VM built from this image generates its own note store password on first boot. Nothing is shared between VMs and nothing is baked into the image.
sudo sed -n '1,32p' /stage/scripts/cryptgeon-credentials.log
sudo stat -c '%a %U:%G %n' /stage/scripts/cryptgeon-credentials.log /etc/cryptgeon/cryptgeon.env
The file is mode 0600, owned by root, and it opens with the complete secret inventory for this product. That inventory is deliberately short, and it is worth reading once:
REDIS_PASSWORDis the only secret Cryptgeon uses. It protects the note store, which holds every note's ciphertext.- There is no signing, session or JWT secret. The entire configuration surface of the Cryptgeon backend is two source files, and the only secret bearing variable in either is the Redis connection string. There is no server side key material to rotate.
- There is no administrator or user account. Cryptgeon is anonymous by design: no accounts, no login, no admin console. No default credential exists, so none ships enabled.
- Note encryption keys never reach the server. They are generated in the visitor's browser and live in the URL fragment.
- The TLS certificate is yours to supply. See Step 6.

Step 3: Watch a note self destruct
This is the whole product in three commands. Create a note limited to one view, read it, then try to read it again. Reading is a DELETE, because reading is the destruction.
NOTE_ID=$(curl -sS -X POST http://127.0.0.1/api/notes \
-H 'Content-Type: application/json' \
-d '{"meta":"demo","contents":"launch-codes-42","views":1}' | jq -r .id)
echo "created note id: ${NOTE_ID:0:24}..."
echo "first read -> $(curl -sS -X DELETE http://127.0.0.1/api/notes/$NOTE_ID | jq -r .contents)"
echo "second read -> HTTP $(curl -sS -o /dev/null -w '%{http_code}' http://127.0.0.1/api/notes/$NOTE_ID) (note destroyed)"
The first read returns the payload. The second returns 404: the note is gone from the store and cannot be recovered by anyone, including you.

Note that these commands send plaintext, because curl is not a browser and cannot run the client side encryption. That is fine for a health check, but it is not how the product is meant to be used. Real notes are encrypted in the browser, which is what Step 5 shows.
Step 4: Confirm the note store is locked down
Redis holds every note's ciphertext, so it is the surface that matters most on this VM. Read the password out of the credentials file rather than pasting it around, then probe the store the way an attacker would:
REDIS_PASSWORD="$(sudo grep '^REDIS_PASSWORD=' /stage/scripts/cryptgeon-credentials.log | cut -d= -f2-)"
echo "anonymous PING -> $(redis-cli ping </dev/null 2>&1 | head -1)"
echo "anonymous KEYS * -> $(redis-cli keys '*' </dev/null 2>&1 | head -1)"
echo "wrong password -> $(redis-cli -a wrong --no-auth-warning ping </dev/null 2>&1 | head -1)"
echo "per-VM password -> $(redis-cli -a "$REDIS_PASSWORD" --no-auth-warning ping </dev/null 2>/dev/null)"
echo "rdb snapshots -> $(redis-cli -a "$REDIS_PASSWORD" --no-auth-warning config get save </dev/null 2>/dev/null | tail -1 | sed 's/^$/(none)/')"
echo "append-only log -> $(redis-cli -a "$REDIS_PASSWORD" --no-auth-warning config get appendonly </dev/null 2>/dev/null | tail -1)"
echo "note data on disk -> $(ls /var/lib/redis/*.rdb /var/lib/redis/appendonlydir 2>/dev/null | wc -l) file(s)"
Anonymous commands are refused with NOAUTH, a wrong password is refused with WRONGPASS, and only the per VM password returns PONG. Snapshots report (none) and the append only log reports no, which together are the proof that no note ciphertext is ever written to this VM's disk. Nothing survives a reboot, and nothing can be recovered from a disk snapshot or a backup.

Step 5: Use the web interface
Open the site in a browser. Until you have installed a certificate in Step 6, use an SSH tunnel from your workstation, because browsers only enable the Web Crypto API on a secure origin and localhost counts as one:
ssh -L 8080:127.0.0.1:80 azureuser@<vm-ip>
Then browse to http://localhost:8080. Type your secret into the note box. The footer tells you the current limit, and the line underneath tells you what will happen to the note. Open advanced if you want to set a view count or a time limit, or switch to file to upload a file instead.

Press create. The note is encrypted in your browser first, and only then is the ciphertext sent to the server. You get back a share link and a QR code. Everything after the # in that link is the decryption key, and it never leaves your browser, so anyone who can read your server's memory or its note store still cannot read the note.

Send that link to the recipient. When they open it they see a confirmation prompt, and pressing show note fetches the ciphertext, decrypts it in their browser, and deletes it from the server in the same operation.

Open the same link a second time and there is nothing left to fetch.

Step 6: Install a TLS certificate
Cryptgeon depends on the browser's Web Crypto API, which browsers disable on insecure origins. Without HTTPS your users cannot create or read notes at all except over a localhost tunnel. Point a DNS record at the VM, open port 443 in your network security group, then issue a certificate.
The following block is deliberately customer specific: replace <your-domain> and <your-email> with your own values before running it.
sudo apt-get update && sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d <your-domain> -m <your-email> --agree-tos --redirect --non-interactive
Certbot rewrites the shipped nginx server block in place, keeping the client_max_body_size and the proxy settings, and installs a renewal timer. Confirm afterwards with sudo nginx -t and sudo systemctl reload nginx.
Step 7: Tune the limits
All configuration lives in one file. Every value the Cryptgeon backend reads is listed there, with the shipped defaults chosen to be bounded rather than permissive:
sudo grep -vE '^\s*#|^\s*$' /etc/cryptgeon/cryptgeon.env | sed 's/^REDIS=.*/REDIS=redis:\/\/:<redacted>@127.0.0.1:6379\//'
| Setting | Shipped value | Meaning |
|---|---|---|
SIZE_LIMIT |
4194304 (4 MiB) |
Largest request body. The UI shows roughly 2.96 MiB of usable note, because encoding adds about 35 percent. Upstream allows up to 512 MiB, but an unbounded note size on a small VM is an easy denial of service. |
MAX_VIEWS |
100 |
Highest view count a note may request. |
MAX_EXPIRATION |
360 |
Longest lifetime in minutes. |
ALLOW_ADVANCED |
true |
Lets users choose views and expiry. Set to false to force every note to be one view only. |
ALLOW_FILES |
true |
Allows file uploads as well as text. |
ID_LENGTH |
32 |
Note id length in bytes. Lower it for shorter links; it does not affect encryption strength. |
LISTEN_ADDR |
127.0.0.1:8000 |
Loopback only. nginx is the public listener. Do not change this. |
After editing, restart the service and confirm the new values are live:
sudo systemctl restart cryptgeon.service
for i in $(seq 1 30); do curl -sf -o /dev/null http://127.0.0.1/api/live && break; sleep 1; done
curl -s http://127.0.0.1/api/status | jq '{max_size, max_views, max_expiration, allow_advanced, allow_files}'
The wait loop matters: the service takes a moment to rebind after a restart, and querying it too early returns nothing to parse.
The note store is capped at 512 MB with a least recently used eviction policy, which is why the UI warns that notes are held in RAM and the oldest may be removed. Raise maxmemory in /etc/redis/cloudimg-cryptgeon.conf if you expect heavy use, keeping it comfortably below the VM's total memory.
Step 8: Components
| Component | Location |
|---|---|
| Cryptgeon binary | /opt/cryptgeon/cryptgeon |
| Web frontend | /opt/cryptgeon/frontend |
| Configuration | /etc/cryptgeon/cryptgeon.env (mode 0640, root:cryptgeon) |
| Per VM secrets | /stage/scripts/cryptgeon-credentials.log (mode 0600, root:root) |
| Note store overrides | /etc/redis/cloudimg-cryptgeon.conf |
| nginx site | /etc/nginx/sites-available/cryptgeon |
| Fail closed pre start gate | /usr/local/sbin/cryptgeon-require-redis-auth.sh |
| First boot script | /usr/local/sbin/cryptgeon-firstboot.sh |
| Service units | cryptgeon.service, cryptgeon-firstboot.service |
Logs are in the journal:
sudo journalctl -u cryptgeon.service -n 20 --no-pager
Step 9: Security notes
- The server cannot read your notes. Encryption happens in the browser and the key stays in the URL fragment. This is structural, not a policy setting, and it holds even against someone with root on the VM.
- Nothing is written to disk. The note store keeps everything in memory with persistence disabled, so no note reaches the filesystem, a snapshot or a backup.
- The note store is loopback bound and password protected on every boot. The password is generated per VM at first boot. The application and nginx are both gated on a bootstrap marker that only appears after that password exists, so nothing serves a request before the store is locked. A pre start check independently refuses to start Cryptgeon against an unauthenticated store.
- There are no accounts to protect, and no admin console to expose. Anyone who can reach the page can create a note. If that is not what you want, restrict access at the network security group or put your own authenticating proxy in front of port 80.
- A share link is the secret. Treat it like the credential it carries: send it over a different channel from the one that will use it, and prefer short view counts and expiry times.
- Security updates continue to install automatically through
unattended-upgrades.
Step 10: Support and licensing
Cryptgeon is licensed under the MIT licence. There is no per seat, per CPU or per site fee for the software itself. The cloudimg charge covers packaging, security patching, image maintenance and 24/7 expert support.
Email support@cloudimg.co.uk for help with certificates, limits, branding, network rules or upgrades. Critical issues receive a one hour average response time.
Deploy on Azure
Find this image on the Azure Marketplace and browse the rest of the catalogue at cloudimg.co.uk/products.