Introduction: Secrets Sprawl Is a Ticking Time Bomb

Hardcoded credentials remain one of the most exploited attack vectors in cloud environments, and the numbers are getting worse. GitGuardian’s 2025 State of Secrets Sprawl report found 23.8 million new hardcoded secrets pushed to public GitHub repositories in 2024 alone. Worse, 70% of secrets leaked in 2022 are still valid today, giving attackers prolonged access to critical systems. An analysis of 15 million public Docker images uncovered 100,000 valid secrets – including AWS keys and GitHub tokens from Fortune 500 companies.

The median time to remediate a leaked secret on GitHub? 94 days. That is three months of exposure before anyone rotates a credential. Stolen credentials appeared in 31% of all breaches over the past decade, with an average incident cost of $3.7 million.

The solution is disciplined secrets management – but which tool do you reach for? If you are running on AWS, you have two native options that work out of the box: SSM Parameter Store (free) and Secrets Manager ($0.40/secret/month). If you need multi-cloud support, dynamic credentials, or fine-grained audit logging, HashiCorp Vault and its open source fork OpenBao provide capabilities that go far beyond what AWS offers natively. And for lightweight GitOps workflows, SOPS + age encrypts secrets right in your repository.

This article walks through each tool, compares them head to head, and gives you a decision framework so you pick the right one for your environment.

Secrets Management Architecture Application secrets flow through AWS native services or Vault to backend resources


AWS SSM Parameter Store: Free, Simple, and Sufficient for Most Teams

AWS Systems Manager Parameter Store is the unsung hero of secrets management on AWS. It stores configuration data and secrets as key-value pairs, encrypts them with KMS, and costs exactly nothing for standard parameters.

Why SSM Parameter Store Works

  • Free tier: Up to 10,000 standard parameters per account per region at no charge
  • SecureString type: Encrypts values with AWS KMS (default aws/ssm key or your own CMK)
  • Hierarchical paths: Organize secrets like /myapp/prod/database/password
  • IAM integration: Control access with standard IAM policies
  • Parameter policies: Set expiration dates and notification windows
  • Cross-service references: Lambda, ECS, CloudFormation, and CodePipeline all read SSM natively

SSM Parameter Store in Practice

Store a secret:

1
2
3
4
5
6
aws ssm put-parameter \
  --name "/myapp/prod/database/password" \
  --value "s3cureP@ss!" \
  --type "SecureString" \
  --tier "Standard" \
  --tags Key=Application,Value=myapp Key=Environment,Value=prod

Retrieve it in Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
import boto3

def get_secret(param_name: str) -> str:
    """Retrieve a SecureString parameter from SSM."""
    ssm = boto3.client("ssm")
    response = ssm.get_parameter(
        Name=param_name,
        WithDecryption=True
    )
    return response["Parameter"]["Value"]

# Usage
db_password = get_secret("/myapp/prod/database/password")

Retrieve multiple parameters by path:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_secrets_by_path(path: str) -> dict:
    """Retrieve all parameters under a path prefix."""
    ssm = boto3.client("ssm")
    params = {}
    paginator = ssm.get_paginator("get_parameters_by_path")

    for page in paginator.paginate(
        Path=path,
        Recursive=True,
        WithDecryption=True
    ):
        for param in page["Parameters"]:
            key = param["Name"].split("/")[-1]
            params[key] = param["Value"]

    return params

# Retrieve all secrets for an app environment
config = get_secrets_by_path("/myapp/prod/")

Limitations

  • No built-in rotation: You must build your own rotation logic or use Secrets Manager
  • No dynamic credentials: Parameters are static values; you set them, you rotate them manually
  • API throttling: Standard throughput is 40 TPS for GetParameter; advanced tier bumps it to 1,000 TPS ($0.05/10,000 advanced API interactions)
  • No cross-account sharing: Each account manages its own parameters (use AWS Organizations + resource policies for workarounds)
  • Limited audit: CloudTrail logs API calls, but there is no built-in audit trail for secret access patterns

AWS Secrets Manager: Rotation, Integration, and a Price Tag

Secrets Manager is the premium offering. It costs more, but it brings automatic rotation and native database integration that SSM Parameter Store does not have.

Key Features

  • Automatic rotation: Lambda-based rotation for RDS, Redshift, DocumentDB, and custom secrets
  • RDS integration: Native credential rotation without application downtime
  • Cross-account sharing: Resource-based policies enable sharing secrets across accounts
  • Versioning: Automatic version tracking with staging labels (AWSCURRENT, AWSPREVIOUS)
  • Replication: Cross-region replication for disaster recovery

Pricing (2026)

Component Cost
Per secret per month $0.40
Per 10,000 API calls $0.05
Rotation Lambda Standard Lambda pricing
KMS encryption $1.00/month per CMK

For 100 secrets with moderate access patterns, expect roughly $40-50/month.

Secrets Manager with Terraform and Rotation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# Secrets Manager secret with automatic RDS rotation
resource "aws_secretsmanager_secret" "db_credentials" {
  name        = "myapp/prod/rds-credentials"
  description = "RDS credentials for myapp production"
  kms_key_id  = aws_kms_key.secrets.arn

  tags = {
    Application = "myapp"
    Environment = "prod"
    Costcenter  = "engineering"
    Owner       = "platform-team"
    Customer    = "internal"
  }
}

resource "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = aws_secretsmanager_secret.db_credentials.id
  secret_string = jsonencode({
    username = "app_user"
    password = random_password.db.result
    engine   = "postgres"
    host     = aws_db_instance.main.address
    port     = 5432
    dbname   = "myapp"
  })
}

resource "aws_secretsmanager_secret_rotation" "db_credentials" {
  secret_id           = aws_secretsmanager_secret.db_credentials.id
  rotation_lambda_arn = aws_lambda_function.rotation.arn

  rotation_rules {
    automatically_after_days = 30
  }
}

resource "random_password" "db" {
  length  = 32
  special = true
}

When to Choose Secrets Manager over SSM

Pick Secrets Manager when you need:

  1. Automatic rotation – especially for RDS, Redshift, or DocumentDB credentials
  2. Cross-account secret sharing – resource policies make this straightforward
  3. Cross-region replication – for DR or multi-region deployments
  4. Compliance requirements – some auditors specifically want a dedicated secrets management service

If none of those apply, SSM Parameter Store with SecureString is probably enough.


HashiCorp Vault: Dynamic Secrets, Multi-Cloud, and Full Audit

Vault is a different class of tool. While SSM and Secrets Manager store and retrieve static secrets, Vault generates credentials on demand, enforces fine-grained policies, and provides a complete audit log of every secret access.

Core Capabilities

  • Dynamic secrets: Generate short-lived database credentials, AWS IAM credentials, TLS certificates on the fly
  • Auth methods: AWS IAM, Kubernetes, OIDC, LDAP, AppRole, and more
  • Policy engine: Path-based ACL policies with templating
  • Audit logging: Every request and response logged to file, syslog, or socket
  • Secret engines: KV, database, PKI, SSH, transit encryption, TOTP, and dozens more
  • Multi-cloud: Single control plane for AWS, GCP, Azure, and on-premises

Vault Server with Docker Compose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
services:
  vault:
    image: hashicorp/vault:1.18
    container_name: vault
    restart: unless-stopped
    ports:
      - "8200:8200"
    environment:
      VAULT_ADDR: "http://0.0.0.0:8200"
      VAULT_API_ADDR: "http://0.0.0.0:8200"
    cap_add:
      - IPC_LOCK
    volumes:
      - vault-data:/vault/data
      - ./vault-config:/vault/config
    command: server
    networks:
      - vault-network
      - backend
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.vault.entrypoints=http"
      - "traefik.http.routers.vault.rule=Host(`vault.local.team-skynet.io`)"
      - "traefik.http.routers.vault.middlewares=https-redirectscheme@file"
      - "traefik.http.routers.vault-secure.entrypoints=https"
      - "traefik.http.routers.vault-secure.rule=Host(`vault.local.team-skynet.io`)"
      - "traefik.http.routers.vault-secure.tls=true"
      - "traefik.http.routers.vault-secure.service=vault"
      - "traefik.docker.network=backend"
      - "traefik.http.services.vault.loadbalancer.server.port=8200"

volumes:
  vault-data:

networks:
  vault-network:
    driver: bridge
    name: vault-network
  backend:
    external: true
    name: backend

Vault server configuration (vault-config/vault.hcl):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
storage "raft" {
  path    = "/vault/data"
  node_id = "vault-node-1"
}

listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = 1  # TLS handled by Traefik
}

api_addr     = "http://vault:8200"
cluster_addr = "http://vault:8201"
ui           = true

# Use AWS KMS for auto-unseal in production
# seal "awskms" {
#   region     = "us-east-1"
#   kms_key_id = "your-kms-key-id"
# }

Vault Policies

Policies control who can access what. They are path-based and support templating:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# policy: app-readonly.hcl
# Applications can read secrets under their own path
path "secret/data//*" {
  capabilities = ["read", "list"]
}

# Applications can request dynamic database credentials
path "database/creds/" {
  capabilities = ["read"]
}

# Deny access to other apps' secrets
path "secret/data/*" {
  capabilities = ["deny"]
}

Apply the policy:

1
vault policy write app-readonly app-readonly.hcl

AWS Auth Backend

Let your AWS workloads authenticate to Vault using their IAM identity – no passwords needed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Enable AWS auth method
vault auth enable aws

# Configure the AWS auth backend
vault write auth/aws/config/client \
  secret_key="VAULT_AWS_SECRET_KEY" \
  access_key="VAULT_AWS_ACCESS_KEY"

# Create a role for EC2 instances
vault write auth/aws/role/app-server \
  auth_type=iam \
  bound_iam_principal_arn="arn:aws:iam::123456789012:role/app-server-role" \
  policies=app-readonly \
  token_ttl=1h \
  token_max_ttl=4h

# Create a role for Lambda functions
vault write auth/aws/role/lambda-worker \
  auth_type=iam \
  bound_iam_principal_arn="arn:aws:iam::123456789012:role/lambda-execution-role" \
  policies=app-readonly \
  token_ttl=15m \
  token_max_ttl=1h

Dynamic Database Credentials

This is where Vault truly shines. Instead of storing a static database password, Vault creates a unique, short-lived credential for each request:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Enable the database secrets engine
vault secrets enable database

# Configure PostgreSQL connection
vault write database/config/myapp-db \
  plugin_name=postgresql-database-plugin \
  allowed_roles="app-readonly,app-readwrite" \
  connection_url="postgresql://:@db.example.com:5432/myapp?sslmode=require" \
  username="vault_admin" \
  password="vault_admin_password"

# Create a role that generates read-only credentials
vault write database/roles/app-readonly \
  db_name=myapp-db \
  creation_statements="CREATE ROLE \"\" WITH LOGIN PASSWORD '' VALID UNTIL ''; \
    GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"\";" \
  revocation_statements="DROP ROLE IF EXISTS \"\";" \
  default_ttl=1h \
  max_ttl=24h

Now any authenticated application can request fresh credentials:

1
2
3
4
5
6
7
8
9
10
# Request dynamic credentials
vault read database/creds/app-readonly

# Output:
# Key                Value
# ---                -----
# lease_id           database/creds/app-readonly/abcd1234
# lease_duration     1h
# username           v-app-readonly-xyz789
# password           A1b2C3d4E5f6G7h8

The credentials are unique per request, automatically revoked after the TTL expires, and every access is logged in Vault’s audit trail. If a credential is compromised, you know exactly which service instance was affected.

KMS Auto-Unseal

In production, use AWS KMS to auto-unseal Vault instead of manual Shamir keys:

1
2
3
4
5
# vault-config/vault.hcl
seal "awskms" {
  region     = "us-east-1"
  kms_key_id = "alias/vault-unseal-key"
}

Required IAM permissions for the Vault server:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"
    }
  ]
}

OpenBao: The Open Source Fork After Vault’s License Change

In August 2023, HashiCorp changed Vault’s license from Mozilla Public License (MPL) 2.0 to the Business Source License (BSL) 1.1. The BSL permits internal and personal use, but prohibits hosting or embedding Vault to compete with HashiCorp’s commercial offerings. If you are running Vault internally, the BSL is fine. If you are building a managed service on top of Vault, it is not.

The community responded by forking Vault’s last MPL-licensed version into OpenBao, managed by the Linux Foundation. As of February 2026, OpenBao has released version 2.5.0, has 5,400+ GitHub stars, and has attracted corporate backing from GitLab (which plans to ship OpenBao as a built-in component).

What OpenBao Adds Over Community Vault

OpenBao includes features that were previously exclusive to Vault Enterprise:

  • Namespaces: Multi-tenancy and workload isolation
  • Horizontal read scalability: HA standby nodes can serve read requests (added in 2.5.0)
  • MPL 2.0 license: True open source with no usage restrictions

When to Consider OpenBao

  • You want Enterprise-grade features without the Enterprise price tag
  • You are building a platform or managed service that the BSL would restrict
  • You want to contribute to the project without CLA restrictions
  • You need namespaces for multi-tenant environments

The trade-off is maturity. OpenBao’s plugin ecosystem and contributor base are still growing, and some Vault plugins may not be compatible yet. For teams already running Vault OSS internally under the BSL, there is no urgent reason to switch – but OpenBao is worth watching.


SOPS + age: Lightweight Encryption for GitOps

Not every team needs a centralized secrets server. If you practice GitOps and want to store encrypted secrets alongside your Kubernetes manifests, Terraform variables, or Helm values, SOPS (Secrets OPerationS) with age encryption is a lightweight alternative.

How It Works

SOPS encrypts the values in structured files (YAML, JSON, ENV, INI) while leaving the keys in plaintext. This means you can review diffs, see which secrets changed, and store them in Git – without exposing the actual values.

age is a modern encryption tool that replaces GPG with simpler key management. No key servers, no web of trust, no expired subkeys.

Setup

1
2
3
4
5
6
7
# Install SOPS and age
brew install sops age

# Generate an age key pair
age-keygen -o ~/.config/sops/age/keys.txt

# The public key looks like: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Encrypting Secrets

Create a .sops.yaml configuration in your repo root:

1
2
3
4
5
6
7
8
creation_rules:
  - path_regex: .*\.secrets\.yaml$
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p,
      age1another_team_member_public_key_here
  - path_regex: .*\.secrets\.json$
    age: >-
      age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

Create a secrets file:

1
2
3
4
5
6
7
# secrets.secrets.yaml (before encryption)
database:
  password: s3cureP@ss!
  connection_string: postgresql://user:s3cureP@ss!@db.example.com:5432/myapp
api_keys:
  stripe: sk_live_abc123
  sendgrid: SG.xyz789

Encrypt it:

1
sops --encrypt --in-place secrets.secrets.yaml

After encryption, the file looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
database:
  password: ENC[AES256_GCM,data:abc123...,iv:...,tag:...,type:str]
  connection_string: ENC[AES256_GCM,data:def456...,iv:...,tag:...,type:str]
api_keys:
  stripe: ENC[AES256_GCM,data:ghi789...,iv:...,tag:...,type:str]
  sendgrid: ENC[AES256_GCM,data:jkl012...,iv:...,tag:...,type:str]
sops:
  age:
    - recipient: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        ...
        -----END AGE ENCRYPTED FILE-----

Keys are visible; values are encrypted. You can commit this safely to Git.

SOPS with AWS KMS

For teams on AWS, SOPS also supports KMS as a key provider, giving you centralized key management without running Vault:

1
2
3
4
# .sops.yaml with AWS KMS
creation_rules:
  - path_regex: .*\.secrets\.yaml$
    kms: "arn:aws:kms:us-east-1:123456789012:key/your-kms-key-id"

When SOPS + age Makes Sense

  • Small teams with GitOps workflows
  • Kubernetes secrets stored alongside manifests (Flux, ArgoCD integration)
  • Terraform variable files that need to be versioned
  • You do not want to run or pay for a secrets management server
  • Development and staging environments where simplicity matters

Comparison Table

Capability SSM Parameter Store Secrets Manager Vault / OpenBao SOPS + age
Cost Free (standard) $0.40/secret/month Self-hosted (compute cost) Free
Dynamic secrets No No Yes No
Automatic rotation No Yes (Lambda-based) Yes (built-in leases) No
Multi-cloud AWS only AWS only Yes Yes (client-side)
Audit logging CloudTrail CloudTrail Built-in detailed audit Git history
Auth methods IAM IAM IAM, K8s, OIDC, LDAP, etc. Key-based
Encryption KMS KMS Transit engine + storage age, GPG, KMS
Complexity Low Low High Low
High availability Built-in (AWS managed) Built-in (AWS managed) Raft/Consul (self-managed) N/A
Cross-account Limited Yes (resource policies) Yes (namespaces) N/A
GitOps friendly No No No Yes
License Proprietary (AWS) Proprietary (AWS) BSL 1.1 / MPL 2.0 (OpenBao) MPL 2.0

Decision Framework: When to Use Which

Use this flowchart to pick the right tool for your situation:

Start here: Are you AWS-only with fewer than 100 secrets?

  • Yes –> Do you need automatic rotation?
    • Yes –> AWS Secrets Manager (native rotation, low operational overhead)
    • No –> SSM Parameter Store (free, simple, sufficient)
  • No –> Continue below

Do you need dynamic, short-lived credentials?

  • Yes –> HashiCorp Vault or OpenBao (dynamic secrets engine is unique to Vault)
  • No –> Continue below

Are you multi-cloud or hybrid?

  • Yes –> Vault / OpenBao (single control plane across clouds)
  • No –> Continue below

Do you practice GitOps and want secrets in version control?

  • Yes –> SOPS + age (or SOPS + KMS for AWS-native key management)
  • No –> SSM Parameter Store or Secrets Manager based on rotation needs

The Hybrid Approach

Many mature organizations use a combination:

  • SSM Parameter Store for application configuration and low-sensitivity secrets
  • Secrets Manager for RDS credentials that need automatic rotation
  • Vault for dynamic database credentials, PKI certificates, and multi-cloud secrets
  • SOPS for secrets that live in Git alongside infrastructure code

These tools are not mutually exclusive. Vault’s AWS auth backend means it integrates seamlessly with IAM, and SOPS can use KMS as its encryption provider.


Real-World Scenarios

Scenario 1: Database Credentials for a Microservices Fleet

Problem: 20 microservices need PostgreSQL access. Static credentials shared across services make it impossible to trace which service performed a questionable query.

Solution: Vault dynamic database credentials.

Each service authenticates to Vault via its IAM role and receives unique, short-lived credentials:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import hvac
import boto3

def get_dynamic_db_creds(vault_addr: str, vault_role: str) -> dict:
    """Authenticate to Vault with AWS IAM and get dynamic DB credentials."""
    # Get AWS credentials for Vault auth
    session = boto3.Session()
    credentials = session.get_credentials().get_frozen_credentials()

    client = hvac.Client(url=vault_addr)

    # Authenticate with AWS IAM
    client.auth.aws.iam_login(
        access_key=credentials.access_key,
        secret_key=credentials.secret_key,
        session_token=credentials.token,
        role=vault_role
    )

    # Request dynamic database credentials
    creds = client.secrets.database.generate_credentials(
        name="app-readonly"
    )

    return {
        "username": creds["data"]["username"],
        "password": creds["data"]["password"],
        "lease_id": creds["lease_id"],
        "lease_duration": creds["lease_duration"]
    }

Result: Every database connection is traceable to a specific service instance. Credentials expire automatically. If a service is compromised, revoke its Vault lease and the credentials are immediately invalid.

Scenario 2: API Keys for a SaaS Integration

Problem: Third-party API keys (Stripe, SendGrid, Datadog) need to be available to Lambda functions.

Solution: SSM Parameter Store with SecureString.

1
2
3
4
5
6
7
8
9
10
11
12
# Store API keys
aws ssm put-parameter \
  --name "/myapp/prod/stripe/api-key" \
  --value "sk_live_abc123" \
  --type "SecureString" \
  --tier "Standard"

aws ssm put-parameter \
  --name "/myapp/prod/sendgrid/api-key" \
  --value "SG.xyz789" \
  --type "SecureString" \
  --tier "Standard"

Reference them in Lambda via CloudFormation:

1
2
3
4
5
6
MyFunction:
  Type: AWS::Serverless::Function
  Properties:
    Environment:
      Variables:
        STRIPE_API_KEY: !Sub ""

Result: Free, simple, and integrated. No servers to manage. IAM controls who can access which parameters.

Scenario 3: TLS Certificates for Internal Services

Problem: Internal microservices need mutual TLS (mTLS) certificates that rotate frequently.

Solution: Vault PKI secrets engine.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Enable PKI and configure a root CA
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki

vault write pki/root/generate/internal \
  common_name="internal.example.com" \
  ttl=87600h

# Enable an intermediate CA
vault secrets enable -path=pki_int pki
vault secrets tune -max-lease-ttl=43800h pki_int

# Create a role for issuing certificates
vault write pki_int/roles/internal-service \
  allowed_domains="internal.example.com" \
  allow_subdomains=true \
  max_ttl=72h

Services request certificates dynamically:

1
2
3
vault write pki_int/issue/internal-service \
  common_name="myservice.internal.example.com" \
  ttl=24h

Result: Short-lived certificates, automatic rotation, and a full audit trail. No certificate sprawl, no expired certs causing outages.


Migration Strategies

Moving from Hardcoded Secrets to SSM Parameter Store

  1. Audit: Scan your codebase for hardcoded secrets using tools like gitleaks or trufflehog
  2. Inventory: Document every secret, its current location, and which services use it
  3. Store: Create SSM parameters following the /{app}/{env}/{secret} naming convention
  4. Update: Modify application code to read from SSM at startup or runtime
  5. Remove: Delete hardcoded secrets from source code and rotate all compromised values
  6. Verify: Confirm applications function correctly with SSM-sourced secrets
  7. Enforce: Add pre-commit hooks with gitleaks to prevent future leaks

Moving from SSM Parameter Store to Vault

  1. Deploy Vault: Set up Vault with Raft storage and KMS auto-unseal
  2. Configure auth: Enable the AWS auth backend and create roles for your workloads
  3. Migrate static secrets: Export from SSM and import into Vault’s KV engine
  4. Enable dynamic secrets: Configure database and other secret engines
  5. Update applications: Replace SSM SDK calls with Vault client calls
  6. Parallel run: Keep SSM as a fallback while validating Vault
  7. Cut over: Remove SSM dependencies once Vault is proven stable

Gradual Adoption: Vault Alongside AWS Native

You do not have to migrate everything at once. A practical approach:

  • Phase 1: Keep API keys and static config in SSM Parameter Store
  • Phase 2: Move database credentials to Vault dynamic secrets
  • Phase 3: Add Vault PKI for internal TLS certificates
  • Phase 4: Migrate remaining secrets as teams gain Vault operational experience

Security Hardening Checklist

Regardless of which tool you choose, follow these baseline practices:

  • Encrypt at rest: KMS for AWS services, Vault’s storage encryption, age for files
  • Encrypt in transit: TLS everywhere, including between Vault and its storage backend
  • Least privilege: IAM policies scoped to specific parameter paths or Vault policies scoped to specific secret paths
  • Audit logging: Enable CloudTrail for AWS services, Vault audit backend for Vault
  • Rotation: Automate credential rotation – 30 days for static, hours for dynamic
  • No hardcoded secrets: Pre-commit hooks with gitleaks or trufflehog
  • Disaster recovery: Back up Vault’s Raft snapshots, replicate Secrets Manager cross-region
  • Access reviews: Quarterly review of who and what can access each secret

Conclusion

There is no single “best” secrets management tool. The right choice depends on your environment, your team’s operational maturity, and your specific requirements:

  • SSM Parameter Store is the right starting point for most AWS-native teams. It is free, integrated, and handles 80% of use cases.
  • Secrets Manager is worth the $0.40/secret/month when you need automatic rotation, especially for RDS credentials.
  • Vault (or OpenBao) is the answer when you need dynamic secrets, multi-cloud support, PKI, or detailed audit logging. It is more complex to operate, but the security model is superior.
  • SOPS + age fills a niche for GitOps workflows where secrets need to live in version control.

Start with AWS native. Adopt Vault when you outgrow it. Use SOPS for what lives in Git. The tools complement each other – build the stack that fits your threat model and operational capacity.



Have questions about secrets management architecture? Connect with me on LinkedIn to discuss your specific environment and requirements.

Updated: