How to store private keys for an AI agent app

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

NeedRight toolWhy
Call Gmail, GitHub, or Slack as the ownerOAuth 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/KMSYou 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 vaultPartners sign with a shared secret or public key you publish
Sign chain transactions or attest on-chain stateChain-specific private key, hardware or MPC where possibleNo third-party OAuth endpoint can substitute for on-chain signing
Decrypt PII at rest your app encryptedData encryption key wrapped by KMSDecryption 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

AlgorithmTypical useStorage note
Ed25519Modern JWT (`EdDSA`), inter-service attestationsPrefer for new issuers; small public keys, fast verify
ECDSA P-256 (`ES256`)JWT for OIDC-shaped products, Apple-style ecosystemsMatch what verifiers already expect in JWKS
RSA-2048 (`RS256`)Legacy enterprise JWT consumersLarger keys; still common in B2B webhooks
HMAC-SHA256Webhook signatures, symmetric MAC APIsTreat 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.

Start your free trial

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.