Introduction


Managing database credentials, API keys, and TLS certificates is one of the most critical security challenges in modern infrastructure. Hard-coded secrets in configuration files or environment variables are a leading cause of data breaches. Dedicated secret management tools provide encryption, access control, rotation, and audit trails. This article compares HashiCorp Vault, AWS Secrets Manager, and Doppler across the dimensions that matter in production.


HashiCorp Vault


Vault offers the most comprehensive feature set with dynamic secrets, encryption-as-a-service, and multi-cloud support:



# Vault configuration

storage "raft" {

  path = "/vault/data"

  node_id = "node1"

}



listener "tcp" {

  address     = "0.0.0.0:8200"

  tls_disable = false

  tls_cert_file = "/vault/certs/cert.pem"

  tls_key_file  = "/vault/certs/key.pem"

}



seal "awskms" {

  region     = "us-east-1"

  kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/abc123"

}



api_addr = "https://vault.example.com:8200"

cluster_addr = "https://vault.example.com:8201"


Dynamic Database Secrets


Vault can generate temporary database credentials on demand, eliminating long-lived credentials:



# Configure database backend

path "database/creds/my-role" {

  capabilities = ["read"]

}



# Generate ephemeral PostgreSQL credentials

vault read database/creds/payment-app



# Key                Value

# ---                -----

# lease_id           database/creds/payment-app/abc123

# lease_duration     1h

# lease_renewable    true

# password           aB3x...kL9p

# username           v-token-payment-app-x7Yz...


Application integration:



// Vault sidecar pattern

package main



import (

    "github.com/hashicorp/vault/api"

)



type DynamicCredentials struct {

    client *api.Client

}



func (d *DynamicCredentials) GetCredentials() (string, string, error) {

    secret, err := d.client.Logical().Read("database/creds/payment-app")

    if err != nil {

        return "", "", err

    }

    username := secret.Data["username"].(string)

    password := secret.Data["password"].(string)



    // Schedule renewal before lease expires

    go d.renewLease(secret.LeaseDuration)

    return username, password, nil

}



func (d *DynamicCredentials) renewLease(duration int) {

    // Renew at 50% of lease duration

    time.Sleep(time.Duration(duration/2) * time.Second)

    d.client.Sys().Renew("database/creds/payment-app", duration)

}


AWS Secrets Manager


Secrets Manager integrates natively with the AWS ecosystem:



# CloudFormation: Create a secret with rotation

Resources:

  DatabaseSecret:

    Type: AWS::SecretsManager::Secret

    Properties:

      Name: payment/db-credentials

      Description: "Payment database credentials"

      GenerateSecretString:

        SecretStringTemplate: '{"username": "payment_app"}'

        GenerateStringKey: "password"

        PasswordLength: 32

        ExcludeCharacters: "@%*"

      RotationSchedule:

        RotationLambdaARN: !GetAtt RotationLambda.Arn

        RotationSchedule:

          Duration: "7d"

      Tags:

        - Key: Environment

          Value: Production


Application retrieval using the SDK:



import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";



const client = new SecretsManagerClient({ region: "us-east-1" });



async function getDbConfig(): Promise<DbConfig> {

    const response = await client.send(new GetSecretValueCommand({

        SecretId: "payment/db-credentials",

    }));



    const secret = JSON.parse(response.SecretString!);

    return {

        host: process.env.DB_HOST,

        username: secret.username,

        password: secret.password,

        database: process.env.DB_NAME,

    };

}


Doppler


Doppler provides a developer-friendly approach with workspace-based secret management:



# CLI workflow

doppler setup --project payment-service --config prd



# Fetch secrets locally

doppler secrets substitute < config.yaml > config.resolved.yaml



# Run an application with secrets injected

doppler run -- npm start



# doppler.yaml for project

setup:

  project: payment-service

  configs:

    - dev

    - stg

    - prd



environments:

  - name: dev

    secrets:

      DB_CONNECTION_STRING: postgres://dev_user:dev_pass@localhost:5432/payment

      STRIPE_API_KEY: sk_test_***

      JWT_SECRET: dev-secret-key



  - name: prd

    secrets:

      DB_CONNECTION_STRING: doppler://payment-service/prd/db_connection_string

      STRIPE_API_KEY: doppler://payment-service/prd/stripe_api_key


CI/CD integration:



# GitHub Actions with Doppler

name: Deploy

on:

  push:

    branches: [main]

jobs:

  deploy:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: dopplerhq/cli-action@v3

      - name: Build with secrets

        run: |

          doppler secrets substitute --project payment-service --config prd \

            --token ${{ secrets.DOPPLER_TOKEN }} \

            < docker-compose.yml > docker-compose.resolved.yml

      - name: Deploy

        run: docker compose -f docker-compose.resolved.yml up -d


Secret Rotation


| Tool | Rotation Method | Automation | Audit Trail |

|---|---|---|---|

| Vault | Dynamic secrets (auto-expire) | Built-in via leases | Audit device |

| AWS Secrets Manager | Lambda-based rotation | Scheduled rotation | CloudTrail |

| Doppler | API key re-generation | Manual + API | Change log |


Vault's dynamic secrets are the gold standard because credentials automatically expire, eliminating the need for rotation entirely. AWS Secrets Manager supports automated rotation but requires custom Lambda functions. Doppler relies on manual intervention or API-driven rotation.


Encryption and Architecture



# Vault: Enterprise auto-unseal with AWS KMS

seal "awskms" {

  region     = "us-east-1"

  kms_key_id = "arn:aws:kms:...:key/abc123"

}



# How a high-availability Vault cluster works:

# 1. Vault starts in sealed state

# 2. Unseal key split across 5 keys (3 quorum required)

# 3. OR: auto-unseal via cloud KMS

# 4. Vault encrypts data with AES-256-GCM

# 5. Encryption key wrapped by master key

# 6. Master key wrapped by unseal key


Disaster Recovery



# Vault: DR replication across regions

path "sys/replication/dr/primary/enable" {

  capabilities = ["update", "sudo"]

}



# Perform DR failover

# vault operator raft snapshot restore snapshot.snap

# vault operator unseal


AWS Secrets Manager and Doppler benefit from the cloud provider's redundancy naturally. Vault requires explicit replication configuration for multi-region availability.


Integration Patterns


| Pattern | Vault | AWS Secrets Manager | Doppler |

|---|---|---|---|

| Sidecar injector | Vault Agent Injector (K8s) | ASCP (plugin) | CLI wrapper |

| SDK access | Go, Python, Java, Node | All AWS SDKs | Any (CLI) |

| Kubernetes | CSI driver, Injector | Secrets Store CSI | Doppler K8s Operator |


Decision Guide


  • **HashiCorp Vault**: Best for multi-cloud, on-premise, or regulated environments requiring dynamic secrets and encryption-as-a-service. Highest operational overhead.
  • **AWS Secrets Manager**: Best for AWS-native workloads where simplicity and managed rotation are priorities. Limited outside AWS.
  • **Doppler**: Best for developer-focused teams wanting a simple, cross-platform secret management solution with minimal operational overhead.

  • Start with a tool that matches your current scale. Vault's complexity is only justified when you need its dynamic secret capabilities or operate across multiple cloud providers.