Secrets Management in Production: Vault, AWS Secrets Manager, and Modern Approaches
Hardcoded secrets and environment variables are security liabilities in cloud-native environments. Modern secrets management provides dynamic credentials, automatic rotation, and audit trails for production systems.
Executive summary
Hardcoded secrets and environment variables are security liabilities in cloud-native environments. Modern secrets management provides dynamic credentials, automatic rotation, and audit trails for production systems.
Last updated: 3/15/2026
Executive summary
The most common security vulnerability in cloud-native applications is poor secrets management. Credentials hardcoded in source code, stored in environment variables, or committed to version control represent existential risks that cannot be mitigated through application-level security controls.
Modern secrets management platforms provide:
- Dynamic credentials that are short-lived and automatically rotated
- Centralized storage with encryption at rest and in transit
- Audit trails for every secret access and modification
- Access controls that enforce least privilege
- Automated rotation without application restarts
In 2026, secrets management is not optional for any production system handling sensitive data. The choice is between using a dedicated secrets manager or accepting that a breach will reveal credentials that provide full access to your infrastructure.
The secrets management problem
Traditional approaches and their failures
| Approach | Why it fails in production |
|---|---|
| Hardcoded secrets | Visible in version control, shared across environments, cannot rotate |
| Environment variables | Visible in process listings, stored in unencrypted config, difficult to rotate |
| Config files in repo | Committed to version control, accessible to anyone with repo access |
| Shared database credentials | Single compromise reveals all application credentials, no per-service isolation |
| Manual rotation | Human error prone, inconsistent timing, often skipped until incident |
The modern security requirement
Production systems must satisfy these requirements:
- No static credentials – All credentials should be dynamically generated
- Short TTL – Credentials expire automatically (minutes to hours, not months)
- Automatic rotation – No manual intervention required
- Access logging – Every secret access is auditable
- Encryption – Secrets encrypted at rest and in transit
- Zero knowledge – Application developers should never see production secrets
Secrets management platforms
Platform comparison
| Platform | Strengths | Trade-offs |
|---|---|---|
| HashiCorp Vault | Dynamic credentials, extensive integrations, open source | Complex setup, operational overhead |
| AWS Secrets Manager | Deep AWS integration, automatic rotation, managed service | AWS lock-in, limited dynamic credential types |
| Azure Key Vault | Azure integration, FIPS 140-2 validated | Azure lock-in, learning curve |
| Google Secret Manager | Cloud-native integration, IAM-based access | GCP lock-in, fewer advanced features |
| Infisical | Open source, developer-friendly, secret scanning | Newer platform, smaller ecosystem |
When to use which platform
Use HashiCorp Vault when:
- You need dynamic credentials for multiple database types
- You require advanced secret engines (PKI, Transit, SSH)
- You want to avoid cloud vendor lock-in
- You have complex secret generation requirements
Use AWS Secrets Manager when:
- Your infrastructure is primarily on AWS
- You need automatic AWS service credential rotation
- You prefer managed services over self-hosting
- You want tight integration with AWS IAM
Use Azure Key Vault when:
- Your infrastructure is primarily on Azure
- You need FIPS 140-2 validated encryption
- You want integration with Azure AD
Implementation patterns
Pattern 1: Dynamic database credentials
Generate short-lived database credentials for each application instance:
typescript// Vault dynamic database credentials
import { Client } from 'node-vault';
const vault = new Client({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
interface DatabaseCredentials {
username: string;
password: string;
leaseId: string;
ttl: number;
}
async function getDynamicDatabaseCredentials(
databaseName: string
): Promise<DatabaseCredentials> {
try {
const response = await vault.read({
path: `database/creds/${databaseName}`
});
return {
username: response.data.username,
password: response.data.password,
leaseId: response.lease_id,
ttl: response.lease_duration
};
} catch (error) {
throw new Error(`Failed to get credentials for ${databaseName}: ${error.message}`);
}
}
// Connection pool with automatic credential renewal
class DatabaseConnectionPool {
private credentials: DatabaseCredentials | null = null;
private pool: Pool;
private renewalTimer: NodeJS.Timeout | null = null;
constructor(
private databaseName: string,
private vault: Client
) {
this.pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: this.databaseName,
max: 20,
idleTimeoutMillis: 30000
});
}
async initialize(): Promise<void> {
await this.refreshCredentials();
// Schedule renewal before expiry
this.renewalTimer = setInterval(
() => this.renewCredentials(),
this.credentials!.ttl * 1000 * 0.8 // Renew at 80% of TTL
);
}
private async refreshCredentials(): Promise<void> {
this.credentials = await getDynamicDatabaseCredentials(
this.databaseName
);
// Update pool with new credentials
this.pool.config.user = this.credentials.username;
this.pool.config.password = this.credentials.password;
}
private async renewCredentials(): Promise<void> {
try {
await vault.write({
path: `sys/leases/renew`,
data: {
lease_id: this.credentials!.leaseId,
increment: this.credentials!.ttl // Renew for full TTL
}
});
console.log(`Credentials renewed for ${this.databaseName}`);
} catch (error) {
console.error(`Failed to renew credentials: ${error.message}`);
await this.refreshCredentials(); // Get new credentials
}
}
async query<T>(sql: string, params?: any[]): Promise<T[]> {
const client = await this.pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
async destroy(): Promise<void> {
if (this.renewalTimer) {
clearInterval(this.renewalTimer);
}
await this.pool.end();
// Revoke credentials
if (this.credentials?.leaseId) {
await vault.write({
path: `sys/leases/revoke`,
data: { lease_id: this.credentials.leaseId }
});
}
}
}
// Usage
const ordersDB = new DatabaseConnectionPool('orders', vault);
await ordersDB.initialize();
const orders = await ordersDB.query('SELECT * FROM orders LIMIT 10');Pattern 2: AWS Secrets Manager with automatic rotation
Use AWS-managed rotation for database and service credentials:
typescriptimport { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secretsManager = new SecretsManagerClient({});
interface AWSSecret {
username: string;
password: string;
host: string;
port: number;
dbname: string;
}
async function getAWSDatabaseSecret(secretName: string): Promise<AWSSecret> {
try {
const command = new GetSecretValueCommand({
SecretId: secretName
});
const response = await secretsManager.send(command);
if (!response.SecretString) {
throw new Error('SecretString is empty');
}
const secret = JSON.parse(response.SecretString);
return {
username: secret.username,
password: secret.password,
host: secret.host,
port: secret.port,
dbname: secret.dbname
};
} catch (error) {
throw new Error(`Failed to retrieve secret ${secretName}: ${error.message}`);
}
}
// Connection pool with secret caching and refresh
class AWSDatabaseConnectionPool {
private secret: AWSSecret | null = null;
private secretExpiry: number = 0;
private pool: Pool;
private readonly CACHE_TTL = 3600000; // 1 hour
constructor(
private secretName: string
) {
this.pool = new Pool({
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '5432'),
database: this.secretName,
max: 20
});
}
async getSecret(): Promise<AWSSecret> {
const now = Date.now();
// Return cached secret if still valid
if (this.secret && now < this.secretExpiry) {
return this.secret;
}
// Fetch new secret
this.secret = await getAWSDatabaseSecret(this.secretName);
this.secretExpiry = now + this.CACHE_TTL;
// Update pool configuration
this.pool.config.user = this.secret.username;
this.pool.config.password = this.secret.password;
this.pool.config.host = this.secret.host;
this.pool.config.port = this.secret.port;
this.pool.config.database = this.secret.dbname;
return this.secret;
}
async query<T>(sql: string, params?: any[]): Promise<T[]> {
await this.getSecret(); // Ensure secret is fresh
const client = await this.pool.connect();
try {
const result = await client.query(sql, params);
return result.rows;
} finally {
client.release();
}
}
}Pattern 3: API keys with Vault Transit
Generate and manage API keys using Vault's Transit secrets engine:
typescriptimport { Client } from 'node-vault';
const vault = new Client({
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
class TransitKeyManager {
constructor(
private keyName: string
) {}
async generateAPIKey(): Promise<string> {
const response = await vault.write({
path: `transit/keys/${this.keyName}/export`,
data: {
type: 'encryption-key',
version: 1
}
});
return response.data.keys[1]; // Return encryption key
}
async encryptAPIKey(apiKey: string): Promise<string> {
const response = await vault.write({
path: `transit/encrypt/${this.keyName}`,
data: {
plaintext: Buffer.from(apiKey).toString('base64'),
context: 'api-key-generation'
}
});
return response.data.ciphertext;
}
async decryptAPIKey(ciphertext: string): Promise<string> {
const response = await vault.write({
path: `transit/decrypt/${this.keyName}`,
data: {
ciphertext,
context: 'api-key-generation'
}
});
return Buffer.from(response.data.plaintext, 'base64').toString();
}
}
// Usage
const keyManager = new TransitKeyManager('payment-api');
// Generate and encrypt API key
const apiKey = generateRandomAPIKey();
const encryptedKey = await keyManager.encryptAPIKey(apiKey);
// Store encrypted key in database
await db.apiKeys.insert({
service: 'payment',
encryptedKey
});
// Decrypt when needed
const decryptedKey = await keyManager.decryptAPIKey(encryptedKey);Pattern 4: Secrets injection at runtime
Inject secrets directly into application containers at startup:
yaml# Kubernetes deployment with Vault Agent
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-service
spec:
replicas: 3
selector:
matchLabels:
app: orders-service
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "orders-service"
vault.hashicorp.com/secret-volume-path: "/vault/secrets"
vault.hashicorp.com/secret-volume-mode: "0600"
spec:
serviceAccountName: orders-service
containers:
- name: app
image: orders-service:latest
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: orders-db-credentials
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: orders-db-credentials
key: password
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
readOnly: true
volumes:
- name: vault-secrets
emptyDir:
medium: MemoryAccess control and audit
Role-based access control (RBAC)
Define who can access which secrets:
yaml# Vault policy for orders service
path "database/creds/orders-db" {
capabilities = ["read"]
}
path "database/creds/orders-db/*" {
capabilities = ["create", "update", "delete"]
}
path "sys/leases/renew" {
capabilities = ["update"]
}
path "sys/leases/revoke" {
capabilities = ["update"]
}
path "transit/encrypt/payment-api" {
capabilities = ["update"]
}
path "transit/decrypt/payment-api" {
capabilities = ["update"]
}typescript// AWS IAM policy for Secrets Manager access
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": [
"arn:aws:secretsmanager:us-east-1:123456789012:secret:orders-db-*"
],
"Condition": {
"StringEquals": {
"aws:PrincipalTag/Application": "orders-service"
}
}
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:RotateSecret"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/Role": "secrets-admin"
}
}
}
]
}Audit logging
Track all secret access and modifications:
typescriptinterface SecretAccessEvent {
timestamp: Date;
secretName: string;
operation: 'read' | 'write' | 'delete' | 'renew';
userIdentity: string;
ipAddress: string;
userAgent: string;
success: boolean;
}
async function logSecretAccess(event: SecretAccessEvent): Promise<void> {
// Log to audit database
await db.auditLogs.insert(event);
// Send to SIEM
await siem.send({
severity: event.success ? 'info' : 'warning',
event_type: 'secret_access',
...event
});
// Alert on suspicious activity
if (!event.success || isSuspiciousAccess(event)) {
await alerting.notify({
message: 'Suspicious secret access detected',
details: event
});
}
}
function isSuspiciousAccess(event: SecretAccessEvent): boolean {
// Alert on access from unusual IP
const recentAccesses = await db.auditLogs
.where('secretName', '=', event.secretName)
.where('timestamp', '>', new Date(Date.now() - 3600000))
.execute();
const uniqueIPs = new Set(recentAccesses.map(a => a.ipAddress));
if (!uniqueIPs.has(event.ipAddress)) {
return true; // New IP address
}
// Alert on excessive access frequency
const accessCount = recentAccesses.filter(
a => a.userIdentity === event.userIdentity
).length;
if (accessCount > 100) {
return true; // Excessive access
}
return false;
}Rotation strategies
Strategy 1: Automatic rotation with Vault
Configure automatic rotation for database credentials:
bash# Enable database secrets engine
vault secrets enable database
# Configure PostgreSQL database connection
vault write database/config/orders-db \
plugin_name="postgresql-database-plugin" \
connection_url="postgresql://{{username}}:{{password}}@orders-db:5432/orders" \
allowed_roles="orders-service" \
max_ttl="24h"
# Create role with rotation enabled
vault write database/roles/orders-service \
db_name="orders-db" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
default_ttl="1h" \
max_ttl="24h" \
rotation_period="24h"Strategy 2: AWS Secrets Manager rotation
Use AWS-managed rotation with Lambda:
typescript// Lambda function for secret rotation
import {
SecretsManagerClient,
RotateSecretCommand,
DescribeSecretCommand
} from '@aws-sdk/client-secrets-manager';
const secretsManager = new SecretsManagerClient({});
export const handler = async (event: any): Promise<void> => {
const secretId = event.SecretId;
// Step 1: Create a new secret version
const newCredentials = await createNewCredentials(secretId);
// Step 2: Set the new secret version
await secretsManager.send(new RotateSecretCommand({
SecretId: secretId,
ClientRequestToken: event.ClientRequestToken,
RotationStage: 'CREATING'
}));
// Step 3: Test the new credentials
await testCredentials(newCredentials);
// Step 4: Mark the new version as current
await secretsManager.send(new RotateSecretCommand({
SecretId: secretId,
ClientRequestToken: event.ClientRequestToken,
RotationStage: 'AWSCURRENT'
}));
console.log(`Secret ${secretId} rotated successfully`);
};Best practices
1. Never log secrets
typescript// Bad: Logging secrets
console.log(`Connecting to database with ${username}:${password}`);
// Good: Log only operation
console.log(`Connecting to database`);2. Use environment-specific secret naming
production/orders-db-credentials
staging/orders-db-credentials
development/orders-db-credentials3. Implement secret validation
typescriptasync function validateSecret(secret: AWSSecret): Promise<boolean> {
const client = new Client({
host: secret.host,
port: secret.port,
user: secret.username,
password: secret.password,
database: secret.dbname,
connectTimeoutMS: 5000
});
try {
await client.connect();
await client.end();
return true;
} catch (error) {
console.error(`Secret validation failed: ${error.message}`);
return false;
}
}4. Use short TTLs
- Database credentials: 1-24 hours
- API keys: 1-6 hours
- Service tokens: 15-30 minutes
Conclusion
Secrets management in 2026 is a mature discipline with multiple production-ready solutions. HashiCorp Vault provides the most comprehensive feature set for complex environments, while cloud-native options like AWS Secrets Manager offer simpler implementation for AWS-centric infrastructures.
The mature organization treats secrets as first-class infrastructure components with automatic rotation, centralized management, and comprehensive audit trails. Implementing proper secrets management is not optional—it is a security baseline for any production system.
Need to implement secrets management or audit your current secrets handling practices? Talk to Imperialis about secrets management architecture, platform selection, and production implementation.
Sources
- HashiCorp Vault Documentation — Official documentation
- AWS Secrets Manager Documentation — AWS documentation
- Azure Key Vault Documentation — Azure documentation
- Google Secret Manager Documentation — GCP documentation
- NIST SP 800-57: Key Management — Cryptographic key management