Security Best Practices
Production hardening for Agent Secret Store — from namespace design to incident response. Follow these patterns to run a credential store that survives real adversarial conditions.
Least-privilege namespace design
The most important security decision is your namespace structure. Namespaces directly control how granularly you can scope tokens. A flat namespace means all agents get access to everything in the same scope; a service-hierarchical namespace lets you issue tokens that cover exactly one service's credentials.
# ❌ BAD: One flat namespace — all agents can access all secrets
production/GEMINI_API_KEY
production/STRIPE_SECRET_KEY
production/DB_PASSWORD
production/GITHUB_TOKEN
# ✅ GOOD: Service-scoped namespaces
# Scope tokens to the minimum path prefix needed
production/payments/stripe/STRIPE_SECRET_KEY
production/payments/stripe/WEBHOOK_SECRET
production/ml-platform/gemini/GEMINI_API_KEY
production/ml-platform/pinecone/PINECONE_API_KEY
production/auth/google/GOOGLE_CLIENT_SECRET
production/data-pipeline/postgres/DATABASE_URL
# ✅ GOOD: Environment isolation prevents cross-env leakage
production/payments/stripe/STRIPE_SECRET_KEY # token: secrets:read:production/payments/*
staging/payments/stripe/STRIPE_SECRET_KEY # token: secrets:read:staging/payments/*
# A staging agent CANNOT read production secretsNamespace changes are breaking
Restructuring namespaces after go-live requires updating all scoped tokens and agent configurations. Design your namespace hierarchy before your first production deployment.
Token best practices
Scoped tokens are the primary access control mechanism. Issue the narrowest possible scope for the shortest necessary TTL:
import { AgentVault } from '@agentsecretstore/sdk';
const vault = new AgentVault({ agentKey: process.env.ASS_AGENT_KEY! });
// ❌ BAD: overly broad scope and long-lived token.
await vault.requestToken({
scopes: ['secrets:*:*'],
ttl: '24h',
});
// ✅ GOOD: narrowly scoped, short TTL, known agent host.
await vault.requestToken({
scopes: ['secrets:read:production/ml-platform/gemini/*'],
ttl: '30m',
ipAllowlist: ['10.0.1.50'],
});
// ✅ GOOD: single-use for high-risk one-off operations.
await vault.requestToken({
scopes: ['secrets:read:production/payments/stripe/STRIPE_SECRET_KEY'],
ttl: '5m',
maxUses: 1,
});| Use case | Recommended TTL | Notes |
|---|---|---|
| One-off task (payment, send email) | 5–15 minutes | Use maxUses: 1 for truly single-use ops |
| Short batch job (< 1 hour) | 30–60 minutes | Match TTL to expected job duration |
| Long-running agent session | 2–8 hours | Request a new token before TTL expires |
| Service worker (e.g. API server) | Max 24 hours | Rotate token daily via cron; never share master key |
| CI/CD pipeline | Duration of pipeline + buffer | Use dedicated service account key per pipeline |
Setting appropriate access tiers
Assign access tiers based on the blast radius if the credential were compromised. When in doubt, err toward a higher tier — you can always relax it after observing false-positive approval friction.
standardRead-only keys, public data sources, sandbox credentials, rate-limited free-tier keys, non-production secrets.
sensitiveWrite-capable API keys, OAuth tokens, staging database passwords, service-to-service tokens with meaningful access.
criticalProduction databases, payment processor secrets, admin API keys, SSH private keys, signing keys, KMS credentials.
IP allowlisting for production agents
IP allowlisting is the most powerful defense-in-depth measure available. A stolen token is useless from the wrong IP address. Always specify ipAllowlist for production token requests when your agents run from known infrastructure:
import { AgentVault } from '@agentsecretstore/sdk';
const vault = new AgentVault({ agentKey: process.env.ASS_AGENT_KEY! });
// Restrict a token to a specific agent host.
await vault.requestToken({
scopes: ['secrets:read:production/stripe/*'],
ttl: '1h',
ipAllowlist: ['10.0.1.50'],
});
// Restrict to a subnet (GCP/AWS private subnet).
await vault.requestToken({
scopes: ['secrets:read:production/*'],
ttl: '1h',
ipAllowlist: ['10.128.0.0/20'],
});
// Restrict to multiple known hosts.
await vault.requestToken({
scopes: ['secrets:read:production/ml-platform/*'],
ttl: '2h',
ipAllowlist: ['10.0.1.50', '10.0.1.51', '10.0.1.52'],
});Cloud provider IP ranges
GCP, AWS, and Azure publish their IP ranges as JSON files. For agents running on managed compute, allowlist the subnet CIDR of your VPC rather than individual IPs. This handles pod autoscaling without manual token updates.
Single-use tokens for high-risk operations
For one-time operations like payment processing, signing, or sending a notification, use maxUses: 1to create a burn-after-reading token. After the credential is retrieved once, the token is server-side invalidated — even if the token string leaks, it's already dead.
Combine with a short TTL (5 minutes) for maximum security: the token is useless after one use or after 5 minutes, whichever comes first.
Approval workflows for production secrets
For critical-tier secrets, require approval even when the requesting agent has appropriate permissions. This adds a human check-in point for sensitive operations:
import { AgentVault, ApprovalRequiredError } from '@agentsecretstore/sdk';
const vault = new AgentVault({ agentKey: process.env.ASS_AGENT_KEY! });
const scopes = ['secrets:read:production/payments/stripe/STRIPE_SECRET_KEY'];
async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForApproval(approvalId: string) {
while (true) {
const status = await vault.getApprovalStatus(approvalId);
if (status.status === 'approved') return;
if (status.status === 'denied' || status.status === 'expired') {
throw new Error('Approval ' + status.status + ': ' + approvalId);
}
await sleep(5000);
}
}
try {
await vault.requestToken({ scopes, ttl: '10m' });
} catch (error) {
if (!(error instanceof ApprovalRequiredError)) throw error;
await waitForApproval(error.approvalId);
await vault.requestToken({ scopes, ttl: '10m' });
}
const secret = await vault.getSecret('production/payments/stripe/STRIPE_SECRET_KEY');
console.log('Approved secret retrieved:', secret.length);Rotation schedule recommendations
| Credential type | Recommended rotation | Priority |
|---|---|---|
| Payment processor keys (Stripe, PayPal) | Every 14 days | 🔴 Critical |
| Production database credentials | Every 30 days | 🔴 Critical |
| SSH private keys | Every 30 days | 🔴 Critical |
| Production model API keys (Gemini, Bedrock) | Every 90 days | 🟡 High |
| OAuth tokens | On provider expiry + proactive 60-day | 🟡 High |
| Staging/dev API keys | Every 180 days | 🟢 Medium |
| Webhook secrets | Every 180 days | 🟢 Medium |
| Read-only data source keys | Annually | ⚪ Low |
Monitoring the audit trail for anomalies
Set up a recurring check (e.g. every 15 minutes) that queries the audit API for suspicious patterns. Key signals to watch:
const token = process.env.FIREBASE_ID_TOKEN!;
const oneHourAgo = new Date(Date.now() - 3600_000).toISOString();
async function auditEvents(params: Record<string, string>) {
const query = new URLSearchParams({ ...params, start_time: oneHourAgo, limit: '100' });
const response = await fetch('https://api.agentsecretstore.com/v1/audit?' + query, {
headers: { Authorization: 'Bearer ' + token },
});
if (!response.ok) throw new Error(await response.text());
return (await response.json()).events as Array<{ ip_address?: string; event_type: string }>;
}
const reads = await auditEvents({ event_type: 'secret.read', resource_type: 'secret' });
if (reads.length > 100) console.warn('Unusual secret read volume: ' + reads.length + ' reads in 1 hour');
const deniedApprovals = await auditEvents({ event_type: 'approval.denied' });
if (deniedApprovals.length > 5) console.warn('Multiple denied approvals: ' + deniedApprovals.length);
const knownIps = new Set(['10.0.1.50', '10.0.1.51', '10.128.0.5']);
const unexpected = reads.filter((event) => event.ip_address && !knownIps.has(event.ip_address));
if (unexpected.length > 0) {
console.warn('Secret reads from unexpected IPs: ' + unexpected.map((event) => event.ip_address).join(', '));
}If a key is compromised
Execute this runbook immediately when you suspect a credential has been leaked or compromised. Speed matters — do steps 1 and 2 before anything else:
# 1. Generate a replacement value at the provider first.
export SECRET_PATH="production/gemini/GEMINI_API_KEY"
export NEW_GEMINI_API_KEY="gemini-new-key-example"
# 2. Rotate the vault secret. Rotation revokes scoped tokens covering this secret.
curl -X POST "https://api.agentsecretstore.com/v1/secrets/$SECRET_PATH/rotate" \
-H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
-H "Content-Type: application/json" \
-d '{"new_value":"'"$NEW_GEMINI_API_KEY"'"}'
# 3. Export the audit trail for incident review.
curl "https://api.agentsecretstore.com/v1/audit/export" \
-H "Authorization: Bearer $FIREBASE_ID_TOKEN" \
-H "Accept: text/csv" \
-G \
--data-urlencode "event_type=secret.read" \
--data-urlencode "resource_type=secret" \
--data-urlencode "start_time=2026-05-01T00:00:00Z" \
-o incident-secret-reads.csvTeam member access control
Human access to the vault dashboard is managed through roles. Apply the same least-privilege principle to humans as to agents:
| Role | Can do | Who should have it |
|---|---|---|
| Admin | All actions including member management and billing | Vault owner only (1–2 people) |
| Editor | Create, update, rotate secrets; manage approval policies | Platform/DevOps leads |
| Viewer | View secret metadata (never plaintext values) | Developers, on-call engineers |
| Auditor | Read-only audit log access; CSV export | Security team, compliance officers |
Never share admin credentials
Each team member should have their own account. Shared admin credentials make it impossible to attribute changes in the audit log — a core SOC 2 requirement.
Scoped Tokens →
Deep dive into scope format, TTLs, and IP allowlisting.
Audit Trail →
Query and export the full event history for compliance.
Secret Rotation →
Automate credential rotation with zero downtime.
Compliance Roadmap →
SOC 2, GDPR, HIPAA, and PCI-DSS status and plans.