The agent requests an authorized operation, not a secret.
AI agents now take real actions on real infrastructure, and to do that they need credentials. The common practice is to hand the agent a long-lived provider secret and hope it never leaks. PastKeys removes the secret from the agent entirely: an agent authenticates as itself and requests an operation, and the broker decides whether it is allowed, uses the narrowest credential needed, and returns only the result. Provider tokens are stored sealed to a key whose private half lives only on the customer-operated broker, so the vendor stores ciphertext and holds no key. That the vendor cannot decrypt a customer's credentials is a property of the cryptography, not a clause in a contract.
An agent that can act needs authority to act. Today that authority is usually a bearer secret placed inside the agent's reach. This creates several distinct failure modes at once:
Secret managers and workload brokers each address part of this, but they share a common weakness: at some point a vendor-held or vendor-reachable key can decrypt the secret. PastKeys is built so that no such point exists.
The broker runs in one of two modes. In co-located mode it talks to the control-plane database directly; in API mode it reaches the control plane over HTTP with a broker token. In both modes the custody private key stays on the broker host and nowhere else.
agent ──(identity + intent)──▶ broker │ 1. authenticate the agent │ 2. authorize the operation (policy) │ 3. [if required] hold for human approval │ 4. mint or retrieve the scoped credential │ 5. execute against the provider │ 6. write one audit event agent ◀─────────(result)─────────
The credential exists only inside steps 4 and 5, on the broker, for one operation. It is never part of what returns to the agent.
This is the property that defines PastKeys.
Custody uses an anonymous public-key sealed box, built only from the Go standard library (crypto/ecdh, crypto/hkdf, crypto/cipher). Anyone holding the broker's public key can seal a token to it; only the holder of the private key can open it.
seal: ephemeral X25519 keypair shared = ECDH(ephemeral_priv, broker_pub) key = HKDF-SHA256(shared, salt = ephemeral_pub || broker_pub, info) blob = 0x01 || ephemeral_pub(32) || nonce(12) || AES-256-GCM(key, nonce, token, aad = ephemeral_pub || broker_pub) open: shared = ECDH(broker_priv, ephemeral_pub) ──▶ same key ──▶ AES-256-GCM open
Both public keys are bound as AEAD additional data, so a sealed blob cannot be re-pointed at a different recipient. A wrong key or any tampering returns a decryption error and never plaintext. This is verified by test.
Because the datastore only ever contains ciphertext, the storage backend can be swapped without weakening the guarantee. A stolen datastore or a fully compromised vendor control plane yields sealed blobs and nothing else.
The broker holds an ordered set of custody keys. The current key seals new tokens and publishes its public key; previous keys are retained only to open older blobs. When a read opens a blob with a previous key, the broker lazily re-seals it forward to the current key. The fleet migrates to a new key as credentials are used, with no bulk re-encryption step and no downtime, and older keys can eventually be retired. The wire format is unchanged, so rotation is fully backward compatible.
The private key can be sourced from a customer KMS or HSM instead of a file, so the key material never leaves the customer's cryptographic boundary. This is the natural enterprise upgrade and does not change the wire format.
Agents present a short-lived signed identity token. Verification sits behind a common interface with two implementations: real OIDC/JWKS verification (RS256/ES256) for production, chained with an HMAC verifier for local development. The OIDC audience is validated against the configured value, which closes a confused-deputy path where an agent could present a token minted for a different service.
In co-located mode the broker fails closed on identity: it refuses to start with the default development secret. A real secret from the secret manager, or an OIDC issuer, must be configured. The first-party broker binds to localhost only.
A request names a provider, a resource, an action, and parameters. The engine evaluates in two passes:
explicit_deny.The decision records which rule matched, which is carried into the audit event. Beyond provider, resource, and action, a rule can additionally constrain:
one_of and not_one_of, prefix, regex (anchored full-match, compiled and cached), and min/max numeric bounds.not_before and not_after bound when a rule is active.rate_limited.Parameter constraints matter because they narrow what an allowed action may touch, not just whether it is allowed. An agent permitted to read from S3 can be held to a specific bucket prefix; an agent permitted to run a query can be held to a set of statements.
Some operations should not proceed on an agent's say-so alone. A policy can mark an action as requiring human approval. When it does:
An agent can also check, before acting, whether an operation would require approval, so it can set the right expectation with its human.
Where a provider supports it, the broker mints a fresh credential scoped to the operation and short in lifetime, rather than reusing a standing secret:
AssumeRole session credentials.VALID UNTIL expiry.Two providers are handled as honestly documented long-lived pass-through: Cloudflare and generic HTTP. Their tokens are used only inside the broker and never returned to the agent; per-call scoped minting for them is future work, and this document does not claim otherwise.
Mint caching. Minting can be expensive, a call to STS or the GitHub App API per operation. The broker keeps an optional bounded, time-trimmed cache of minted short-lived credentials, keyed by a secret-free scope hash. A cached credential is reused only while a comfortable safety margin remains before its expiry, so a credential is never served close to expiry. The long-lived root is never cached. On any provider error the broker invalidates the relevant cache entry, so a revoked or rejected credential is re-minted on the next attempt rather than served again from cache.
Every request produces exactly one event, and events contain no secret data. The event type has no field capable of holding a credential value; this is enforced by test, not left to discipline.
The log is append-only JSONL with a SHA-256 hash chain: each event commits to the previous one, so any insertion, edit, or deletion breaks the chain, and a verify command detects tampering. Events are enriched with request latency, a fingerprint of the request parameters (a hash, never the values), the rule that matched, and an error class, so an operator can reason about behavior and failures without any secret entering the log. A write-once or SIEM sink is the recommended production addition.
Control plane. Argon2id password hashing (64 MiB, t=3, p=2, 10-character minimum); login two-factor by emailed one-time code with hashed codes, a short TTL, an attempt cap, and throttled resend; email verification for new accounts; opaque database-backed sessions (HttpOnly, Secure, SameSite=Lax) with double-submit CSRF on every state-changing request; per-IP and per-account rate limiting with lockout on login, signup, reset, verify, and two-factor; no user enumeration; and a strict header set (HSTS, a CSP with no inline scripts, X-Frame-Options: DENY, nosniff, a same-origin referrer policy).
HTTP layer. Panic-recovery middleware, a request-body size cap on mutating methods, constant-time login comparison, and a bounded per-provider execution timeout with a clear error taxonomy (mint_failed, provider_error, timeout).
Host and deployment. Services run as a dedicated non-root user under extensive systemd sandboxing (NoNewPrivileges, ProtectSystem=strict, a restricted system-call filter, an empty capability set, MemoryDenyWriteExecute, and more), bound to localhost. Secret files are owned by root and readable only before privileges drop; the custody key is group-readable only by the service user. The origin is reachable only over a private Cloudflare Tunnel, so the host exposes no public ingress ports, with a modern TLS floor at the edge. Transactional email is sent over an authenticated relay with SPF, DKIM, and a DMARC policy of quarantine. Secret values live in a secret manager and never in source control; the repository records only where a secret lives, never its value.
| Threat | How PastKeys bounds it |
|---|---|
| Prompt injection | The broker authorizes the action, not the prompt. Default-deny policy bounds blast radius regardless of what the agent was told. |
| Compromised agent runtime | No long-lived provider secret is present to steal; the agent holds only a short-lived identity token. |
| Stolen agent identity | Tokens are short-lived and revocable, single-use tokens are replay-protected, policy still limits the identity, and audit attributes every use. |
| Stolen datastore or compromised vendor control plane | Only sealed ciphertext is exposed. The private key is not present, so decryption is impossible. |
| Credential replay | Short-lived scoped credentials expire quickly; single-use identity tokens cannot be re-presented. |
| Privilege escalation | Explicit resource, action, and parameter constraints; no silent broadening; unknown agent denies. |
| Confused deputy | Operations bind to the authenticated principal, not agent-supplied identity fields; OIDC audience is validated. |
| Audit tampering | SHA-256 hash chain detects any insertion, edit, or deletion. |
| Secret leakage via logs or responses | Secrets are never placed in events, responses, or errors; enforced by test. |
A credential broker asks its customers for the most sensitive thing they have: the keys to their infrastructure. Most brokers answer the resulting trust question with policy, with a promise not to look. PastKeys answers it with math. The vendor holds ciphertext and no key, so "we cannot decrypt your credentials" is a checkable property of the system rather than a statement of intent. Every other control in this document, policy, identity, approval, audit, hardening, sits on top of that foundation and makes the bounded cases smaller.
The agent requests an authorized operation, not a secret. That single change is what makes agentic access to real infrastructure safe to run.