How to Share Passwords and Secrets Safely with PrivateBin (Encrypted, Self-Destructing Links)

"What's the Wi-Fi password?" — texted, forever in a chat history, in a backup of that chat, on a lost phone. A client's new login, a router password for a colleague, an API key for a contractor: the same problem. PrivateBin is a paste site where the encryption happens in the browser — the link carries the key in the part after # that never reaches the server — with burn after reading and expiry. You paste, send the link, they open it once, it's gone. Your server stored ciphertext it couldn't decrypt.
Quick reference
| Task | What |
|---|---|
| Folder | ~/docker/privatebin/compose.yaml |
| Web | https://paste.example.com — must be HTTPS (crypto in the browser) |
| Share a secret | paste → Burn after reading → expiry 1 day → Send → copy the link |
| Password option | an extra passphrase sent separately (voice/SMS) for high-value secrets |
| Config | conf.php — default expiry, max size, burn as default |
| Data | ./data — encrypted blobs, auto-deleted on expiry |
1. The compose file
mkdir -p ~/docker/privatebin/{data,cfg} && cd ~/docker/privatebin
sudo chown -R 65534:82 data
nano compose.yaml
services:
privatebin:
image: privatebin/nginx-fpm-alpine:latest
container_name: privatebin
ports:
- "127.0.0.1:8099:8080"
volumes:
- ./data:/srv/data
- ./cfg/conf.php:/srv/cfg/conf.php:ro
read_only: true
restart: unless-stopped
nano cfg/conf.php
[main]
name = "Secure share"
discussion = false
opendiscussion = false
password = true
fileupload = true
burnafterreadingselected = true
defaultformatter = "plaintext"
sizelimit = 10485760
template = "bootstrap5"
languageselection = false
[expire]
default = "1day"
[expire_options]
5min = 300
10min = 600
1hour = 3600
1day = 86400
1week = 604800
[formatter_options]
plaintext = "Plain Text"
markdown = "Markdown"
[traffic]
limit = 10
[purge]
limit = 300
batchsize = 10
[model]
class = Filesystem
[model_options]
dir = PATH "data/"
Burn after reading on by default, one-day expiry, file uploads allowed (a config file, a certificate), discussions off.
docker compose up -d
2. HTTPS is mandatory
paste.example.com → 127.0.0.1:8099 (28) with a real certificate (27 or Cloudflare Tunnel, 109). The page's JavaScript does the encryption; without HTTPS anyone on the path could alter it. Public is fine and intended — the recipients are outside.
3. Share a password
Open https://paste.example.com, paste Wi-Fi: HomeNet / correct-horse-battery, tick Burn after reading (already ticked), expiry 1 day, Send. Copy the link: https://paste.example.com/?abc123#Gk3qvLw9… — the part after # is the key and is never sent to the server.
Send the link by whatever channel. They click; the browser downloads the ciphertext, decrypts with the key from the URL, shows the text, and tells the server to delete it. A second click shows "paste does not exist". If the link was intercepted and opened first, they see nothing and you know something's wrong — which is the point.
For the valuable ones, tick Password too and give that passphrase by phone: the link alone is then useless.
4. For clients, repeatably
A short link (191) go.example.com/secure → the PrivateBin page, and a saved reply in FreeScout (190): "I'll send your login via a secure one-time link; it opens once and expires in 24 hours — please save it to a password manager (43) when it opens." Clients learn the pattern; your support history never contains a password.
5. Alternatives in the same spirit
- Yopass — secrets only, with Redis; one-time, with a password/OTP split by design.
- Send (the Firefox Send fork) and Pingvin Share (193) — for files, encrypted, expiring.
- Vaultwarden Send (43) — if the household already uses it; "Send" creates exactly these links from inside the password manager.
6. Housekeeping
Expired pastes are purged on visits ([purge]). The ./data folder stays small. Nothing in it is readable without the link, so backups are unimportant — a lost server just means lost links, which is fine. Keep the image updated (50); it's internet-facing.
Troubleshooting
"Could not create paste" / write errors. ./data ownership (the image's nginx/php user: 65534:82 on the alpine image; check docker compose logs).
Page loads but nothing decrypts. Plain HTTP, or a proxy stripping/altering scripts; HTTPS with no HTML "optimisation" (Cloudflare's Rocket Loader off).
Links with # lose the key in some apps. Some messengers truncate at #; send as a plain-text message, not a preview card, or use the password option.
Rate limited. [traffic] limit — raise it on a busy day.
Wrapping up
One container behind HTTPS, burn-after-reading by default, a day's expiry. Secrets travel as a link that opens once in the recipient's browser, and the server in the middle — yours — only ever held ciphertext it couldn't read.
