Agent Secrets Vault
Secure per-agent secrets management with AES-256-GCM encryption and audit logging
Overview
The Agent Secrets Vault provides secure, encrypted storage for API keys, credentials, and tokens that your agents need to access external services. Think of it like Vercel's environment variables, but per-agent with cryptographic access control.
Zero-Trust Security Model
Your human owner configures secrets in the dashboard. Agents can only read their own secrets (verified by Ed25519 signature), never create, update, or delete them. This prevents compromised agents from modifying credentials.
Quickstart (2 minutes)
Get from “no secrets” to “my agent can read a secret” with the fewest steps.
- In the dashboard, open your agent → Secrets → New secret.
- Add a name (e.g. OPENAI_API_KEY) and value.
- From your agent runtime, call GET /v1/vault/secrets/:name with your agent signature headers.
- Use the returned value immediately (don’t print it, don’t store it in logs).
Common gotchas: most failures come from a DID mismatch or a signature timestamp/nonce issue. If you’re stuck, log the HTTP status + error body (not the secret).
Want a copy/paste example that generates a real X-Agent-Signature? See Docs → Examples → Node Signing.
Key Features
Envelope Encryption
Secrets encrypted with AES-256-GCM using unique data encryption keys (DEK), which are themselves encrypted with a master key encryption key (KEK) derived via HKDF
Per-Agent Isolation
Each agent only sees their own secrets. Access verified via Ed25519 signature authentication on every request
Complete Audit Trail
Every secret access logged with timestamp, IP address, user agent, and success/failure status
Environment Support
Organize secrets by environment (production, development, staging) and provider (openai, stripe, github)
Expiration Support
Set expiration dates on secrets. Expired secrets return 410 Gone and are automatically flagged
Read-Only for Agents
Agents can only retrieve secrets. All create/update/delete operations require dashboard JWT authentication
How It Works
Human Configures Secrets
Developer logs into NervePay dashboard, navigates to their agent, and creates secrets in the Secrets tab. Each secret has a name, value, optional description, provider, and environment.
Server Encrypts Secret
Backend generates a random 256-bit data encryption key (DEK), encrypts the secret value with AES-256-GCM, then encrypts the DEK with the master key encryption key (KEK) derived from SUPABASE_SERVICE_ROLE_KEY via HKDF.
Agent Requests Secret
Agent signs request with Ed25519 private key and calls GET /v1/vault/secrets/SECRET_NAME. Server verifies signature and checks that the agent DID matches the secret owner.
Server Decrypts & Returns
Backend decrypts the DEK using KEK, then decrypts the secret value using the DEK. Returns plaintext value to agent. Access is logged to secret_access_log table.
Agent Uses Secret
Agent uses the decrypted secret (e.g., API key) to call external services like OpenAI, Stripe, GitHub, etc. Agent should immediately track usage with /v1/agent-identity/track-service.
Copy/paste snippets
Minimal examples for signing requests and reading Vault secrets. Don’t log secret values.
TypeScript (Node)
// Minimal Passport signing + Vault get (Node)import crypto from "node:crypto";const BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";function decodeBase58(str: string): Buffer {const bytes: number[] = [];for (const char of str) {const index = BASE58.indexOf(char);if (index === -1) throw new Error('Invalid base58 char: ' + char);let carry = index;for (let i = 0; i < bytes.length; i++) {carry += bytes[i] * 58;bytes[i] = carry & 0xff;carry >>= 8;}while (carry > 0) {bytes.push(carry & 0xff);carry >>= 8;}}for (const char of str) {if (char !== "1") break;bytes.push(0);}return Buffer.from(bytes.reverse());}function signEd25519(privateKeyBase58: string, payload: unknown): string {const keyStr = privateKeyBase58.replace(/^ed25519:/, "");const privateKeyBytes = decodeBase58(keyStr);const seed = privateKeyBytes.subarray(0, 32);const pkcs8Header = Buffer.from("302e020100300506032b657004220420", "hex");const keyObject = crypto.createPrivateKey({key: Buffer.concat([pkcs8Header, seed]),format: "der",type: "pkcs8",});const msg = JSON.stringify(payload);const sig = crypto.sign(null, Buffer.from(msg), keyObject);return sig.toString("base64");}function authHeaders({ did, privateKey, method, path, bodyJson }: any) {const nonce = crypto.randomUUID();const timestamp = new Date().toISOString();const bodyHash = bodyJson? crypto.createHash("sha256").update(JSON.stringify(bodyJson)).digest("hex"): null;const payload = {method: method.toUpperCase(),path,query: null,body: bodyHash,nonce,timestamp,agent_did: did,};return {"Agent-DID": did,"X-Agent-Signature": signEd25519(privateKey, payload),"X-Agent-Nonce": nonce,"X-Signature-Timestamp": timestamp,};}async function vaultGetSecret({ apiUrl, did, privateKey, name }: any) {const path = "/v1/vault/secrets/" + encodeURIComponent(name);const headers = authHeaders({ did, privateKey, method: "GET", path });const res = await fetch(apiUrl + path, { method: "GET", headers });if (!res.ok) throw new Error(await res.text());return res.json();}
Python
# pip install pynacl requestsimport base64import hashlibimport jsonimport timeimport uuidimport requestsfrom nacl.signing import SigningKeyBASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"def b58decode(s: str) -> bytes:n = 0for ch in s:n *= 58n += BASE58_ALPHABET.index(ch)h = n.to_bytes((n.bit_length() + 7) // 8, "big")pad = 0for ch in s:if ch == "1":pad += 1else:breakreturn b"