Guides

Sealing a secret to the box that will use it

Seal a secret in the browser to the box that will use it: X25519, HKDF-SHA256, AES-256-GCM, and a binding that lets an envelope open in one context only.

Last reviewed: October 2026. This guide is kept current in place, so its address never changes when it is refreshed.

Sealing a credential is encrypting it at the moment a person types it, to a public key held only by the machine that will use it, so that every system in between carries ciphertext none of them can open. The management surface still relays the value, still records that a delivery happened, and still shows whether it landed — it simply never learns what the value was.

This matters for AI agent products more than for most software, because an agent needs real credentials to be useful. In Klingbar, the personas in a crew report in Slack, so a crew on its own Slack app needs two tokens on the box; a persona that reads your analytics needs that service’s API key. Somebody has to get those onto the machine, and the ordinary answer — paste it into a settings page — quietly makes the settings page the most sensitive system in the deployment.

Where a pasted secret usually goes

Trace one honestly and the list is longer than the architecture diagram suggests. It exists in the browser tab’s memory. It is serialized into a form post. It is decrypted at the TLS terminator. It sits in the application process’s memory, and possibly in a request log, a crash dump, or an error report. It is written to a database, and from there to every backup and every replica. It appears in the support tooling that reads that database.

Encrypting the column addresses the last few hops and none of the first ones. The application still holds the plaintext at the moment of receipt, and “the application” means the deployment, the logging stack, and everyone with production access.

Sealing attacks the problem at the first hop instead: the value is encrypted before it leaves the page, to a key that exists only on the target machine. Every hop after that carries an opaque blob. This is what makes a management surface for self-hosted agents defensible — the claim is not that box secrets are encrypted at rest, it is that klingbar.com never has them.

The construction

Klingbar’s envelope format is named agseal1. Nothing in it is novel; the value is in the assembly and in the fact that it is frozen. The browser builds each envelope with the platform’s own Web Crypto API:

  1. Generate a fresh X25519 key pair for this one envelope (RFC 7748). Its private half is created non-extractable and used once.
  2. Do an X25519 exchange between that ephemeral private key and the box’s published public key.
  3. Derive a 256-bit key with HKDF-SHA256 (RFC 5869), salted with the box’s public key followed by the ephemeral one, under a fixed info string, so a key derived here can never collide with one derived for another purpose.
  4. Encrypt with AES-256-GCM (NIST SP 800-38D) under a fresh 12-byte nonce, passing a binding digest as additional authenticated data.
  5. Concatenate and encode.

The wire form is one string:

agseal1:BASE64( ephemeral_public_key[32] || nonce[12] || ciphertext || gcm_tag[16] )

The base64 is the standard padded alphabet — exactly what a browser’s own encoder produces — and the box’s opener refuses anything that is not. The whole envelope, prefix included, is capped at 8 KiB on both sides, which bounds the work a hostile input can ask for before parsing starts. When opening fails for any reason — a wrong key, a truncated body, a forged tag, a mismatched binding — the box records one flat sentence, “envelope did not open”, so a rejection never tells anyone which part of a forged envelope was wrong.

If this reads like a public-key sealed box, it is one: libsodium’s sealed boxes and the age format are built from the same parts. The reason to write one down is the next section.

The binding is the interesting part

Encryption answers “who can open this”. It does not answer “what may they open it as”, and for a credential delivery that second question is where the real failures live.

The additional authenticated data is a SHA-256 digest over seven fields joined by NUL bytes: a fixed version tag, the box’s id, the delivery id (in two slots, because there is exactly one delivery per envelope), the crew’s slug, the secret’s name, and its purpose. A NUL inside any field is refused, so the join is unambiguous.

Because GCM authenticates that digest, an envelope presented under any other context fails to open at all. The box never takes the context from the envelope; it computes the digest from what it believes it is opening. Concretely: an envelope sealed for one box cannot be opened by another; one sealed for one crew cannot be replayed into a second crew on the same box; one sealed as RESEND_API_KEY cannot be delivered under a different secret name; and one sealed for one delivery cannot be spent against a later one.

The delivery id is minted by klingbar.com when the secrets page renders, before anything is typed, because the browser needs it to seal and the box needs the same value to open. A second post under the same id is refused as a replay.

For a credential, purpose is one of three values: slack-bot-token, slack-app-token, or crew-secret. The two Slack bot and app tokens are kept apart deliberately. A Slack app that can post but cannot receive events is a real half-working state, and separate purposes make swapping the two a hard cryptographic failure rather than a coincidence caught downstream.

The general lesson survives the specific format: bind the ciphertext to the context it is allowed to be used in, and make that binding part of the authentication rather than a field somebody remembers to check.

Where the public key comes from

Sealing to the wrong key fails with the appearance of success, so key distribution matters as much as the cipher suite.

A box holds two key pairs: an Ed25519 pair that signs every request it sends, and an X25519 pair that secrets are sealed to. The private halves live in the box’s own secrets/ directory, readable only by the account that runs the daemon, and never leave the machine. The public halves reach klingbar.com once, at pairing, in the request that presents the one-time KB-… code you copied from your account. That code lives for fifteen minutes and is consumed by the first box that claims it, so the trust anchor is a person signed in to klingbar.com handing a code to a machine they control. Every request after pairing is signed, and klingbar.com stores only public keys, so a copy of its database can neither impersonate a box nor open an envelope.

If a box has not published a sealing key, the secrets page renders no form at all. It says that a secret cannot be sent safely and names pairing again as the repair.

What the browser must and must not do

Seal with the platform’s own cryptography and have no fallback. Klingbar’s sealing module has exactly one output — an agseal1: string — or a refusal. If the browser cannot do X25519, the form is replaced with a sentence asking for a current version of Chrome, Edge, Firefox, or Safari, and nothing is accepted. A plaintext fallback is not a degraded mode; it is the vulnerability.

Keep the secret out of the form. The input carries no name attribute and its form is never submitted natively, so the value cannot ride a form serialization, an autofill store, or a query string even by accident. On submit the script seals the value, clears the field, and posts exactly two things: the envelope and the CSRF token. The plaintext buffer is zeroed after every seal, successful or not. The server accepts those two fields and nothing else, and checks only what ciphertext allows: the prefix and the size. The value never becomes a server input, because there is no mechanism by which it could.

How a sealed value reaches a box nobody can dial into

A box dials out; klingbar.com never connects to one. So a delivery is not pushed. The envelope is stored as a queued delivery and handed to the box in the response to its next signed sync. The box opens it, writes the value into the crew’s own secrets directory with owner-only permissions, and reports secret.applied or secret.rejected on the sync after that. A new Slack token restarts only that crew’s Slack connection.

A crew on Klingbar’s app keeps no Slack token on the box; Slack events arrive as slack-event envelopes klingbar.com seals, never a browser.

Klingbar.com deletes the envelope as soon as the delivery settles — applied, rejected, or expired — so the window in which its database holds even ciphertext is about one sync. Queuing a delivery writes a row to the crew’s audit trail naming who, which box, which secret name, and which purpose; no part of the value is recorded (audit trails for AI agents covers the shape of that trail).

The page shows each delivery’s state — queued, delivered, acknowledged, rejected, or expired — and “not reported” when there has been none. It never shows a value, a prefix, or a length. For the two Slack tokens klingbar.com also shows the box’s own auth.test result, as the box reported it, because only Slack can say whether Slack honors a token.

Not every secret has to travel. A file: spec in the crew’s configuration is the kind you hand over through this form; a gcp: or env: spec is resolved by the box itself, and that value never passes through klingbar.com at all.

Rotation and moving a crew

Rotation is delivery again: paste the new value, and the box replaces the file when it opens the envelope. At no point does klingbar.com need the previous value.

Moving a crew to another box is where a naive design would need plaintext after all. You pair the new box and start the move from the crew’s danger settings. Klingbar.com sends the old box secret.transfer, naming the new box’s public key. The old box re-seals every secret it holds, plus a bundle of the crew’s jobs, threads, cards, and state, to that key, bound to the transfer’s id, and ships the envelopes on its next sync. Klingbar.com relays them and deletes each on acknowledgment; when the new box reports the import, the old box is told to retire the crew. If the old box is gone for good, the secrets are entered again on the web.

What sealing does not buy

It does not protect a secret from the machine that legitimately opens it. A compromised box has the plaintext, by construction, and no envelope format changes that. Sealing narrows the blast radius to the machine you already had to trust, which is not the same as eliminating trust.

It does not protect against an engine or a tool that prints the value after decryption; a run that is handed a key can put it in a transcript. It does not repair an over-scoped token: a Slack token with more scopes than the crew’s app needs is dangerous however carefully it travelled. And it does not remove the need to rotate.

Finally, a format like this holds its promises only if it is frozen. agseal1 names a version because the frame layout, the salt order, the info string, the digest, and the base64 alphabet are all part of the contract between the browser’s implementation and the box’s Python opener. Both are held to the same golden test vector — a fixed key, nonce, and binding that must produce one exact envelope — byte for byte. The purpose list is a value inside the frame rather than part of its structure, which is why adding crew-secret left the golden vector unchanged. Changing anything else means a new version tag and a coordinated update on every peer, not a patch release. If you build your own, decide that before you ship it.

Put a crew to work

Klingbar is free during early access. Request a spot and we will send an invite as capacity opens, or read how it works first.

Request early access

Read the other guides