Hardcoded secrets are a security incident waiting to happen. Modern secrets management means centralized storage, automatic rotation, audit logging, and dynamic credentials. This guide compares the two leading solutions and shows how to implement both.

The Problem with Secrets

What counts as a secret:

  • Database passwords
  • API keys and tokens
  • TLS certificates
  • SSH keys
  • Encryption keys
  • OAuth credentials

What happens when you hardcode them:

  • Git history exposes them forever
  • Rotation requires code deployments
  • No audit trail of access
  • Lateral movement after compromise

HashiCorp Vault

Vault is a dedicated secrets management platform with advanced features like dynamic secrets, encryption as a service, and identity-based access.

Architecture

┌─────────────────────────────────────────────────┐
│                    Vault                         │
├─────────────────────────────────────────────────┤
│  Auth Methods    │  Secrets Engines  │  Audit   │
│  - Token         │  - KV             │  - File  │
│  - Kubernetes    │  - Database       │  - Syslog│
│  - AWS IAM       │  - AWS            │  - Socket│
│  - LDAP          │  - PKI            │          │
└─────────────────────────────────────────────────┘

Installation (Kubernetes)

# Using Helm
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
  --set server.ha.enabled=true \
  --set server.ha.replicas=3 \
  --set injector.enabled=true

Static Secrets (KV)

# Enable KV secrets engine
vault secrets enable -path=secret kv-v2

# Store a secret
vault kv put secret/myapp/database \
    username="admin" \
    password="supersecret123"

# Read a secret
vault kv get secret/myapp/database

# Read specific field
vault kv get -field=password secret/myapp/database

Dynamic Database Credentials

# Enable database secrets engine
vault secrets enable database

# Configure PostgreSQL connection
vault write database/config/mydb \
    plugin_name=postgresql-database-plugin \
    connection_url="postgresql://{{username}}:{{password}}@db.example.com:5432/mydb" \
    allowed_roles="myapp-role" \
    username="vault-admin" \
    password="admin-password"

# Create role with TTL
vault write database/roles/myapp-role \
    db_name=mydb \
    creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
        GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
    default_ttl="1h" \
    max_ttl="24h"
# Application requests dynamic credentials
import hvac

client = hvac.Client(url='http://vault:8200', token=os.environ['VAULT_TOKEN'])
creds = client.secrets.database.generate_credentials('myapp-role')

db_user = creds['data']['username']
db_pass = creds['data']['password']
lease_id = creds['lease_id']
lease_duration = creds['lease_duration']  # 3600 seconds

# Credentials automatically expire after TTL
# Renew if needed:
client.sys.renew_lease(lease_id)

Kubernetes Authentication

# Configure Kubernetes auth
vault auth enable kubernetes

vault write auth/kubernetes/config \
    kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

# Create policy
vault policy write myapp - <<EOF
path "secret/data/myapp/*" {
  capabilities = ["read"]
}
path "database/creds/myapp-role" {
  capabilities = ["read"]
}
EOF

# Create role for service account
vault write auth/kubernetes/role/myapp \
    bound_service_account_names=myapp \
    bound_service_account_namespaces=production \
    policies=myapp \
    ttl=1h

Vault Agent Sidecar (Kubernetes)

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    metadata:
      annotations:
        # Enable Vault Agent injection
        vault.hashicorp.com/agent-inject: 'true'
        vault.hashicorp.com/role: 'myapp'
        
        # Inject database credentials
        vault.hashicorp.com/agent-inject-secret-db-creds: 'database/creds/myapp-role'
        vault.hashicorp.com/agent-inject-template-db-creds: |
          {{- with secret "database/creds/myapp-role" -}}
          export DB_USER="{{ .Data.username }}"
          export DB_PASS="{{ .Data.password }}"
          {{- end }}
        
        # Inject static secrets
        vault.hashicorp.com/agent-inject-secret-config: 'secret/data/myapp/config'
        vault.hashicorp.com/agent-inject-template-config: |
          {{- with secret "secret/data/myapp/config" -}}
          {
            "api_key": "{{ .Data.data.api_key }}",
            "secret_key": "{{ .Data.data.secret_key }}"
          }
          {{- end }}
    spec:
      serviceAccountName: myapp
      containers:
        - name: myapp
          image: myorg/myapp:latest
          command: ["/bin/sh", "-c"]
          args:
            - |
              source /vault/secrets/db-creds
              /app/start.sh

AWS Secrets Manager

AWS Secrets Manager is a fully managed service tightly integrated with AWS services. Simpler than Vault but less flexible.

Creating Secrets

# Using boto3
import boto3
import json

client = boto3.client('secretsmanager')

# Create secret
client.create_secret(
    Name='prod/myapp/database',
    SecretString=json.dumps({
        'username': 'admin',
        'password': 'supersecret123',
        'host': 'db.example.com',
        'port': 5432
    }),
    Tags=[
        {'Key': 'Environment', 'Value': 'production'},
        {'Key': 'Application', 'Value': 'myapp'}
    ]
)

Terraform

# Create secret
resource "aws_secretsmanager_secret" "database" {
  name        = "prod/myapp/database"
  description = "Database credentials for myapp"

  tags = {
    Environment = "production"
  }
}

resource "aws_secretsmanager_secret_version" "database" {
  secret_id = aws_secretsmanager_secret.database.id
  secret_string = jsonencode({
    username = "admin"
    password = random_password.db_password.result
    host     = aws_db_instance.main.endpoint
    port     = 5432
  })
}

# Auto-rotation for RDS
resource "aws_secretsmanager_secret_rotation" "database" {
  secret_id           = aws_secretsmanager_secret.database.id
  rotation_lambda_arn = aws_lambda_function.rotate_secret.arn

  rotation_rules {
    automatically_after_days = 30
  }
}

Retrieving Secrets

import boto3
import json
from functools import lru_cache

@lru_cache(maxsize=100)
def get_secret(secret_name: str) -> dict:
    """Retrieve and cache secret from Secrets Manager"""
    client = boto3.client('secretsmanager')
    response = client.get_secret_value(SecretId=secret_name)
    return json.loads(response['SecretString'])

# Usage
db_creds = get_secret('prod/myapp/database')
connection = psycopg2.connect(
    host=db_creds['host'],
    user=db_creds['username'],
    password=db_creds['password'],
    database='myapp'
)

Lambda Integration

# Lambda function with Secrets Manager
import json
import boto3
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities import parameters

logger = Logger()

# Automatically caches and refreshes
@parameters.secrets_provider()
def handler(event, context):
    # Get secret (cached for 5 minutes by default)
    db_secret = parameters.get_secret('prod/myapp/database', transform='json')
    
    logger.info(f"Connecting to {db_secret['host']}")
    # Use credentials...

ECS/EKS Integration

# ECS Task Definition
{
  "containerDefinitions": [
    {
      "name": "myapp",
      "image": "myorg/myapp:latest",
      "secrets": [
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/myapp/database:password::"
        },
        {
          "name": "API_KEY", 
          "valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:prod/myapp/api-keys:primary::"
        }
      ]
    }
  ],
  "executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole"
}
# Kubernetes with External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: myapp-secrets
    template:
      type: Opaque
      data:
        DB_HOST: "{{ .db_host }}"
        DB_USER: "{{ .db_user }}"
        DB_PASS: "{{ .db_pass }}"
  data:
    - secretKey: db_host
      remoteRef:
        key: prod/myapp/database
        property: host
    - secretKey: db_user
      remoteRef:
        key: prod/myapp/database
        property: username
    - secretKey: db_pass
      remoteRef:
        key: prod/myapp/database
        property: password

Comparison

FeatureVaultAWS Secrets Manager
HostingSelf-managed or HCPFully managed
Dynamic secrets✅ Full support❌ Limited (RDS only)
Secret rotation✅ Any secret⚠️ Limited types
Auth methodsMany (K8s, LDAP, AWS, etc.)IAM only
Multi-cloud❌ AWS only
PKI/Certificates✅ Built-in❌ Use ACM
Encryption as service✅ Transit engine❌ Use KMS
CostInfrastructure + license$0.40/secret/month
ComplexityHighLow

When to Use What

Use AWS Secrets Manager when:

  • All-in on AWS
  • Simple static secrets
  • RDS credential rotation
  • Minimal operations overhead
  • Lambda/ECS native integration

Use HashiCorp Vault when:

  • Multi-cloud or hybrid
  • Dynamic credentials needed
  • PKI/certificate management
  • Complex access policies
  • Encryption as a service
  • Compliance requirements (SOC2, HIPAA)

Security Best Practices

1. Never Log Secrets

# ❌ Don't do this
logger.info(f"Connecting with password: {password}")

# ✅ Do this
logger.info(f"Connecting to {host} as {username}")

2. Use Short TTLs

# Vault: Short-lived credentials
vault write database/roles/myapp-role \
    default_ttl="1h" \
    max_ttl="24h"

3. Audit Everything

# Vault audit logging
vault audit enable file file_path=/var/log/vault/audit.log

4. Rotate Regularly

# AWS: Automatic rotation
rotation_rules {
  automatically_after_days = 30
}

5. Least Privilege Access

# Vault policy - read only specific path
path "secret/data/myapp/*" {
  capabilities = ["read"]
}

# Deny list capability
path "secret/metadata/*" {
  capabilities = ["deny"]
}

6. Use Environment-Specific Secrets

secret/
├── dev/
│   └── myapp/
├── staging/
│   └── myapp/
└── prod/
    └── myapp/

Key Takeaways

  1. Never hardcode secrets — use a secrets manager
  2. Dynamic credentials > static — they can’t be stolen if they expire
  3. Rotate regularly — assume compromise, minimize blast radius
  4. Audit access — know who accessed what and when
  5. Least privilege — applications only get what they need
  6. Start simple — AWS Secrets Manager is fine for most cases
  7. Graduate to Vault — when you need dynamic secrets or multi-cloud

“The best secret is one that doesn’t exist. The second best is one that expires before an attacker can use it.”