Skip to main content
Technical Blueprints

Cryptgeon: Self-Hosted Secret Sharing vs PrivNote

How I deploy Cryptgeon as a self-hosted secret sharing service: the Compose file, the TTL defaults I trust for client onboarding, and the proxy in front.

Published Updated 10 min read

I run Cryptgeon as the self-hosted secret sharing service that replaced “I’ll just send the password in this email” across every client onboarding I do. It’s a small, single-purpose tool: a Rust backend, a Svelte frontend, Redis for storage. It solves exactly one problem, which is getting a credential from me to a client without leaving a copy in either of our inboxes.

This post is the deployment I actually run for client work. The Compose file, the proxy configuration, the TTL defaults I’ve settled on after a couple of years in production, and the boring-but-important opinions about why agency workflows should stop treating email as a credential channel.

If you’re looking at PrivNote, PrivateBin, or 1Password’s secure-share feature and wondering whether a self-hosted Cryptgeon is worth the 30 minutes of setup, the answer is yes, provided you set the TTL defaults correctly and put it on a domain you actually trust.

Why self-hosted secret sharing is worth running

The agency security stat that’s bothered me for years: client onboarding emails are the single most common credential leak vector I see in incident postmortems. Not phishing, not breached databases, not zero-day exploits. The original sin is “here are your WordPress admin credentials, please change the password after first login,” sent to a client’s Gmail account, forwarded to two colleagues, archived forever.

The credentials are now in at least three inboxes you don’t control, indexed by Google, replicated to whatever desktop search tool the client uses, and probably backed up to iCloud. The “please change after first login” rider gets followed maybe 40% of the time, in my experience. The other 60% live with that initial password until the site gets hit by a bot net six months later.

Cryptgeon fixes this with a 30-second flow: paste the credential, set 1 view and 24 hours, send the link. The recipient opens it once, the note self-destructs, and Redis flushes the entry. Nothing in either inbox to be subpoenaed, breached, or forwarded. If the link is opened twice, you have a security incident on your hands, but you also have evidence that something went wrong, which is more than email gives you.

The audit trail (or lack of one) lives on infrastructure you control, the URL is on a domain you control, and you’re not paying per-seat for a feature your team uses three times a week.

How Cryptgeon’s encryption actually works

Worth a paragraph because this is the part that determines whether the threat model holds up.

Encryption happens client-side, in the browser, before the note leaves your machine. AES-256 in GCM mode, a 256-bit key generated locally. The ciphertext goes to the server, stored in Redis under a random 256-bit ID. The decryption key is appended to the URL as a fragment, the part after the # symbol, which browsers do not send to the server in HTTP requests.

What the server sees: the ID, the ciphertext, the TTL, the view counter. What the server does not see: the key, ever. When the recipient opens the URL, the browser pulls the ciphertext from the server using the ID, then decrypts it locally using the key from the URL fragment.

A full server breach gives the attacker a pile of ciphertext blobs and no way to read them. End-to-end encryption protects the server side, not the transport side. If the link is intercepted in transit (TLS broken, malicious browser extension, compromised email account), the interceptor has everything they need. The 1-view rule is what protects against transport-layer compromise. If the legitimate recipient can’t open their link, you know it was intercepted.

Notes and files never touch disk. Redis holds them in memory until the TTL expires or the view counter hits zero. A server reboot wipes every pending note, which is technically a feature, because there’s no persistent state to subpoena, image, or accidentally back up to S3.

The Cryptgeon deployment stack

Here’s what runs on my agency VPS. Two containers, one network, behind Nginx Proxy Manager for TLS.

Prerequisites

A small VPS. A 1GB Hetzner CX21 or equivalent is plenty for an agency-sized team. Cryptgeon and Redis together use about 80MB of RAM at idle. The bottleneck is the proxy, not Cryptgeon itself.

A hardened server before anything else. SSH keys, no root login, UFW with deny-by-default, automatic security updates. My Linux server security fundamentals post covers the baseline I run on every fresh box. Cryptgeon stores nothing persistent, but the proxy in front of it does, and an unhardened server is a bad starting point for anything client-facing.

A domain you control. This is one of the few self-hosted tools that has to live on a public URL, because the whole point is sharing a link with someone outside your network. Pick a domain that signals legitimacy to your recipients. secrets.youragency.com reads better than notes.random-vps-provider.cloud to a non-technical client opening their first one-time link from you.

DNS access for Let’s Encrypt, ideally with a provider that supports API-driven challenges. Cloudflare is the pragmatic default.

Docker engine

Install Docker from the official Docker repository. Follow the official Docker installation guide for Ubuntu. Don’t install from apt’s default repo or from a one-line curl | sh script. The official repository is the only correct source.

Cryptgeon and Redis

The whole stack is two services in one Compose file. Cryptgeon needs Redis; nothing else needs Cryptgeon.

version: '3.8'
services:
  redis:
    image: redis:alpine
    restart: always

  app:
    image: cupcakearmy/cryptgeon:latest
    depends_on:
      - redis
    environment:
      SIZE_LIMIT: 100 MiB
      THEME_IMAGE: "https://yourdomain.com/your-logo.svg"
    ports:
      - 6880:8000
    restart: always
    labels:
      - "com.centurylinklabs.watchtower.enable=true"

The two values worth tuning:

  • SIZE_LIMIT defaults to 4 MiB. I bump it to 100 MiB so I can share things like exported password vaults, signed contracts, or client SSH keys. If you only ever share text notes, leave it at the default.
  • THEME_IMAGE is the logo that shows on the create-note page. I host my logo on the same agency CDN I use for everything else. Whatever URL you put here gets fetched by the recipient’s browser, so use HTTPS and a domain you control.

Bring it up:

docker compose -f /path/to/cryptgeon.yml up -d

Cryptgeon listens on port 8000 inside the container, mapped to 6880 on the host. In Nginx Proxy Manager, add a Proxy Host pointing secrets.yourdomain.com to http://app:8000 (assuming you put both stacks on the same Docker network), request a Let’s Encrypt certificate, and tick “Force SSL” plus “HTTP/2 Support”. On the Advanced tab, add the standard security headers: HSTS, X-Frame-Options DENY, a strict CSP. My Nginx security hardening post covers the exact set I use.

That’s the entire deployment. There’s no admin account, no database to back up, no first-run wizard. Cryptgeon is genuinely a single-purpose tool and it shows in the install footprint.

TTL defaults I’ve settled on

The configuration that actually matters lives in the create-note dialog, not in the Compose file. After a couple of years of using Cryptgeon for client work, the defaults I’ve settled on:

For credentials going to a client (WordPress admin, hosting panel, SFTP): 1 view, 24-hour expiry. The 1-view rule is the canary. If the recipient can’t open the link, I know it was intercepted, and I rotate the credential before re-sending. The 24-hour cap is a backstop for the case where the recipient forgets to open the link at all. Notes shouldn’t sit in Redis indefinitely just because nobody got around to reading them.

For internal team secrets (a shared API key for a one-off task): 5 views, 4-hour expiry. The team is on the same Slack channel, so transit-layer compromise is less of a concern than for external recipients, and somebody is going to want to copy-paste it more than once.

For files (signed contracts, exported backups): 1 view, 1 hour. Files are typically time-sensitive and contain more sensitive payloads than text notes. The shorter window is appropriate.

For nothing, ever: unlimited views or TTLs over 7 days. If a secret needs to live longer than a week, it doesn’t belong in Cryptgeon. It belongs in a password manager with proper access controls and an audit log.

What you give up versus PrivNote or 1Password

Cryptgeon doesn’t have everything the SaaS one-time-link tools have. There’s no audit log of who opened a link from where; the view counter ticks down, but Cryptgeon won’t tell you it was opened from a Chrome browser in Berlin at 14:32. There’s no per-recipient access control; the link is the credential, and anyone who has it can open it once. There’s no compliance certification, so if you need SOC 2 or ISO 27001 attestations on every tool in the credential chain, the self-hosted instance is your responsibility to certify.

For my agency these trade-offs are fine. The threat I’m actually defending against is “client’s email account gets breached and the WordPress password from three months ago is in the inbox.” Cryptgeon eliminates that risk entirely, and the trade-offs above are not in the threat model. If yours are different, this might not be the right tool.

Verifying the deployment before you trust it

Run this sanity check before you send the first real secret through it.

  1. Create a test note with one view and a 24-hour TTL. Copy the link.
  2. Open the link in a fresh incognito window. Confirm the note shows.
  3. Refresh or open the same link in a second incognito window. Confirm you get a “note not found” message, not the original content. This is the test that the view counter actually works.
  4. Inspect the network tab during the create flow. The request body should contain a data field of base64 ciphertext, not the plaintext you typed. That’s the AES-GCM payload, which confirms client-side encryption is working.
  5. Confirm the URL fragment (after the #) is not sent in any request the browser makes to the Cryptgeon API.

Step 4 and 5 are the ones that matter. Cryptgeon’s whole security argument hinges on those two behaviours. If either is wrong, do not put real credentials through the instance until you’ve fixed it.

What pairs well with Cryptgeon

A few tools I run alongside this in an agency stack. A self-hosted password manager for the long-term credential store, paired with 2FAuth for the 2FA side. Cryptgeon is for transit only; the credentials themselves live in the password manager and get exported to a one-time link when they need to move. Authentik sits in front of the create-note page once you scale beyond a one-person agency, leaving the open-link page public. And a VPN for the rest of the admin layer, because Cryptgeon has to be public but everything else doesn’t; I use Wireguard Easy for solo work and Mistborn when the team grows.

Closing the loop

Cryptgeon has been my agency’s secret sharing service for the past two-plus years. The setup time is 30 minutes including the proxy, the recurring cost is the rounding error on a shared VPS, and the operational burden is approximately zero. There’s no database to back up, no upgrade migrations to plan, no user accounts to manage.

The actual benefit is harder to measure but I’ve felt it. Client credentials no longer sit indefinitely in inboxes I can’t audit. When a credential needs to move, it moves through a channel I control, with a TTL, on a domain that signals to the recipient that I take this seriously. The companion read for the people side of all this is the human element in cybersecurity defense post. Most leaks are workflow problems, and a one-time-link tool that’s faster than email is one fewer reason for the workflow to fail.

Watch on YouTube

Video walkthrough

Prefer the screen-recording version of this guide? Watch it on YouTube — opens in a new tab so the player only loads when you ask for it.

Frequently Asked Questions

Want this handled, not just understood?

Reading the playbook is one thing. Running it on production at 2am is another. If you'd rather have me run it for you, the door is open.

Apply for Access