Security AWS

Cryptgeon 2.9.3 on AWS User Guide

| Product: Cryptgeon 2.9.3 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Cryptgeon 2.9.3 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg identity, secrets and PKI listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Cryptgeon is an open source service for sharing a secret once. Paste a password, an API key or a file, choose how many times it may be opened or how long it may live, and send the link. Each note is encrypted in the sender's browser (AES-GCM, with a key generated on that device). The key is placed in the part of the link after the #, which browsers never send to a server, so the instance only ever receives and stores ciphertext. When the last permitted view happens, or the time limit runs out, the note is deleted.

The cloudimg image is a hardened, ready-to-use Cryptgeon server:

  • nginx in front, Cryptgeon behind it. nginx is the only public listener (ports 80 and 443). Cryptgeon listens on 127.0.0.1:8000, the loopback address only.
  • HTTPS from the first boot. Browsers only allow the Web Crypto functions Cryptgeon needs on a secure (HTTPS) page, so the image serves HTTPS from the first boot with a certificate generated on your instance (an end-entity certificate, not a certificate authority). Port 80 redirects to HTTPS. The instance's name is set from its own EC2 public DNS name at first boot; you can change it to your own domain.
  • Notes in memory only. Notes are stored by Valkey, the Redis-compatible key-value store from Ubuntu 24.04, with snapshots and the append-only file switched off: a note never reaches a disk, a snapshot or a backup. Valkey listens on 127.0.0.1 and ::1 only.
  • A per-instance note store password, never in a process environment. First boot generates a random Valkey password. Cryptgeon reads it from a file only its own service user can read; it is never on a command line, in a systemd unit or in a process environment, and Valkey itself keeps only its SHA-256 hash. The one Valkey account may run only the commands Cryptgeon uses. Valkey does not start at all until first boot has created that account.
  • Abuse limits. There are no accounts: anyone who can reach the address can create notes, which is how Cryptgeon is designed. The image bounds what one client can do: a note request is at most 4 MiB, at most 100 views and at most 360 minutes, nginx allows 12 new notes per minute per client address (with a burst of 20), and a page on another website cannot create or read notes here.
  • Real client addresses, fixed host name. The rate limits use the real TCP peer address, never a header a client can forge. Requests for any host name other than the instance's configured names are dropped.
  • Browser hardening. A Content-Security-Policy allows only the application's own scripts (the one inline start-up script is pinned by its hash), so injected script cannot read a note's key from the page; framing is refused; the server sends no referrer. The nginx access log records /api/notes/:id instead of note ids.
  • Locked down. TLS 1.2 and 1.3 only; Cryptgeon, nginx, Valkey and the first-boot service are blocked from the instance metadata service, and the AMI requires IMDSv2.
  • Built from source, verified, and fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on. See Step 12 for exactly how it is built.

What is on the image: Cryptgeon 2.9.3 (a cloudimg build of the upstream v2.9.3 tag, /usr/local/bin/cryptgeon, with its web app in /usr/share/cryptgeon/frontend), Valkey 7.2.13 and nginx from Ubuntu 24.04, cloud-init and the AWS Systems Manager agent.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet.
  • A security group that allows TCP 22 (SSH) from your own networks only, and TCP 443 (the web app) and TCP 80 (redirects to HTTPS) only from the networks whose people should use this service: your office, your VPN, or your VPC CIDR. Anyone who can reach port 443 can create notes, so do not open it to 0.0.0.0/0 unless you want a public service. Do not open 8000 or 6379: Cryptgeon and Valkey listen on the loopback address only.
  • For production, a DNS name you control (for example secrets.example.com) and, ideally, a certificate from a certificate authority. Until then the instance uses its EC2 public DNS name with its own self-signed certificate.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Cryptgeon 2.9.3 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair, and a security group as above.

With the AWS CLI (replace the values in angle brackets):

aws ec2 run-instances --image-id <ami-id-from-the-listing> --instance-type m5.large \
  --key-name <your-key-pair> --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=cryptgeon}]'

First boot takes a few seconds after the instance is running. If you want the instance to use your own name from the start, put it in /etc/cloudimg/cryptgeon-domain with user data (for example a cloud-init write_files entry); otherwise first boot uses the instance's public DNS name, or its public IP address if it has no DNS name.

Step 2: Connect to your instance

Operating system SSH user
Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

systemctl is-active cryptgeon-firstboot valkey-server cryptgeon nginx
active
active
active
active

Only SSH, HTTP and HTTPS are public. Cryptgeon and Valkey listen on the loopback address:

sudo ss -tln | grep -E ':(22|80|443|6379|8000) '
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*
LISTEN 0      4096         0.0.0.0:22        0.0.0.0:*
LISTEN 0      511        127.0.0.1:6379      0.0.0.0:*
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*
LISTEN 0      128        127.0.0.1:8000      0.0.0.0:*
LISTEN 0      511            [::1]:6379         [::]:*
LISTEN 0      511             [::]:443          [::]:*
LISTEN 0      4096            [::]:22           [::]:*
LISTEN 0      511             [::]:80           [::]:*

A local health check (answered only on the loopback address) confirms that Cryptgeon can reach its note store:

curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1/api/live
200

Step 4: Read the address and the certificate fingerprint

sudo cat /root/cryptgeon-credentials.txt

The file (root only, mode 0600) gives this instance's address and the SHA-256 fingerprint of its certificate, plus the note limits. There is no administrator password: Cryptgeon has no accounts and no admin console.

# Cryptgeon 2.9.3 - written on first boot by cryptgeon-firstboot.service. Unique to this instance.
CRYPTGEON_URL=https://ec2-100-53-91-212.compute-1.amazonaws.com/
CRYPTGEON_TLS_SHA256=FE:DE:21:21:...:(64 hex digits in pairs)
CRYPTGEON_SIZE_LIMIT_BYTES=4194304
CRYPTGEON_MAX_VIEWS=100
CRYPTGEON_MAX_EXPIRATION_MINUTES=360

# There are no accounts: anyone who can reach CRYPTGEON_URL can create notes. Restrict the security group's
# 80/443 rules to the networks that should use it (your office, VPN or VPC CIDR).
...

The certificate is self-signed and unique to your instance, so your browser warns about it. Open the certificate details in the browser, check that its SHA-256 fingerprint matches CRYPTGEON_TLS_SHA256, and only then continue. Do not add the certificate to your operating system's trust store; for production, replace it with a certificate from a certificate authority (Step 9). The certificate is an end-entity certificate for this instance's names only:

openssl x509 -in /etc/ssl/cryptgeon/cryptgeon.crt -noout -subject -ext basicConstraints,keyUsage,extendedKeyUsage,subjectAltName
subject=CN = ec2-100-53-91-212.compute-1.amazonaws.com, O = cloudimg
X509v3 Subject Alternative Name: 
    DNS:ec2-100-53-91-212.compute-1.amazonaws.com, IP Address:100.53.91.212, IP Address:172.31.93.75
X509v3 Basic Constraints: critical
    CA:FALSE
X509v3 Key Usage: critical
    Digital Signature, Key Encipherment
X509v3 Extended Key Usage: 
    TLS Web Server Authentication

Step 5: Share a secret

Browse to the CRYPTGEON_URL address. Type or paste the secret:

The Cryptgeon home page with a secret typed into the note field, the file and advanced switches, the create button and the maximum size

Choose create. Your browser encrypts the note and uploads only the ciphertext; the page then shows the share link (also as a QR code). Send the link to the recipient over any channel:

The share link and QR code shown after the note was created, with the availability notice

The recipient opens the link and chooses show note. Their browser downloads the ciphertext, decrypts it with the key from the link, and shows the secret:

The recipient's view: the decrypted secret with the warning that it cannot be seen again

The note is now gone. Opening the same link again shows that it was not found:

Opening the link a second time: note was not found or was already deleted

By default a note may be opened once. Switch on advanced to allow more views (up to 100) or to keep the note for a number of minutes instead (up to 360), or to protect it with your own password, which is then not part of the link:

The advanced options: views, expiry in minutes and a custom password

Step 6: Share a file

Switch on file, select one or more files and choose create. The files are encrypted in the browser like a text note; the recipient downloads and decrypts them:

A file selected for a new note

The recipient's view of a file note: the file name, its type and size, and download all

A note request is limited to 4 MiB, which after encryption and encoding means files of about 3 MB in total (the page shows the exact figure, max: 2.96 MiB). A larger file is refused:

A file over the limit: could not create note, note is too big

Step 7: Use the API

The web app talks to a small JSON API, which you can also call from scripts. The server never sees a key: anything you send to POST /api/notes is stored as you send it, so encrypt it first (the Cryptgeon command-line client, npx cryptgeon, does that for you). On the instance itself, with the instance's own certificate as the trust anchor:

U=$(sudo sed -n 's/^CRYPTGEON_URL=//p' /root/cryptgeon-credentials.txt)
curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt "${U}api/status"; echo
{"version":"2.9.3","max_size":4194304,"max_views":100,"max_expiration":360,"allow_advanced":true,"allow_files":true,"imprint_url":"","imprint_html":"","theme_image":"","theme_text":"","theme_page_title":"","theme_favicon":"","theme_new_note_notice":true,"theme_home_link":true}

Create a note that may be read once, look at it without using the view (GET returns only its metadata), read it (DELETE returns the contents and uses the view), and try again:

U=$(sudo sed -n 's/^CRYPTGEON_URL=//p' /root/cryptgeon-credentials.txt)
ID=$(curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt -H 'Content-Type: application/json' \
  --data '{"meta":"{\"type\":\"text\"}","contents":"api-test","views":1}' "${U}api/notes" | python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
echo "created a note with id of ${#ID} characters"
curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt -o /dev/null -w 'preview: %{http_code}\n' "${U}api/notes/${ID}"
curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt -X DELETE -w '\nfirst read: %{http_code}\n' "${U}api/notes/${ID}"
curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt -o /dev/null -X DELETE -w 'second read: %{http_code}\n' "${U}api/notes/${ID}"
created a note with id of 64 characters
preview: 200
{"meta":"{\"type\":\"text\"}","contents":"api-test"}
first read: 200
second read: 404

Requests outside the limits are refused, and so is a request a page on another website makes (its Origin is not this instance):

U=$(sudo sed -n 's/^CRYPTGEON_URL=//p' /root/cryptgeon-credentials.txt)
curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' --data '{"meta":"x","contents":"x","views":101}' "${U}api/notes"
curl -s --cacert /etc/ssl/cryptgeon/cryptgeon.crt -o /dev/null -w '%{http_code}\n' -H 'Content-Type: application/json' -H 'Origin: https://evil.example' --data '{"meta":"x","contents":"x","views":1}' "${U}api/notes"
400
403

Step 8: Limits and abuse protection

The limits Cryptgeon enforces are in /etc/cryptgeon/cryptgeon.env:

sudo grep -E '^(LISTEN_ADDR|SIZE_LIMIT|MAX_VIEWS|MAX_EXPIRATION|ALLOW_ADVANCED|ALLOW_FILES)=' /etc/cryptgeon/cryptgeon.env
LISTEN_ADDR=127.0.0.1:8000
SIZE_LIMIT=4194304
MAX_VIEWS=100
MAX_EXPIRATION=360
ALLOW_ADVANCED=true
ALLOW_FILES=true

SIZE_LIMIT is in bytes. ALLOW_FILES=false turns file notes off; ALLOW_ADVANCED=false makes every note a one-view note. After editing the file, run sudo systemctl restart cryptgeon. If you raise SIZE_LIMIT above 4 MiB, also raise client_max_body_size (5m) in /usr/local/lib/cloudimg/cryptgeon/nginx-site.tmpl and run sudo cryptgeon-set-domain --boot, then sudo systemctl reload nginx.

nginx limits how fast one client address can create notes and call the API. The address is the real TCP peer; a forged X-Forwarded-For header neither changes it nor appears in the log:

sudo grep -E 'limit_req_zone|rate=' /etc/nginx/conf.d/cloudimg-cryptgeon-maps.conf
limit_req_zone $cg_create_key zone=cg_create:10m rate=12r/m;
limit_req_zone $binary_remote_addr zone=cg_api:10m rate=20r/s;

A client over the limit gets HTTP 429 until its allowance refills. The access log records the note API without note ids:

sudo tail -3 /var/log/nginx/access.log | sed -E 's/^[0-9.]+ /<client-ip> /'
<client-ip> - - [29/Oct/2026:10:05:04 +0000] "DELETE /api/notes/:id HTTP/2.0" 404 0 "curl/8.5.0"
<client-ip> - - [29/Oct/2026:10:05:04 +0000] "POST /api/notes HTTP/2.0" 400 13 "curl/8.5.0"
<client-ip> - - [29/Oct/2026:10:05:04 +0000] "POST /api/notes HTTP/2.0" 403 146 "curl/8.5.0"

Who can create notes is decided by your security group. Keep the 443 and 80 rules limited to the networks that should use the service. To put the service on a private address only, launch it in a private subnet (or remove the public IP) and allow 443 from your VPC CIDR or VPN range.

The note store and memory. Valkey keeps everything in RAM with a 512 MB cap. Notes with an expiry time may be removed early when the store is full; notes limited by view count are never removed early, so when the store is full of them a new note is refused (the page says the note could not be created) until notes are read or expire. A reboot or a Valkey restart deletes every unread note. Check the store with:

sudo cryptgeon-valkey-password check
sudo grep -E '^(bind|port|save|appendonly|maxmemory|maxmemory-policy|tls-port|cluster-enabled) ' /etc/valkey/cloudimg-cryptgeon.conf
VALKEY_PASSWORD_OK auth=PONG anonymous=NOAUTH wrong=WRONGPASS save='' appendonly=no
bind 127.0.0.1 -::1
port 6379
save ""
appendonly no
maxmemory 512mb
maxmemory-policy volatile-ttl
tls-port 0
cluster-enabled no

To change the cap, edit maxmemory in /usr/local/lib/cloudimg/cryptgeon/valkey.conf.tmpl and run sudo cryptgeon-valkey-password rotate (it re-renders the file with a new password and restarts Valkey and Cryptgeon, which deletes all unread notes).

Step 9: Use your own domain and certificate

Point a DNS record (for example secrets.example.com) at the instance, then:

sudo cryptgeon-set-domain secrets.example.com

nginx then serves that name (the instance's EC2 name and addresses keep working) and, if the certificate is still the cloudimg self-signed one, a new self-signed certificate is issued for the new name. Share links use the name the sender browsed to. To use a certificate from a certificate authority, replace /etc/ssl/cryptgeon/cryptgeon.crt (full chain) and /etc/ssl/cryptgeon/cryptgeon.key (mode 0600, root) and run sudo systemctl reload nginx; cryptgeon-set-domain never overwrites a certificate that is not cloudimg's own. Check the current name and fingerprint with sudo cryptgeon-set-domain --show.

Step 10: The note store password

The Valkey password is generated on each instance's first boot. Cryptgeon reads it from /etc/cryptgeon/.env (mode 0600, owned by the cryptgeon service user) with its own configuration loader, so it is not in the service's environment; Valkey's file holds only its SHA-256:

sudo stat -c '%a %U %n' /etc/cryptgeon/.env /etc/valkey/cloudimg-cryptgeon.conf
600 cryptgeon /etc/cryptgeon/.env
600 valkey /etc/valkey/cloudimg-cryptgeon.conf

To change it, run sudo cryptgeon-valkey-password rotate. Like any Valkey restart, this deletes all unread notes.

Step 11: Security notes

  • Metadata service. The AMI requires IMDSv2, and Cryptgeon, nginx, Valkey and the first-boot service are denied the instance metadata addresses by systemd. Cryptgeon makes no outbound requests at all.
systemctl show -p IPAddressDeny cryptgeon nginx valkey-server cryptgeon-firstboot
IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 ::ffff:169.254.169.254/128

IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 ::ffff:169.254.169.254/128

IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 ::ffff:169.254.169.254/128

IPAddressDeny=fd00:ec2::254/128 169.254.169.254/32 168.63.129.16/32 ::ffff:169.254.169.254/128
  • Response headers. Every page is served with a Content-Security-Policy that allows only Cryptgeon's own scripts, plus X-Frame-Options: DENY, X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer; API responses are not cacheable.
  • Valkey advisories. Valkey 7.2.13 comes from the Ubuntu 24.04 universe archive. Ubuntu publishes the fixes in USN-8771-1 (CVE-2026-56684 and CVE-2026-63639, both rated medium) only through Ubuntu Pro (ESM Apps). Neither is reachable on this image: CVE-2026-56684 needs Valkey's own TLS listener, which is never enabled here (tls-port 0; nginx terminates TLS for the web app), and CVE-2026-63639 needs a client that may run RESTORE, which the image's only Valkey account may not. Valkey is also reachable only from the instance itself, with the per-instance password. If you attach the instance to an Ubuntu Pro subscription, unattended upgrades install the fixed package automatically; otherwise it arrives with Ubuntu's next public Valkey update or the next cloudimg version of this AMI.
  • Upgrades. Ubuntu security updates (including nginx and Valkey) install automatically. Cryptgeon itself is a single binary; cloudimg publishes new versions of this AMI for new Cryptgeon releases.

Step 12: Licences and source

Cryptgeon is MIT licensed. The image ships its licence, the exact source reference and the build record:

cat /usr/share/doc/cloudimg-cryptgeon/SOURCE.txt
sed -n '1,4p' /usr/share/doc/cloudimg-cryptgeon/BUILD-INFO
cloudimg Cryptgeon 2.9.3 appliance - source

Cryptgeon (MIT licence, LICENSE-cryptgeon-MIT.txt, Copyright (c) 2021 Niccolo Borgioli):
  https://github.com/cupcakearmy/cryptgeon/tree/v2.9.3  (commit 40f35595337a6008afd41cd834b8eb4e8644f956)
  Built unmodified from that tag: the backend with Rust 1.99.1 against the dependency lock file Cargo.lock in this
  directory (semver-compatible security bumps only, listed in BUILD-INFO), the frontend with upstream's own
  pnpm-lock.yaml (unmodified). Build details: BUILD-INFO.
Valkey (BSD-3-Clause) is the Ubuntu 24.04 valkey-server package: /usr/share/doc/valkey-server/copyright.
Cryptgeon is a trademark of its respective owner; cloudimg is not affiliated with or endorsed by it.
Cryptgeon 2.9.3 - cloudimg build of the pinned upstream tag (no source patch)
source:      https://github.com/cupcakearmy/cryptgeon.git tag v2.9.3 commit 40f35595337a6008afd41cd834b8eb4e8644f956 (MIT, LICENSE sha256 5e0d5cf72b0db671448dae3b495ce60c46308e74459119fcd0fc9637b85f01a9)
toolchain:   rustc 1.99.1 (58cf8f9c3 2026-10-13) (rustup 1.29.2, profile minimal); node v24.22.2; pnpm 11.5.0
backend:     cargo build --release --locked (packages/backend), path prefixes remapped to /build

cloudimg builds the binary from the upstream tag, unmodified, with the current stable Rust toolchain, on a separate build host (the image never contains a compiler). The only change is the dependency lock file: eleven semver-compatible updates that fix every advisory cargo audit reports at the tag (zstd, zstd-safe and zstd-sys through async-compression and compression-codecs, tokio and anyhow). The web app is upstream's own build from its unmodified lock file. All product and company names are trademarks or registered trademarks of their respective holders; cloudimg is not affiliated with or endorsed by them.

Support

cloudimg supports this image: email support@cloudimg.co.uk. See the identity, secrets and PKI listing guide for the other versions in this listing.