Skip to main content

Envelope encryption

Every secret value is encrypted with AES-256-GCM under a data encryption key (DEK) generated for that value alone. The DEK is then itself encrypted with the deployment’s master key before anything is written.
What lands in the database is the ciphertext, its nonce, the wrapped DEK and its nonce. Neither the plaintext value nor the unwrapped DEK is ever stored. Reading a secret runs the same steps backwards: unwrap the DEK with the master key, then decrypt the ciphertext with it. Nonces are 12 random bytes, generated fresh for every encryption. GCM authenticates as well as encrypts, so a ciphertext that has been tampered with fails to decrypt rather than returning something wrong.

Why per-secret keys

A single key over every value in the database would make one compromise total, and would make rotation an all-or-nothing rewrite. Per-value DEKs keep the blast radius of any one key at one value, and mean the master key is only ever used on 32-byte payloads.

The master key

The master key is deployment configuration, held in the environment and never in the database. On the hosted service it is managed by Sink; a self-hosted deployment supplies its own. Losing it means losing every value in the deployment — the readiness probe treats an unreadable master key as a critical failure and takes the node out of rotation rather than serving requests that cannot decrypt.
This is at-rest encryption with a server-held key, not end-to-end encryption. A Sink deployment can decrypt the values it holds — that is what makes sink pull, the dashboard and share links work. If your threat model requires that the operator cannot read your values, Sink is not the right tool.

When a value cannot be decrypted

Listing an environment decrypts each secret independently. If some values fail — a rotated master key, a corrupted row — the call answers 207 Multi-Status with the readable records under detail.succeeded and the ids that failed under detail.failed_ids, instead of failing the whole read. The CLI prints a warning naming how many were skipped and carries on with the rest.

Credentials

API keys

Keys are sk- followed by random hex, minted by POST /keys/. Only the SHA-256 digest is stored, so the token is shown exactly once, at creation, and a lookup miss is indistinguishable from a bad key. Each key records a last_used timestamp, can carry an expiry, and can be revoked without touching any other credential.

Passwords

Passwords are hashed with bcrypt and a per-password salt. Hashing runs on a worker thread rather than the event loop, so one login’s cost is paid by the caller who provoked it and not by every request in flight.

Sessions

Browser login issues a short-lived access token and an HttpOnly, Secure, SameSite=Lax refresh cookie. Sessions are one per device: signing in again from the same user agent revokes that device’s previous session, so the refresh token it held stops working. Only the digest of a refresh token is stored, and GET /sessions/ lists live devices without ever returning it.

Defences at the edge

CSRF

An origin check stops a page on another origin spending the session cookie in a browser. Credentialed cross-origin calls are limited to an explicit allowlist.

Rate limiting

A global backstop on every request, plus tighter budgets on login, OTP, share redemption and billing. Buckets key on API key, then session, then IP.

Interactive docs off

/docs, /redoc and /openapi.json are served only when the deployment runs with DEBUG enabled — so not on the hosted production instance.

Local file permissions

Files written by sink pull, and the CLI’s own config file, are created 0600. Values print masked unless you ask for them.
Rate limits fail open. If the counter store is unreachable the request proceeds and the failure is logged — a secrets manager that deployments read from at boot should lose abuse protection during an outage, not lock every caller out of their own configuration.

What is written down

For what Sink retains and for how long, see the Privacy Policy and the list of subprocessors.