seekrit
← all posts

Secrets at the edge: why a decrypting server can't scale the way a ciphertext-only one does

Architecture

A secrets manager has one route that carries almost all of its traffic: the call a container, CI job, function, or agent makes at startup to fetch its environment. One organization can make tens of thousands of those calls a day. Each one sits on the critical path of a deploy, an autoscale event, or a cold start.

How that route is served depends on a decision made before any code is written: does the server decrypt, or not? Most managers, including Infisical and Doppler, decrypt on the server. seekrit does not. That decision determines whether the route can be cached and where it can run.

When the server decrypts

In the conventional design, values are encrypted at rest with keys the server can use: a root key in its configuration, or a cloud KMS it has permission to call. A workload authenticates, the server reads the values, decrypts them, and returns plaintext over TLS.

This design is common and it has real advantages. Because the vendor can read a value, the vendor can rotate it, transform it, or push it to another platform on your behalf. It also has three costs that follow directly from producing plaintext on the server per request.

The response can't be cached anywhere shared. An edge cache is a place where responses wait, unattended, for the next matching request. A plaintext database password can't be stored there. So every resolve has to reach a machine that holds the decryption key, and that machine is at the origin. Doppler's origin runs in cloud regions; Infisical's runs wherever you deployed it. A workload booting in Singapore against an origin in Virginia pays the round trip on every boot, and the origin's capacity caps the fleet's boot rate.

Decryption scales with the vendor's fleet, not yours. Each resolve costs the server a key lookup and one decryption per value. A thousand pods restarting after a node drain is a thousand simultaneous decryptions of the same environment at the origin. The usual mitigations are rate limits and a plaintext cache on the client's disk.

The read path is inside the blast radius. If the decrypting service is compromised, or an operator at the vendor is, values are readable. Doppler and Infisical both encrypt at rest, control access, and keep audit trails, and Infisical can run entirely inside your network. Those are defenses of a server that holds readable data. A server that holds no readable data doesn't need them for the read path.

Infisical used to offer end-to-end encryption in its web UI and removed it. The stated reason was that E2EE made machine identities, dynamic secrets, and integrations hard to build, because the server could not participate. That is the same trade-off from the other side: if the server should act on values, the server has to read them. seekrit's position is that the server should not, and that the features this removes can be rebuilt on the client.

When the server never decrypts

Every environment has its own data encryption key. Secrets are encrypted in the browser or CLI with that key before upload, so the server receives only ciphertext. Each principal that may read the environment, a person or a service token, has a keypair. The environment key is wrapped to the principal's public key and stored. A service token carries its private key inside the token string. The server stores a hash of the token for authentication and the public key for wrapping, and never sees the private key.

A resolve response therefore contains two things: the environment's ciphertext and a copy of the environment key wrapped to the caller's public key. Neither is readable without the caller's private key, which never leaves the caller. The full model is in the encryption docs.

This changes each of the three costs above.

The response is cacheable. seekrit's API is a Cloudflare Worker running at every Cloudflare edge location, and it caches resolve responses there. The cache key includes the caller's principal, org, environment, and any overrides, so two principals never share an entry and no caller receives another's wrapped key. Identity is verified on every request, hit or miss. After that check, a repeat boot of the same workload is served from the nearest edge location and never reaches the database. The credential lookup is served from an edge key-value store, so a cached resolve makes zero database round trips.

The cache holds the same bytes the database holds, and the database was designed to hold nothing readable. The cache is a consequence of that, not a feature added on top.

Decryption scales with your fleet. Each workload decrypts its own environment with its own key on its own CPU. A thousand restarting pods are a thousand edge cache hits and a thousand local AES-GCM operations. Nothing on the vendor's side does more work.

The read path is outside the blast radius. A compromised edge cache or a database dump yields ciphertext and wrapped keys. There is no server-side key to steal. The scale docs describe the caching in more detail.

Invalidation

A rotated key that keeps being served is worse than a slow resolve. Each cached response is tagged with every environment it read, including composed groups. Every mutation that can change a resolve, whether a write, a rotation, a grant, a revoke, or a composition change, purges those tags. Other environments keep their caches. A short backstop lifetime bounds staleness if a purge is missed, and a test in the API's build fails if a mutating route omits the purge.

Revoking a token is a mutation and takes effect on the next resolve. No architecture can reach into a process that already decrypted; that is what rotation is for.

Where it matters

Serverless and edge runtimes. Workers, Lambdas, and Vercel functions cold start often and run anywhere. A resolve that reaches one region adds that region's latency to every cold start. A resolve from the nearest edge adds almost none.

Autoscaling and spot capacity. Fleets that scale in bursts or churn on preemptible nodes boot the same environment repeatedly. That is the best case for an edge cache and the worst case for an origin.

Agents. An agent sandbox is created per task and discarded, so every task is a cold boot, and it is the workload least suited to holding a plaintext cache. The sandbox guides cover the hosted runtimes.

Outages. Since the client can already decrypt, it can keep the last encrypted response on disk and fall back to it when the API is unreachable. The file is no more sensitive than the token beside it. seekrit's launcher does this behind a flag, and a refused resolve deletes the entry rather than falling back, so revocation still applies on the next successful contact. A server-side design can offer the same fallback only by caching plaintext on your disk.

Using it

End-to-end encrypted tools have a reputation for making users manage keys by hand. This is the full workflow.

Sign in and link a project. The CLI mints its credential locally and you approve it in the browser; the value never crosses the wire:

seekrit login
seekrit init --org acme --app storefront

Import an existing .env. Each value is encrypted on your machine before upload:

seekrit secrets import .env.production --app storefront --env production

Mint a token for a workload. It is created client-side and bound to one environment. The server stores a hash and a public key and cannot reconstruct it:

seekrit token create --app storefront --env production --name api-prod

Run the workload. This is the whole integration on the machine that needs the secrets. No SDK, no agent process, no config file:

SEEKRIT_TOKEN=skt_… seekrit run -- ./start-server

For containers, seekrit-run is the same command as a static binary with no runtime dependencies, so it works as an entrypoint on scratch. With --cache it survives an API outage on the last encrypted response it saw.

If you would rather not wrap the process, the SDKs resolve and decrypt in Python, Go, JavaScript, Ruby, and Elixir, each checked against the same test vectors. Kubernetes has an External Secrets Operator chart that decrypts inside the cluster. Terraform has a provider that keeps values out of state. There is a Nomad plugin, a WASM component, and an AWS KMS-compatible endpoint for tooling that already speaks that API.

None of these decrypt on seekrit's side. Each is the client half of the same envelope, so adding one never widens what the server can read.

Trade-offs

The server can't act on values. Pushing a secret to a platform whose runtime you can't inject into, such as Vercel, requires plaintext at a moment when no client of yours is running. seekrit offers that sync as the one exception to the model. It is opted in per environment, requires a grant computed on your side that the server can't create alone, decrypts in an isolated worker for the duration of one push, and records an acknowledgement in the audit log.

Writes are not at the edge. Mutations go to one Postgres region, because a write has to be durable and ordered. Writes are rare and human-paced; the read path is what a fleet loads.

No self-hosting. Infisical is open source and runs inside your network. seekrit does not offer that. Its answer to vendor disappearance is a signed export and an offline decryptor that needs no network and no seekrit.

A token is still a credential. A leaked service token can resolve and decrypt until it is revoked. It is one credential, bound to one environment, with a record of what it read, and revoking it is one step.

Summary

Feature lists converge. Where plaintext is produced does not. Produce it on the server and the hot path is tied to the server: one region, no shared cache, and a blast radius that includes every read. Produce it on the client and the server is a store of ciphertext, which is the one kind of data that can sit at the edge safely.

seekrit scales the way it does because there is nothing at the origin that the cache would expose.