A tiny self-hosted "burn after reading" secret-drop server. Paste a secret, get a one-time URL, send the URL via Teams / Signal / email — the secret is destroyed on first read (or after TTL).
- Zero-knowledge. Encryption runs in the browser; the server only ever stores ciphertext. A memory dump of the server reveals nothing decryptable.
- No disk. Secrets live in
sync.Map. Restarting the container wipes everything — by design. - Opt-in gating. Optional passphrase, configurable TTL + view count, IP/geo allowlist, self-share passkey mode.
go run ./cmd/goash
# open http://localhost:8080Built and run inside Docker:
docker build -t goash .
docker run --rm -p 8080:8080 -e GOASH_BASE_URL=http://localhost:8080 goashAll knobs are environment variables. Sensible defaults make the bare command work.
| Variable | Default | Purpose |
|---|---|---|
GOASH_LISTEN |
127.0.0.1:8080 |
Bind address |
GOASH_BASE_URL |
http://localhost:8080 |
Externally-visible URL — appears in share links and is the WebAuthn origin |
GOASH_RP_ID |
hostname of GOASH_BASE_URL |
WebAuthn relying-party ID override |
GOASH_TRUSTED_PROXY_CIDRS |
(empty) | When set, X-Forwarded-For is honored for peers in these ranges |
GOASH_GEOIP_PATH |
(empty) | Path to a MaxMind GeoLite2-Country .mmdb — enables the country-code allowlist |
GOASH_GEOIP_LICENSE_KEY |
(empty) | MaxMind account license key. When set, goash fetches and refreshes the mmdb automatically |
GOASH_GEOIP_EDITION |
GeoLite2-Country |
MaxMind edition to fetch |
GOASH_GEOIP_REFRESH_INTERVAL |
24h |
How often the background updater refreshes the mmdb |
GOASH_MAX_SECRETS |
10000 |
Hard cap on live in-memory secrets; new creates return 503 when full |
GOASH_MAX_WEBAUTHN_SESSIONS |
1000 |
Hard cap on in-flight WebAuthn ceremonies |
GOASH_MAX_SECRET_BYTES |
65536 |
Cap on ciphertext size |
GOASH_RATE_LIMIT_PER_MIN |
30 |
Token-bucket cap on /api/secrets |
The server is the wrong place to attack. The threats it does defend against:
- Server compromise reveals plaintext. It doesn't — every secret is AES-GCM-encrypted in the browser. In passphrase mode, the key is derived from the passphrase via PBKDF2 (600k iterations, two independent salts: one for the AES key, one for the server-side auth check). In the no-passphrase mode, the key is randomly generated and embedded in the URL fragment, which never reaches the server.
- Brute-forcing the passphrase online. The secret burns after a configurable number of wrong attempts (default 3).
- Probing for live secret IDs. UUIDv4 IDs (~122 bits of entropy) plus uniform 404 responses for missing, expired, off-network, and otherwise-blocked secrets.
The server cannot defend against:
- The recipient screenshotting / copying the revealed plaintext.
- A network observer between the client and the server when not using TLS. Always run behind Caddy / Traefik / a similar reverse proxy in production.
goash binds to 127.0.0.1 by default. Front it with Caddy:
secret.example.com {
reverse_proxy 127.0.0.1:8080
}If your reverse proxy adds X-Forwarded-For, set GOASH_TRUSTED_PROXY_CIDRS to its IP/CIDR so the IP allowlist sees real client addresses.
Country-code allowlists need a MaxMind GeoLite2-Country mmdb. Two ways to provision it:
Self-managed. Download the mmdb (free, requires a MaxMind account) and mount it into the container:
docker run --rm -p 8080:8080 \
-e GOASH_GEOIP_PATH=/data/GeoLite2-Country.mmdb \
-v /host/path/GeoLite2-Country.mmdb:/data/GeoLite2-Country.mmdb:ro \
goashAuto-update. Set GOASH_GEOIP_LICENSE_KEY and goash will fetch the latest mmdb on startup (if missing or older than the refresh interval) and refresh it on a background loop:
docker run --rm -p 8080:8080 \
-e GOASH_GEOIP_PATH=/data/GeoLite2-Country.mmdb \
-e GOASH_GEOIP_LICENSE_KEY=xxxxxxxxxxxx \
-e GOASH_GEOIP_REFRESH_INTERVAL=24h \
-v geoip-data:/data \
goashThe updater downloads the official tar.gz from download.maxmind.com, extracts the .mmdb, and atomically renames it into place. The running *ipcheck.Geo is hot-swapped behind a sync.RWMutex — no restart needed.
This mode is for ferrying secrets between your own devices (laptop → homelab) using a synced passkey provider (iCloud Keychain, Google Password Manager, 1Password, Bitwarden).
- On device A, create a secret and tick "Lock with passkey (self-share)". The browser registers a new passkey, which the synced provider replicates to your other devices automatically.
- Send the URL to device B by any means.
- On device B, open the URL and authenticate with the same synced passkey.
If your passkey isn't synced, you won't be able to read your own secret from a different device. This mode is not for sharing with other people — pre-coordinating their public key is incompatible with the "drop a URL in Teams" flow. Use a passphrase instead.
WebAuthn requires the relying-party ID to be a registrable domain, which is why GOASH_BASE_URL defaults to http://localhost:8080 rather than the bind address.
Ready-to-use compose files and reverse proxy configs live in deploy/. Each subdirectory is a known-working, validated stack:
| Proxy | Config | Notes |
|---|---|---|
| Caddy | Caddyfile |
Automatic TLS — just set your domain |
| Traefik | traefik.yml |
Let's Encrypt resolver block included (commented) |
| Nginx | nginx.conf |
TLS cert mount points commented in |
| HAProxy | haproxy.cfg |
PEM bundle mount commented in |
| Apache httpd | httpd.conf |
Full standalone config with all LoadModule directives |
For bare-metal installs, see deploy/goash.service (systemd) and deploy/goash.env (annotated environment template).
cmd/goash/ entry point
internal/
secret/ Secret struct, atomic claim/burn
store/ sync.Map + TTL timers
handler/ HTTP routes, middleware, page rendering
ipcheck/ CIDR + geo allowlist evaluation
webauthn/ thin wrapper over go-webauthn for self-share mode
web/
templates/ html/template files (embedded)
static/ crypto.js, passkey.js, styles.css (embedded)
deploy/
caddy/ Caddy compose + Caddyfile
traefik/ Traefik v3 compose + static config
nginx/ Nginx compose + nginx.conf
haproxy/ HAProxy compose + haproxy.cfg
apache/ Apache httpd compose + httpd.conf
goash.service systemd unit (bare-metal)
goash.env environment variable template
go test ./...Unit tests cover the store, secret, ipcheck, and the HTTP handler surface (including the IP gate via httptest.NewRequest + manual RemoteAddr). The WebAuthn ceremony is exercised by manual cross-device test — see the verification section of plans/1-the-burn-after-fuzzy-harp.md.