Hardcoded credentials in your repository are a security incident waiting to happen. One leaked .env file, one accidental commit, and your database is exposed to the internet.

Let’s do secrets properly.

The Basics

What’s a Secret?

Anything that grants access:

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

Where Secrets Don’t Belong

1
2
3
# ❌ Never do this
DATABASE_URL = "postgres://admin:supersecret123@db.prod.internal/myapp"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Also bad:

  • .env files committed to git
  • Docker image layers
  • CI/CD logs
  • Chat messages
  • Wikis or documentation

Secret Storage Options

Environment Variables

Simple, but limited:

1
2
3
4
5
6
7
# Set in shell
export DATABASE_PASSWORD="supersecret"

# Or in systemd unit
[Service]
Environment="DATABASE_PASSWORD=supersecret"
EnvironmentFile=/etc/myapp/secrets
1
2
import os
password = os.environ.get("DATABASE_PASSWORD")

Pros: Simple, works everywhere Cons: Visible in process listings, no rotation, no audit

HashiCorp Vault

The gold standard for secrets management:

1
2
3
4
5
6
7
8
# Start dev server
vault server -dev

# Store a secret
vault kv put secret/myapp/database password="supersecret"

# Retrieve it
vault kv get -field=password secret/myapp/database

Application integration:

1
2
3
4
5
6
7
8
9
import hvac

client = hvac.Client(url='https://vault.internal:8200')
client.token = os.environ['VAULT_TOKEN']

secret = client.secrets.kv.v2.read_secret_version(
    path='myapp/database'
)
password = secret['data']['data']['password']

Vault features:

  • Dynamic secrets (generate on-demand)
  • Automatic rotation
  • Audit logging
  • Fine-grained access policies
  • Encryption as a service

AWS Secrets Manager

Managed service, tight AWS integration:

1
2
3
4
5
6
7
8
9
# Store a secret
aws secretsmanager create-secret \
  --name prod/myapp/database \
  --secret-string '{"username":"admin","password":"supersecret"}'

# Retrieve it
aws secretsmanager get-secret-value \
  --secret-id prod/myapp/database \
  --query SecretString --output text
1
2
3
4
5
6
7
import boto3
import json

client = boto3.client('secretsmanager')
response = client.get_secret_value(SecretId='prod/myapp/database')
secrets = json.loads(response['SecretString'])
password = secrets['password']

Built-in rotation for:

  • RDS databases
  • DocumentDB
  • Redshift
  • Custom Lambda functions

AWS Parameter Store

Lighter weight, lower cost:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Store (encrypted)
aws ssm put-parameter \
  --name /prod/myapp/database_password \
  --value "supersecret" \
  --type SecureString

# Retrieve
aws ssm get-parameter \
  --name /prod/myapp/database_password \
  --with-decryption \
  --query Parameter.Value --output text

Kubernetes Secrets

Native K8s solution:

1
2
3
4
5
6
7
8
apiVersion: v1
kind: Secret
metadata:
  name: db-credentials
type: Opaque
data:
  username: YWRtaW4=  # base64 encoded
  password: c3VwZXJzZWNyZXQ=
1
2
3
4
5
6
7
# Use in pod
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-credentials
        key: password

Warning: K8s secrets are base64 encoded, not encrypted. Use:

  • Sealed Secrets (Bitnami)
  • External Secrets Operator
  • Vault integration

Secret Injection Patterns

Sidecar Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Vault Agent sidecar
containers:
  - name: vault-agent
    image: vault:latest
    args:
      - agent
      - -config=/etc/vault/config.hcl
    volumeMounts:
      - name: secrets
        mountPath: /secrets
        
  - name: app
    image: myapp:latest
    volumeMounts:
      - name: secrets
        mountPath: /secrets
        readOnly: true

Vault Agent writes secrets to shared volume; app reads them.

Init Container Pattern

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
initContainers:
  - name: fetch-secrets
    image: amazon/aws-cli
    command:
      - sh
      - -c
      - |
        aws secretsmanager get-secret-value \
          --secret-id prod/myapp/database \
          --query SecretString --output text > /secrets/db.json
    volumeMounts:
      - name: secrets
        mountPath: /secrets

External Secrets Operator

Sync cloud secrets to Kubernetes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: SecretStore
  target:
    name: db-credentials
  data:
    - secretKey: password
      remoteRef:
        key: prod/myapp/database
        property: password

Secret Rotation

Why Rotate?

  • Limits exposure window if compromised
  • Compliance requirements
  • Principle of least privilege over time

AWS RDS Automatic Rotation

1
2
3
4
aws secretsmanager rotate-secret \
  --secret-id prod/myapp/database \
  --rotation-lambda-arn arn:aws:lambda:...:function:SecretsManagerRDSRotation \
  --rotation-rules AutomaticallyAfterDays=30

Vault Dynamic Secrets

Vault generates credentials on-demand with automatic expiration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Configure database backend
vault write database/config/mydb \
  plugin_name=postgresql-database-plugin \
  connection_url="postgresql://{{username}}:{{password}}@db:5432/myapp" \
  allowed_roles="readonly"

# Create role
vault write database/roles/readonly \
  db_name=mydb \
  creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';" \
  default_ttl="1h" \
  max_ttl="24h"

# App requests credentials (fresh each time)
vault read database/creds/readonly

Each credential is unique and auto-expires.

Application Considerations

Apps must handle rotation gracefully:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class DatabaseConnection:
    def __init__(self, secret_id):
        self.secret_id = secret_id
        self.refresh_credentials()
    
    def refresh_credentials(self):
        self.credentials = get_secret(self.secret_id)
        self.connection = create_connection(self.credentials)
    
    def execute(self, query):
        try:
            return self.connection.execute(query)
        except AuthenticationError:
            # Credential rotated, refresh and retry
            self.refresh_credentials()
            return self.connection.execute(query)

CI/CD Secrets

GitHub Actions

1
2
3
4
5
6
7
8
jobs:
  deploy:
    steps:
      - name: Deploy
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        run: ./deploy.sh

Better: Use OIDC for temporary credentials:

1
2
3
4
5
6
7
8
9
permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions
      aws-region: us-east-1

GitLab CI

1
2
3
4
5
deploy:
  script:
    - ./deploy.sh
  variables:
    DATABASE_PASSWORD: $DATABASE_PASSWORD  # From CI/CD settings

Detection and Prevention

Git Hooks (Pre-commit)

1
2
3
4
5
6
7
# .pre-commit-config.yaml
repos:
  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:
      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

Gitleaks

1
2
3
4
5
# Scan repository
gitleaks detect --source . --verbose

# In CI
gitleaks detect --source . --redact --exit-code 1

TruffleHog

1
2
3
4
5
# Scan git history
trufflehog git file://. --only-verified

# Scan GitHub org
trufflehog github --org=mycompany

Emergency Response

When a secret is leaked:

  1. Revoke immediately - Don’t wait
  2. Rotate - Generate new credentials
  3. Audit - Check for unauthorized access
  4. Remediate - Remove from git history if needed
  5. Post-mortem - How did this happen?
1
2
3
4
5
6
7
# Remove secret from git history (nuclear option)
git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch path/to/secret" \
  --prune-empty --tag-name-filter cat -- --all

# Force push (coordinate with team!)
git push --force --all

Quick Reference

SolutionBest ForRotationAudit
Env varsSimple appsManualNone
VaultEnterpriseAutomaticFull
AWS Secrets ManagerAWS nativeAutomaticCloudTrail
K8s SecretsK8s workloadsManualAPI audit

Checklist

  • No secrets in source code
  • No secrets in Docker images
  • Pre-commit hooks detect secrets
  • Secrets stored in dedicated manager
  • Rotation policy defined
  • Access audited
  • Emergency rotation procedure documented

Treat every secret like it’s already been compromised. Store securely, rotate regularly, and always have a revocation plan.