Private keys for an AI agent app must live in a custody layer the language model never reads — not in chat, not in repository files, and not in environment variables the agent can echo into logs or tool output.
The safe pattern is server-side storage with policy: the runtime sends a payload in and receives a signature, MAC, or short-lived credential out. The key bytes stay in a vault, KMS, or HSM.
OAuth access tokens solve a different problem (acting as a user at a third-party API). This page is about material you actually sign with: JWT issuers, webhook HMAC, chain keys, and TLS client identities.
What counts as private key material for agents
A private key is any secret used to prove authenticity or decrypt data: Ed25519 or ECDSA for JWTs, RSA for legacy integrations, HMAC-SHA256 for webhook verification, or a secp256k1 key that signs blockchain transactions.
API keys from Stripe or OpenAI are also secrets, but they are usually bearer tokens you send as headers — not asymmetric keys you sign payloads with. Custody overlaps (never give the model the raw value), yet rotation and blast radius differ.
Agents make custody harder because they routinely assemble context from files, stderr, and `.env` loaders. Anything the process can read can end up in a prompt or a committed patch within one unattended cycle.
OAuth access token vs private key — when each fits
| Need | Right tool | Why |
|---|---|---|
| Call Gmail, GitHub, or Slack as the owner | OAuth access token (brokered refresh) | Upstream issued delegation; you are not holding Google's signing key |
| Issue JWTs your product verifies (`RS256`, `ES256`) | Private signing key in vault/KMS | You are the issuer; OAuth from a vendor does not replace your key pair |
| Verify inbound webhooks (Stripe, Shopify, custom) | HMAC or asymmetric verify key in vault | Partners sign with a shared secret or public key you publish |
| Sign chain transactions or attest on-chain state | Chain-specific private key, hardware or MPC where possible | No third-party OAuth endpoint can substitute for on-chain signing |
| Decrypt PII at rest your app encrypted | Data encryption key wrapped by KMS | Decryption is a controlled operation, not a bearer header |
Where private keys must not live
These placements fail audits and fail in production with agents:
Model context or chat transcripts
A pasted PEM block becomes training-adjacent context and is trivial to exfiltrate via prompt injection.
Repository files (even “gitignored” locals)
Agents commit. `.gitignore` is not enforced at generation time. Assume any file in the tree can ship.
Frontend bundles and `NEXT_PUBLIC_*`
Anything bundled for the browser is public. Client-side “signing” is not signing with a private key.
Shared `.env` on the agent worker
Better than committing, but the agent's tools can read process env when debugging. Treat env as readable by the model's toolchain.
Long-lived copies on the laptop that runs the agent
Founder machines sync to cloud drives and screen shares. Custody should be a service with ACLs, not a file on disk.
Server-side custody patterns that work
Pick one layer; combine policy + audit on top:
Cloud KMS (AWS KMS, GCP Cloud KMS, Azure Key Vault keys)
Keys never leave the HSM boundary; your API calls `Sign` with an IAM-scoped principal. Good default for JWT issuers on AWS/GCP.
Dedicated secrets vault (HashiCorp Vault, cloud SM + signing plugin)
Central policy, namespaces per tenant, dynamic credentials. Agents call your API; your API calls Vault with a narrow token.
Signing-without-export product API
Agent holds only a `vlt_live_…` capability token. `POST /sign` returns `signature`; there is no `GET /private-key`.
Temporary credentials
Mint a short-lived, read-limited token for one job instead of handing the agent the root secret. Revoke by TTL.
Enclave or MPC (high-value chain keys)
When a stolen file means immediate fund loss, keep material in hardware or multi-party computation; agent never sees shards.
Signing without export (payload in, signature out)
The architectural goal is to shrink what the agent possesses from “a key” to “permission to request a signature on named payloads.”
Your worker validates the agent identity, checks policy (allowed algorithms, max payloads per minute, named key ids), hashes the payload server-side, and asks the vault to sign. The response contains `signature` and often `public_key` for verification — never PEM private bytes.
Verification can happen anywhere with the public half. Issuance stays in one place. Compromise of the agent token might allow fraudulent signatures until revocation, but it does not let an attacker download a key that works offline forever.
Algorithms agents commonly need stored
| Algorithm | Typical use | Storage note |
|---|---|---|
| Ed25519 | Modern JWT (`EdDSA`), inter-service attestations | Prefer for new issuers; small public keys, fast verify |
| ECDSA P-256 (`ES256`) | JWT for OIDC-shaped products, Apple-style ecosystems | Match what verifiers already expect in JWKS |
| RSA-2048 (`RS256`) | Legacy enterprise JWT consumers | Larger keys; still common in B2B webhooks |
| HMAC-SHA256 | Webhook signatures, symmetric MAC APIs | Treat like a private key — same no-export rule |
Anti-patterns specific to agent apps
These show up when a coding agent “helpfully” wires auth:
Generating a key pair inside the repo
The private half lands in `keys/private.pem` and gets committed on the next deploy fix.
Loading PEM from env into the prompt for “context”
Debug steps that print `process.env` teach the model where secrets live.
Reusing the founder's personal JWT issuer key for every customer tenant
One leak breaks every tenant. Per-tenant or per-agent key ids contain blast radius.
Using OAuth refresh tokens where a signing key is required
You still need your own issuer key to mint first-party session JWTs.
Storing keys in the same database row as agent memory
Memory tables are exported, logged, and synced. Keys belong in a vault table with different ACLs.
Vault SDK: sign without reading the key (1.0.0)
From `@empyre/vault-sdk` **1.0.0** (this repo's `packages/vault-sdk` and registry.npmjs.org on 2026-09-28). Run on your server, not inside the model loop:
import { VaultAgent } from "@empyre/vault-sdk";
// vlt_live_… token — agent capability, not the signing key
const vault = new VaultAgent({ token: process.env.VAULT_AGENT_TOKEN });
const keys = await vault.signingKeys.list();
const payload = JSON.stringify({ sub: "agent-7", action: "issue-invoice" });
// Private key never returned — only signature + public_key for verify
const { signature, public_key, algorithm } = await vault.sign(
keys[0].id,
payload,
"utf8",
);
// Optional: batch up to 20 payloads in one round-trip
// await vault.signBatch([{ keyId: keys[0].id, payload: "..." }]);
How this fits Empyre Vault, Relay, and operators
Empyre Vault at vault.empyre.dev returned HTTP 200 on 2026-09-28. The npm package is @empyre/vault-sdk 1.0.0. Stored signing keys have no read path: agents list key metadata, call sign, and receive signatures for Ed25519, ECDSA P-256, RSA-2048, and HMAC-SHA256.
Empyre Relay at relay.empyre.dev (also HTTP 200 on 2026-09-28, @empyre/relay-sdk 1.0.0) handles OAuth for agents — hosted consent, PKCE, refresh — not custody of your issuer private key. Use Relay when the authority is a third-party API; use Vault when the secret is yours to sign with.
Empyre itself is a company operator: eight fixed agent roles build and run a business after launch, with a thirty-minute first-deploy ceiling when code is the bottleneck. Generated products still need the same custody discipline for webhooks and JWTs the CTO might add.
For exposure patterns after a leak, read how AI agents leak API keys. For OAuth wiring, read let an AI agent authenticate with third-party APIs securely and agent OAuth token exchange. Broader secret hygiene: secret management for AI agents.
Common questions
Are environment variables ever OK for private keys?
For a conventional single-purpose server, sometimes — with strict ACLs. For agent runtimes, treat env as readable by tooling the model invokes. Prefer a vault sign API the agent calls over HTTP from your worker.
Can I encrypt the key and store ciphertext in the repo?
Only if decryption happens in KMS/HSM with an IAM gate the agent lacks. Ciphertext plus a decryptable passphrase in env is the same failure mode.
Does OAuth remove the need to store keys?
No for first-party JWTs, webhook HMAC, or chain signing. Yes for calling third-party APIs on user delegation — that is access tokens, not your issuer private key.
What should the agent hold instead of the key?
A scoped capability token, session id, or job ticket that your backend exchanges for a signature. The agent proves identity; the vault proves cryptography.
How is this different from the leak article?
That page covers how secrets escape prompts, logs, and commits. This page covers where keys should live before exposure — custody, KMS, and sign-without-export.
What versions were verified on 2026-09-28?
`@empyre/vault-sdk` and `@empyre/relay-sdk` at 1.0.0 on registry.npmjs.org; vault.empyre.dev and relay.empyre.dev both returned HTTP 200 via curl -sI.
Try Empyre free for 3 days
Describe a business in plain words and watch eight AI agents build and deploy it. Starter is free for the first 3 days.
Related
Last updated 2026-09-28. Competitor descriptions reflect each product's publicly documented capabilities at that date; they change often, so check the source before relying on a detail.