Safe Secrets - safe(r) and easy way to transfer sensitive data

The primary use case is sharing sensitive data securely - messages that self-destruct, can only be accessed once, and are protected by an easy-to-share PIN code. I got tired of the usual "security" approach of sending username and password in two separate emails - that's just a joke. So I built this as a simple and better alternative to passing passwords around.
Quick Start
# run with docker
docker run -p 8080:8080 -e SIGN_KEY=your-random-secret-key -e DOMAIN=localhost -e PROTOCOL=http umputun/secrets
# or build and run locally
cd app && go build -o secrets && ./secrets -k "your-secret-key" -d localhost -p http
Then open http://localhost:8080 in your browser.
How It Works
- Enter your secret message (or upload a file)
- Set expiration time and a PIN
- Get a one-time link to share
Your recipient opens the link, enters the PIN, and sees the message. That's it. The message is deleted immediately after reading, and wrong PIN attempts are limited (default: 3 tries).
Try it live: safesecret.info - feel free to use it if you're crazy enough to trust me, or run your own instance.
Screenshots (click to expand)
Desktop View

Dark Mode

Mobile View

PIN Entry

Decoded Message

How Safe Is This Thing
- Messages are encrypted with your PIN (which is also hashed) - the original is never stored
- Nothing sensitive in logs
- Nothing on disk by default (in-memory storage)
- Messages are permanently destroyed after reading or expiration
- An attacker would need both the link AND the PIN to access anything
Feel free to suggest any other ways to make the process safer.
Installation
Docker (Recommended)
Simple setup:
docker run -p 8080:8080 \
-e SIGN_KEY=your-long-random-secret \
-e DOMAIN=example.com \
-e PROTOCOL=https \
umputun/secrets
Production setup with docker-compose:
- Download
docker-compose.yml from this repo
- Configure environment variables (see Configuration section below)
- Run
docker-compose up -d
For SSL termination, put a reverse proxy in front (e.g., reproxy, nginx, or traefik).
See docker-compose.yml for a complete example.
Binary
Download from releases or build from source:
cd app && go build -o secrets
./secrets -k "your-secret-key" -d "example.com" -p https
Configuration
All options work as both CLI flags and environment variables. The app listens on port 8080 by default.
Core Options
| Flag |
Env Variable |
Default |
Description |
-k, --key |
SIGN_KEY |
required |
Signing key for encryption |
-d, --domain |
DOMAIN |
required |
Site domain(s), comma-separated for multiple |
-p, --protocol |
PROTOCOL |
https |
Site protocol (http/https) |
--branding |
BRANDING |
Safe Secrets |
Application title |
--dbg |
- |
false |
Enable debug mode |
Message Settings
| Flag |
Env Variable |
Default |
Description |
--pinsize |
PIN_SIZE |
5 |
PIN length in characters |
--expire |
MAX_EXPIRE |
24h |
Maximum message lifetime |
--pinattempts |
PIN_ATTEMPTS |
3 |
Max wrong PIN attempts |
Storage
| Flag |
Env Variable |
Default |
Description |
-e, --engine |
ENGINE |
MEMORY |
Storage engine: MEMORY or BOLT |
--bolt |
BOLT_FILE |
/tmp/secrets.bd |
BoltDB file path |
File Uploads
Share files securely - they're encrypted with your PIN just like text messages and self-destruct after download. The filename is preserved but stored encrypted.
| Flag |
Env Variable |
Default |
Description |
--files.enabled |
FILES_ENABLED |
false |
Enable file uploads |
--files.max-size |
FILES_MAX_SIZE |
1048576 |
Max file size in bytes (1MB) |
Authentication
Optional password protection for creating secrets. When enabled, users must log in before they can generate links. Viewing/consuming secrets remains anonymous - no login required to open a link with the correct PIN.
| Flag |
Env Variable |
Default |
Description |
--auth.hash |
AUTH_HASH |
disabled |
bcrypt hash to enable auth |
--auth.session-ttl |
AUTH_SESSION_TTL |
168h |
Session lifetime (7 days) |
Generate a bcrypt hash:
# htpasswd (Apache utils)
htpasswd -bnBC 10 "" yourpassword | tr -d ':'
# mkpasswd (apt install whois)
mkpasswd -m bcrypt yourpassword
# Docker
docker run --rm caddy caddy hash-password --plaintext yourpassword
Web UI: Login popup appears when creating a secret. Sessions stored in cookies.
API: Requires HTTP Basic Auth with username secrets and your password.
Examples
# basic usage
./secrets -k "secret-key" -d "example.com"
# multiple domains
./secrets -k "secret-key" -d "example.com,alt.example.com"
# persistent storage
./secrets -k "secret-key" -d "example.com" -e BOLT --bolt=/data/secrets.db
# with file uploads (5MB limit)
./secrets -k "secret-key" -d "example.com" --files.enabled --files.max-size=5242880
# with authentication
./secrets -k "secret-key" -d "example.com" --auth.hash='$2a$10$...'
Architecture
Safesecret is a single binary with embedded web UI. Typical production setup:
secrets container - handles API and serves the web interface (port 8080)
- reverse proxy - SSL termination (reproxy, nginx, traefik, etc.)
The app works fine without a proxy for development or if you're running behind a load balancer (AWS ALB, haproxy, etc.).
Integrations
API
Simple REST API for programmatic access.
Health Check
GET /ping
$ curl https://safesecret.info/ping
pong
Create Secret
POST /api/v1/message
Body: {"message": "secret text", "exp": 3600, "pin": "12345"}
exp - expiration in seconds
pin - PIN code (must match configured length)
- Requires Basic Auth when authentication is enabled (user:
secrets)
$ curl -X POST https://safesecret.info/api/v1/message \
-H "Content-Type: application/json" \
-d '{"message": "my secret", "exp": 3600, "pin": "12345"}'
{
"exp": "2024-01-15T10:30:00Z",
"key": "f1acfe04-277f-4016-518d-16c312ab84b5"
}
Retrieve Secret
GET /api/v1/message/:key/:pin
$ curl https://safesecret.info/api/v1/message/f1acfe04-277f-4016-518d-16c312ab84b5/12345
{
"key": "f1acfe04-277f-4016-518d-16c312ab84b5",
"message": "my secret"
}
Get Configuration
GET /api/v1/params
$ curl https://safesecret.info/api/v1/params
{
"max_exp_sec": 86400,
"max_pin_attempts": 3,
"pin_size": 5,
"files_enabled": true,
"max_file_size": 1048576
}