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 worked — it simply never learns what the value was.
This matters for agent platforms more than for most software, because an agent needs real credentials to be useful. A bot that posts to Slack needs a workspace token. A bot that runs an engine needs an engine account. 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.
This guide covers the hops a pasted secret normally takes, the construction that removes most of them, why the binding is the interesting part, and what sealing does not buy you.
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 serialised 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, and on the screen of whoever is using it.
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, the people with production access, and any future code path a colleague adds under time pressure.
Sealing attacks the problem at the first hop instead: the value is encrypted before it ever leaves the page, to a key that exists only on the target machine. Every hop after that is carrying an opaque blob. This is what makes a control plane for self-hosted agents a defensible design rather than a naming exercise — the claim is not that secrets are encrypted, it is that the control plane 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. Each envelope is built like this:
- Generate a fresh X25519 key pair for this one envelope (RFC 7748).
- Do an ECDH exchange between that ephemeral private key and the runner’s published box public key.
- Derive a 256-bit key with HKDF-SHA256 (RFC 5869), salted with the two public keys and separated by a fixed info string, so a key derived here can never collide with one derived for another purpose.
- Encrypt with AES-256-GCM (NIST SP 800-38D) under a fresh 12-byte nonce, passing a binding digest as additional authenticated data.
- 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 decoder is strict, so a non-canonical encoding is refused. One ciphertext therefore maps to exactly one envelope string, which closes off a class of malleability games. The whole envelope is capped at 8 KiB; no legitimate credential is larger, and the cap bounds the work a hostile input can ask the opener to do before any parsing happens.
If this reads like a public-key sealed box, that is because it is one. libsodium’s sealed boxes and the age file format are built from the same parts. The reason to write one down rather than adopt a library wholesale 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 a NUL-joined frame:
"agency.seal.v1" \0 runner \0 command \0 request \0 org \0 subject \0 purpose
Because that digest is authenticated by GCM, an envelope presented under any different context fails to open at all. Not “opens and is rejected by application logic” — fails the AEAD. Concretely: an envelope sealed for one runner cannot be opened by another; one sealed for a particular organization cannot be replayed into a different one; one sealed for a specific command cannot be spent against a second command that arrives later.
purpose is a closed set of four values, one per class of secret, and two of those values are the Slack bot token and app token slots kept deliberately separate. A workspace that can post but cannot receive is a real half-working state, and separating the purposes means slot confusion is a hard cryptographic failure rather than a coincidence that happens to be 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.
Getting the right public key
Sealing to the wrong key is a complete failure with the appearance of success, so key distribution deserves as much attention as the cipher suite.
The runner publishes its box public key in the snapshots it pushes. The browser seals to that key. Klingbar pins the key on first contact and renders exactly three states afterwards: it matches, it does not match — named as a mismatch, with re-pairing as the stated repair — or it has not been reported. Absence is treated as unknown rather than as a fault, because a runner that has not yet reported a key has not said anything wrong. A mismatch, on the other hand, suppresses the human approval ceremony entirely: there is no point signing something the machine has already decided not to honour.
The trust-on-first-use model is not the strongest possible one, and it is worth stating its terms plainly. It defends against everything after the first contact and nothing during it. Verifying the fingerprint out of band at pairing time is the step that closes that window, which is why the fingerprint is rendered where a person pairing a machine will actually see it.
What the browser must and must not do
The browser side is where a design like this is usually undone, so the rules are worth being blunt about.
Do the sealing with the platform’s own Web Crypto API, and have no fallback. If the primitives are unavailable — an insecure context, an ancient browser — the correct behaviour is to say so and refuse. A plaintext fallback path is not a degraded mode, it is the vulnerability, and it will be the code path that runs on the one machine that matters.
Keep the secret out of the form. In Klingbar the input that receives a pasted credential carries no name attribute at all, so it cannot serialise into a form post even by accident; the field is zeroed on every path out; and the only thing that ever leaves the page is the agseal1: envelope. This is the structural version of a rule that is otherwise only a promise: the value never becomes a server input, because there is no mechanism by which it could.
Downstream, the sealed envelope travels the command vocabulary as an ordinary opaque parameter. No verb carries a key, a token, or a signing secret as a flag — the absence is pinned by a test rather than left to reviewer vigilance — and the snapshot ingest path runs an entropy check that treats a secret-shaped string as a defect rather than as data.
Rotation, revocation, and honest states
Delivery is a command like any other, which means it inherits the gating and the record-keeping described in human approval workflows and audit trails for AI agents. The trail records that a credential was delivered, to which runner, for which slot, and who approved it. It records no part of the value.
The states a surface may honestly show are narrower than they look. A succeeded delivery means the runner accepted a token. Whether the provider will honour it is a question only the provider can answer, so between the command and the runner’s next report Klingbar says “applied — awaiting the runner’s next report” rather than rendering a green state it cannot see. Probe results, when the runner reports them, are shown as what they are: the runner’s verdict, at a stated time.
Rotation is delivery again, and revocation is its own verb. Neither requires the control plane to have ever held the previous value, which is the point.
What sealing does not buy
It does not protect a secret from the machine that legitimately opens it. A compromised runner 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 worth a great deal and is not the same as eliminating trust.
It does not protect against an engine or a tool that logs the value after decryption. It does not repair an over-scoped token: a workspace token with more permissions than the bot needs is dangerous regardless of how carefully it travelled. And it does not remove the need to rotate.
Finally, a format like this only holds its promises 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 a browser implementation and a Go one. Klingbar pins the two against a shared golden fixture, byte for byte, in both repositories. Changing any of it means a new version tag and a coordinated re-blessing on every peer — not a patch release. If you build your own, decide that before you ship it, because the alternative is discovering the coupling during an incident.