Cloud and platform

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.

3/15/20268 min readCloud
Secrets Management in Production: Vault, AWS Secrets Manager, and Modern Approaches

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

ApproachWhy it fails in production
Hardcoded secretsVisible in version control, shared across environments, cannot rotate
Environment variablesVisible in process listings, stored in unencrypted config, difficult to rotate
Config files in repoCommitted to version control, accessible to anyone with repo access
Shared database credentialsSingle compromise reveals all application credentials, no per-service isolation
Manual rotationHuman error prone, inconsistent timing, often skipped until incident

The modern security requirement

Production systems must satisfy these requirements:

  1. No static credentials – All credentials should be dynamically generated
  2. Short TTL – Credentials expire automatically (minutes to hours, not months)
  3. Automatic rotation – No manual intervention required
  4. Access logging – Every secret access is auditable
  5. Encryption – Secrets encrypted at rest and in transit
  6. Zero knowledge – Application developers should never see production secrets

Secrets management platforms

Platform comparison

PlatformStrengthsTrade-offs
HashiCorp VaultDynamic credentials, extensive integrations, open sourceComplex setup, operational overhead
AWS Secrets ManagerDeep AWS integration, automatic rotation, managed serviceAWS lock-in, limited dynamic credential types
Azure Key VaultAzure integration, FIPS 140-2 validatedAzure lock-in, learning curve
Google Secret ManagerCloud-native integration, IAM-based accessGCP lock-in, fewer advanced features
InfisicalOpen source, developer-friendly, secret scanningNewer 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: Memory

Access 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-credentials

3. 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

Related reading