A one-time secret is a link that reveals its contents once and then destroys them. You paste a credential into a service, get back a URL, send that URL to the person who needs it, and after they open it the secret no longer exists. It solves a narrow, real problem: credentials pasted into chat and email stay there, searchable, forever.
This is a small tool with an outsized effect on how much credential material is sitting in your organisation’s message history. It is also frequently misunderstood as a secrets management solution, which it is not. Both halves are worth being clear about.
Table of contents
- The problem it actually solves
- How it works, and what zero-knowledge means
- Using it well
- Self-hosting it
- What this does not solve
- A workable policy
- How this fits the rest of the stack
- FAQ
The problem it actually solves
Someone needs a database password. You paste it into Slack. That message is now:
- In Slack’s message history, searchable by everyone in the channel forever
- In Slack’s backups and, on paid plans, in the export archive
- In desktop and mobile notification history on every device
- Possibly in a third-party integration that indexes messages
- Retained under whatever retention policy your workspace has, which is often forever
Deleting the message does not reliably undo any of that. The same applies to email, tickets, and shared documents — a password in a Jira comment is in the ticket, the audit log, and every notification email it generated.
The core insight is that chat is a permanent record and credentials are supposed to be ephemeral. A one-time link puts a credential in a channel designed to forget, and leaves only a dead URL behind in the permanent one.
How it works, and what zero-knowledge means
- You enter the secret. A good implementation encrypts it in your browser before sending anything.
- The server stores only ciphertext, keyed by a random identifier.
- You get a URL containing that identifier, with the decryption key in the fragment — the part after
#. - The recipient opens it. The browser sends the identifier but never sends the fragment, so the server retrieves ciphertext it cannot read and the browser decrypts locally.
- The record is deleted. A second visit finds nothing.
https://example.com/secret/a1b2c3d4e5f6#dGhpcyBpcyB0aGUga2V5
└─ sent to server ─┘└─ never sent ─┘
The fragment behaviour is the whole design. Browsers do not transmit anything after # in an HTTP request, so a properly-built service holds data it is technically unable to decrypt. That is what zero knowledge means here, and it is worth verifying rather than assuming — plenty of services encrypt server-side, which means they hold the key.
A useful consequence: because the key travels in the URL, splitting the link across two channels is genuinely effective. Send the URL by email and the passphrase by text, and neither channel alone is sufficient.
Using it well
- Set the shortest expiry that works. An hour for someone at their desk; a day if they are in another timezone. Never the maximum by default.
- Add a passphrase for anything sensitive, communicated separately. This is the single biggest improvement to the basic flow.
- Send the link and the passphrase over different channels. Link in email, passphrase in a text message.
- Tell the recipient it is one-time. People forward links, and the second person gets nothing while the first has already consumed it.
- Use the burn feature if you created a link that was never opened and no longer should be.
- Watch for link preview bots. This is the sharp edge — see below.
The unfurl problem is real and catches people. Slack, Teams, and some mail clients fetch URLs to generate previews. If that fetch consumes the secret, your recipient opens the link and finds nothing — and worse, the content may have been retrieved by the preview service.
- Wrap the URL in angle brackets in Slack (
<https://...>) to suppress unfurling, or disable previews for the domain. - Prefer services with an interstitial click to reveal page, which bots do not click through.
- If a recipient reports an already-viewed link, assume a bot consumed it, and rotate the credential rather than just resending.
Self-hosting it
For organisations that would rather not route credentials through a third party at all, the main implementations are open source and small.
The requirements are modest: a web service, a data store with TTL support, TLS, and a domain. The one architectural decision worth making deliberately is the storage backend, since anything with an expiry mechanism saves you writing a cleanup job.
# Typical shape of the configuration
SECRET_MAX_TTL=604800 # 7 days, in seconds
SECRET_DEFAULT_TTL=3600 # 1 hour
SECRET_MAX_LENGTH=65536
ENCRYPTION_KEY=<from the environment, never from a file in the repo>
DATABASE_URL=<managed database connection string>
Do not put the encryption key in the repository or the image. It belongs in the service’s environment variables, injected at runtime — the services documentation covers where those live on RunxBuild, alongside a managed Postgres for storage.
Two operational points that get missed. Expiry must be enforced server-side, not just presented in the UI — a record whose TTL passed should be unreadable regardless of how it is requested. And log the fact of a retrieval without logging the secret: knowing a link was consumed at 14:32 from one IP is useful; the payload in your log file defeats the entire purpose.
What this does not solve
This is the part that matters most, because one-time links get used as a substitute for credential management and they are not one.
- It is a transport, not a store. The recipient still has to put the credential somewhere. If that somewhere is a text file on their desktop, you have moved the problem, not solved it.
- It does not rotate anything. A password shared once is still valid until someone changes it.
- It does not scope access. A full-privilege database password sent securely is still a full-privilege database password.
- It has no audit trail worth the name. You know a link was opened. You do not know by whom.
- It does not help applications. Services need credentials at runtime, continuously, not once.
For applications, the answer is environment variables injected by the platform, or a secrets manager — never a shared link and never the repository. A credential in a Git repository is in every clone, every fork, and the entire history, and removing it later does not remove it from anyone’s local copy. The only real remedy is rotation.
For humans, a team password manager is the actual solution: shared vaults, per-item access, audit logs, and rotation reminders. One-time links complement that — they are how you get a credential to a contractor who is not in your vault, or hand something to a client — rather than replacing it.
A workable policy
- Team password manager as the system of record for anything humans need. Shared vaults by team, not one vault for everyone.
- Platform environment variables for application secrets. Never in the repository, never in the image, never in a config file that happens to be gitignored.
- One-time links for the exceptions — external parties, a temporary credential, a one-off handover.
- Rotate on departure and on exposure, automatically where you can. Assume any credential that ever appeared in chat is compromised.
- Secret scanning in CI to catch commits before they land. This is cheap and catches the most expensive mistake.
- A short, written rule everyone knows: never paste a credential into chat, email, or a ticket. One sentence, and it prevents most of the problem.
That last item does more than any tool. The reason credentials end up in Slack is not that people do not care — it is that it is the fastest option in the moment. Making the secure path nearly as fast is what changes behaviour, which is why the one-time link matters: it takes about twenty seconds.
How this fits the rest of the stack
One-time secret links are a small tool that fixes a specific bad habit: credentials living forever in chat history. Use short expiries, add a passphrase sent over a different channel, and watch for preview bots consuming links before the recipient does. Then remember it is transport only — a password manager for people, platform environment variables for applications. If you are pricing a small self-hosted service with a managed database behind it, the RunxBuild hosting calculator shows both as separate line items.
Useful related references:
- Heroku Env Variables: The Config Vars, the .env Mismatch, the Secret Manager, and the Migration to Modern Platforms
- Docker Secret: The Runtime Pattern, the Build-Time Pattern, and the One Mistake That Ships Them to the Image
- Linux View Environment Variables: The Five Commands, the .bashrc, the systemd Unit, and the One Mistake That Leaks the Secret to ps
- Services on RunxBuild
FAQ
What is a one-time secret link?
A URL that reveals its contents once and then destroys them. It lets you pass a credential to someone without leaving a permanent copy in chat history, email, or a ticket.
Are one-time secret services actually secure?
The well-built ones encrypt in the browser and put the decryption key in the URL fragment, which browsers never transmit — so the server holds data it cannot read. Verify that a service works this way rather than assuming it, since server-side encryption means they hold the key.
Why was my secret already viewed before the recipient opened it?
A link preview bot in Slack, Teams, or a mail client fetched the URL and consumed it. Wrap the link in angle brackets to suppress unfurling, and rotate the credential, since the preview service retrieved the content.
Should I use one-time links for application secrets?
No. Applications need credentials continuously at runtime. Use environment variables injected by your platform or a secrets manager. One-time links are for handing something to a person once.
Does a one-time link replace a password manager?
No. It is transport, not storage — the recipient still has to keep the credential somewhere. Use a team password manager as the system of record and one-time links for external parties and handovers.