1. The link as a security object ๐
I recently built psst-rs, a small secret-sharing service written in Rust.The goal is simple: paste a small secret, generate a link, send the link to someone, and let the secret be read only once.
A secret-sharing link is not a normal link. It does two things: It points to the encrypted secret and it carries the key needed to decrypt it.
In psst-rs, the server never sees the key: the secret is encrypted in the browser with AES-GCM. The server receives only the ciphertext and the nonce, the key remains in the URL after #.
Example:
This does not protect against a compromised browser, compromised JavaScript, or a leaked full link.
2. Threat model ๐
Before mapping controls to risks, I needed a small threat model to define what this service is supposed to protect, and what it is not supposed to solve.
For psst-rs, the main asset is not the database record itself. The database only stores encrypted material. The real asset is the ability to decrypt the secret before it expires or gets consumed. That ability comes from the combination of the secret identifier and the key contained in the URL fragment.
So the main security goal is simple: the server should be able to store and deliver an encrypted blob, but it should not be able to decrypt it.
There are a few realistic threats worth caring about.
- Someone could try to guess or scrape secret IDs.
- Someone could try to read the same secret more than once.
- Someone could abuse the service to create many secrets and fill the storage.
- Someone could bypass the reverse proxy and hit the origin directly.
- Operational data could also become a leakage path if secrets, keys, request bodies, or tokens end up in logs.
There are also things this model deliberately does not try to solve.
- It does not protect against a compromised browser.
- It does not protect against malware on the sender or recipient machine.
- It does not stop someone from forwarding the link after receiving it.
- It also still requires trust in the JavaScript delivered to the browser, because that is where encryption and decryption happen.
The goal is to make a small ephemeral sharing flow where the server-side trust boundary is as small as possible.
This leads to a simple security architecture.
3. Security architecture ๐
At a high level, the architecture looks like this: keep the key client-side, force public traffic through Cloudflare, keep administration separate, and store only encrypted material on the origin.
4. Controls mapped to risks ๐
Once the trust boundaries are clear, the controls become easier to reason about.
The point is not to say โthe service is secure because it is encryptedโ. Encryption is only one control. The more useful question is: which risks remain, and what reduces them?
Risk 1: someone discovers or scrapes secret IDs ๐
If the backend returned encrypted material based only on the secret ID, the ID would effectively become an access token.
The control here is to require a digest derived from the decryption key when reading a secret. The browser sends the digest, the backend checks it, and only then returns the ciphertext and nonce.
The server still does not know the key, but the ID alone is not enough to retrieve the encrypted payload.
Risk 2: the same secret is read multiple times ๐
A single-read secret should not rely on the frontend behaving correctly.
The control here is backend-enforced consumption. When a secret is read, the encrypted payload is returned and the database record is deleted as part of the same operation.
โSingle-readโ should be a server-side property, not a UI promise.
Risk 3: abuse and resource exhaustion ๐
Even if the service stores only encrypted material, it is still a public endpoint.
Someone could automate secret creation, submit large payloads, fill storage, or create secrets that are never read.
The controls here are Turnstile, application-level rate limiting, global quotas, size limits, TTLs, and periodic cleanup.
These controls are not about confidentiality. They are about keeping the service small, bounded, and operable.
Risk 4: direct origin exposure ๐
If Cloudflare is part of the security model, the origin should not become a second public entry point.
The control here is to force web traffic through Cloudflare, keep the application behind a reverse proxy, bind the application locally, and separate administration through Tailscale.
The goal is simple: reduce the number of ways into the host.
Risk 5: operational leakage ๐
A system can avoid storing the key and still leak sensitive data through logs, metrics, traces, reverse proxy configuration, or debugging output.
The control here is log hygiene.
The application should not log secrets, keys, request bodies, verification tokens, or decrypted content. Logs and metrics should describe system behavior, not the content being protected.
5. Limits ๐
This model has limits.
The server does not need to know the decryption key, but the model still depends on the browser, the delivered JavaScript, and the way the link is shared.
The security model is not based on one big control. It is a chain of small decisions: keep the key client-side, avoid treating the ID as sufficient authority, enforce single-read behavior on the backend, limit abuse, reduce origin exposure, and avoid leaking sensitive material through operations.