Go + One = Gone — a tiny service for sharing a secret exactly once.
Gone lets you paste a sensitive value (password, token, wifi key), generate a one‑time link, and send that link. The first person to open it sees the secret; after that it’s gone for good.
Run with Docker:
docker run --rm -p 8080:8080 ghcr.io/haukened/gone:latestVisit http://localhost:8080, paste a secret, pick an expiry, copy the generated link, send it. The recipient opens the link, the secret displays once, and the server deletes it immediately.
Want metrics? (optional)
docker run --rm \
-p 8080:8080 -p 9090:9090 \
-e GONE_METRICS_ADDR=0.0.0.0:9090 \
-e GONE_METRICS_TOKEN=tok \
ghcr.io/haukened/gone:latestFetch metrics snapshot:
curl -H 'Authorization: Bearer tok' http://localhost:9090/- You type a secret in the web form and choose how long it should live (its TTL).
- Your browser encrypts it locally before it ever leaves your machine.
- The server stores only the encrypted blob plus when it should expire.
- You get a link like:
https://example/secret/abcd#v1:ENC_KEY_MATERIAL. - You send that full URL (including everything after the
#) to someone. - When they open it, the server gives their browser the encrypted blob once, deletes it, and the browser decrypts it locally using the part after
#. - A refresh or second visit won’t work—the secret is already gone.
Guarantees (simple terms):
- Server never learns the plaintext.
- Link works only one time.
- Expired or used links are dead.
Environment variables only (no flags, no config files):
| Variable | Description | Default |
|---|---|---|
GONE_ADDR |
Listen address (host:port or :port). |
:8080 |
GONE_DATA_DIR |
Data directory (SQLite DB + blobs). | /data |
GONE_INLINE_MAX_BYTES |
Max ciphertext size stored inline in SQLite. | 8192 |
GONE_MAX_BYTES |
Absolute max secret size (bytes). | 1048576 |
GONE_TTL_OPTIONS |
Comma list of selectable TTLs. | 5m,30m,1h,2h,4h,8h,24h |
GONE_METRICS_ADDR |
Optional metrics listener address. | (empty) |
GONE_METRICS_TOKEN |
Optional bearer token required for metrics. | (empty) |
Derived automatically:
- MinTTL / MaxTTL = smallest / largest in
GONE_TTL_OPTIONS(accepted range is any duration inside that span, not just the listed ones). - SQLite DSN →
<GONE_DATA_DIR>/gone.db(WAL mode, FULL sync enforced).
TTL Format: comma‑separated Go durations using s, m, h (e.g. 30s,5m,90m,2h).
Disabled unless GONE_METRICS_ADDR is set. If GONE_METRICS_TOKEN is non‑empty you must supply Authorization: Bearer <token>.
JSON snapshot example:
{
"counters": {
"secrets_created_total": 123,
"secrets_consumed_total": 118,
"secrets_expired_deleted_total": 47
},
"summaries": {
"janitor_deleted_per_cycle": {"count": 42, "sum": 420, "min": 1, "max": 25}
}
}Definitions:
| Name | Type | Meaning |
|---|---|---|
secrets_created_total |
counter | Secrets stored |
secrets_consumed_total |
counter | Secrets consumed & deleted |
secrets_expired_deleted_total |
counter | Expired secrets janitor removed |
janitor_deleted_per_cycle |
summary | Distribution of expirations per janitor run |
Persistence notes:
- In‑memory metrics flushed periodically to SQLite; snapshot merges persisted + current deltas.
- Graceful stop attempts a final flush.
Enable + fetch quickly:
GONE_METRICS_ADDR=127.0.0.1:9090 GONE_METRICS_TOKEN=tok \
go run ./cmd/gone &
curl -H 'Authorization: Bearer tok' http://127.0.0.1:9090/OpenAPI file: docs/openapi.yaml (enumerates every emitted status code).
Importable into Postman / Insomnia / ReDoc.
Note
Everything below this line is advanced detail for operators and developers. Read on if you're curious, but most people can stop here.
This project uses Task (Taskfile.yml) to coordinate building the binary and (for production) minifying and embedding static assets.
Please first install Task: https://taskfile.dev/docs/installation
Core tasks:
| Task | What it does |
|---|---|
task dev |
Clean + build development binary (no minified assets, no -tags=prod). |
task prod |
Full production build: clean, minify assets into web/dist, build with -tags=prod. |
task run |
Convenience: rebuild dev binary and run with a temporary data dir. |
task cover |
Run tests with coverage output. |
Development build:
task dev
./bin/goneProduction build (minified assets embedded):
task prod
./bin/goneRun with overrides (development example):
GONE_ADDR=127.0.0.1:8080 \
GONE_DATA_DIR=$(pwd)/data \
GONE_TTL_OPTIONS="5m,30m,1h" \
GONE_MAX_BYTES=$((1024*1024)) \
GONE_METRICS_ADDR=127.0.0.1:9090 \
GONE_METRICS_TOKEN=localtok \
./bin/goneOr just:
task run- Metadata (IDs, expiry, consumed state) → SQLite (WAL, FULL sync).
- Ciphertext: inline if ≤
GONE_INLINE_MAX_BYTES; otherwise filesystem blob underblobs/in data dir. - Expirations cleared by janitor + immediate deletion on consume.
This section is intentionally lower in the file—most users can stop above.
- Browser creates random AES‑GCM key + nonce (Web Crypto API).
- Encrypts plaintext with AAD
gone:v1. - Sends ciphertext + nonce (
X-Gone-Nonce) + version (X-Gone-Version). Key never leaves browser. - Server stores ciphertext + metadata only.
- Response returns secret ID + expiry.
- Share link:
https://host/secret/{id}#v1:<base64url-key>. - First GET streams ciphertext and deletes record atomically.
- Browser decrypts locally; reload fails (already deleted).
Properties:
- Compromise yields only ciphertext & nonces.
- Must possess both path ID and fragment key.
- Atomic consume prevents replay.
- AES‑GCM integrity + fixed AAD protect against tamper.
Defended:
- TLS transport assumed.
- No server knowledge of keys / plaintext.
- Atomic single consumption.
- Timely expiry deletion.
Out of Scope (current):
- Malicious browser extensions.
- Brute force ID enumeration (future: lightweight rate limiting).
- Sophisticated timing side channels.
- URL hygiene / accidental fragment leakage.
- Large scale DoS floods.
Operational Tips:
- Keep TTLs short for higher sensitivity.
- Restrict metrics listener to loopback or secured network.
- Backups should exclude transient expired blobs (or run quiescent snapshot).
- Optional separate KMS‑sealed metadata.
- Link burn confirmation UX.
- Structured audit events (without sensitive payload) to external sink.
Middleware sets:
Cache-Control: no-storeReferrer-Policy: no-referrerX-Content-Type-Options: nosniffContent-Security-Policy: default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'
Notes:
- CSP blocks inline code; only same‑origin static assets permitted (images allow data URIs).
frame-ancestors 'none'removes need for X-Frame-Options.- Dynamic pages: forced no‑store; static assets may be cached briefly.
Enable client timing logs either:
- Append
?debug=timingto a page URL, or - DevTools:
localStorage.setItem('goneDebugTiming','1')then refresh.
Disable by removing parameter & clearing the key.
- Rate limiting / abuse guard
- Optional Prometheus exposition
- CSP tightening & documentation
- Graceful shutdown coordination improvements
GNU Affero General Public License v3.0 – see LICENSE.