Skip to content

CWE-798: Hard-coded Credentials - Python

Overview

Hard-coded credentials in Python code create serious security vulnerabilities. Never embed passwords, API keys, database credentials, or encryption keys in source code or configuration files committed to version control. Use environment variables, configuration files, or secrets managers.

Primary Defence: Use environment variables with os.getenv(), python-dotenv for local development, or cloud secrets managers (AWS Secrets Manager, Azure Key Vault) for production.

Common Vulnerable Patterns

Hard-coded Database Credentials

# VULNERABLE - Credentials in source code

import psycopg2

class DatabaseConnection:
    DB_HOST = "localhost"
    DB_NAME = "mydb"
    DB_USER = "admin"
    DB_PASSWORD = "P@ssw0rd123"  # DANGEROUS!

    def get_connection(self):
        return psycopg2.connect(
            host=self.DB_HOST,
            database=self.DB_NAME,
            user=self.DB_USER,
            password=self.DB_PASSWORD
        )

Why this is vulnerable: Hard-coded database passwords in Python source files are visible to anyone with repository access, persist in git history forever, are deployed to production servers where they can be extracted, and cannot be rotated without code changes and redeployment.

Hard-coded API Keys

# VULNERABLE - API key in code

import requests

class ApiClient:
    API_KEY = "sk_live_51H7x8y9z10a11b12c"  # DANGEROUS!
    API_SECRET = "whsec_abcdef123456"  # DANGEROUS!

    def make_request(self):
        headers = {
            "Authorization": f"Bearer {self.API_KEY}"
        }
        response = requests.get("https://api.example.com/data", headers=headers)
        return response.json()

Why this is vulnerable: API keys in Python code are exposed to all developers with repository access, remain in version control history permanently, enable unauthorized API usage and billing charges, and can be easily extracted from .pyc bytecode files or deployed applications.

Hard-coded Encryption Keys

# VULNERABLE - Encryption key in code

from cryptography.fernet import Fernet

class Encryptor:
    SECRET_KEY = b'MySecretKey12345678901234567890='  # DANGEROUS!

    def encrypt(self, data: str) -> bytes:
        f = Fernet(self.SECRET_KEY)
        return f.encrypt(data.encode())

Why this is vulnerable: Hard-coded encryption keys defeat encryption's purpose since anyone with code access can decrypt data, keys cannot be rotated without code changes, and compromised keys expose all historical encrypted data permanently.

Credentials in config.py (Committed to Git)

# VULNERABLE - config.py with real credentials

DATABASE_URL = "postgresql://admin:P@ssw0rd123@localhost/mydb"
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
AWS_SECRET_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
STRIPE_API_KEY = "sk_live_51H7x8y9z10a11b12c"

Why this is vulnerable: Config files with credentials committed to git expose secrets permanently in version control history, make them accessible to all repository clones and forks, are often accidentally pushed to public repositories, and remain visible even after deletion through git history.

Secure Patterns

Environment Variables with os.environ

# SECURE - Read from environment variables

import os
import psycopg2

class DatabaseConnection:
    def __init__(self):
        self.db_host = os.environ.get("DB_HOST")
        self.db_name = os.environ.get("DB_NAME")
        self.db_user = os.environ.get("DB_USER")
        self.db_password = os.environ.get("DB_PASSWORD")

        if not all([self.db_host, self.db_name, self.db_user, self.db_password]):
            raise ValueError("Database credentials not configured")

    def get_connection(self):
        return psycopg2.connect(
            host=self.db_host,
            database=self.db_name,
            user=self.db_user,
            password=self.db_password
        )

# Set environment variables:
# export DB_HOST=localhost
# export DB_NAME=mydb
# export DB_USER=admin
# export DB_PASSWORD=SecurePassword123

Why this works: os.environ.get() retrieves credentials from environment variables set at the OS/container/platform level, keeping them out of source code. The validation ensures the application fails fast if credentials are missing (raising ValueError). Environment variables can be configured per environment (dev, staging, prod) without code changes. Credentials can be rotated by updating environment variables and restarting the application, with no code deployment.

python-dotenv (.env files)

# SECURE - Use python-dotenv for environment variables

import os
from dotenv import load_dotenv
import requests

# Load environment variables from .env file

load_dotenv()

class ApiClient:
    def __init__(self):
        self.api_key = os.getenv("API_KEY")
        self.api_secret = os.getenv("API_SECRET")

        if not self.api_key or not self.api_secret:
            raise ValueError("API credentials not configured")

    def make_request(self):
        headers = {
            "Authorization": f"Bearer {self.api_key}"
        }
        response = requests.get("https://api.example.com/data", headers=headers)
        return response.json()

# .env file (NOT committed to version control):

"""
API_KEY=sk_live_51H7x8y9z10a11b12c
API_SECRET=whsec_abcdef123456
DB_PASSWORD=SecurePassword123
"""

# Install: pip install python-dotenv

Why this works: python-dotenv loads environment variables from .env files for local development convenience. The .env file is excluded from version control via .gitignore, preventing secrets from entering Git history. load_dotenv() populates os.getenv() at runtime, not compile time. The validation (or operator with raise ValueError) ensures required credentials are present. Different .env files can be used per environment without modifying code.

AWS Secrets Manager

# SECURE - AWS Secrets Manager

import boto3
import json
from botocore.exceptions import ClientError

class SecretsManager:
    def __init__(self):
        self.client = boto3.client('secretsmanager')

    def get_database_credentials(self):
        secret_name = "prod/database/credentials"

        try:
            response = self.client.get_secret_value(SecretId=secret_name)
        except ClientError as e:
            raise Exception(f"Failed to retrieve secret: {e}")

        # Parse the secret
        secret = json.loads(response['SecretString'])

        return {
            'host': secret['host'],
            'database': secret['database'],
            'username': secret['username'],
            'password': secret['password']
        }

    def get_connection(self):
        creds = self.get_database_credentials()

        import psycopg2
        return psycopg2.connect(
            host=creds['host'],
            database=creds['database'],
            user=creds['username'],
            password=creds['password']
        )

# Install: pip install boto3
# AWS credentials from environment or IAM role
# export AWS_ACCESS_KEY_ID=your_access_key
# export AWS_SECRET_ACCESS_KEY=your_secret_key
# export AWS_DEFAULT_REGION=us-east-1

Why this works: AWS Secrets Manager provides centralized secret storage with KMS encryption, automatic rotation, and IAM-based access control. The boto3 client uses AWS credentials from environment variables or IAM roles (EC2, ECS, Lambda) - no credentials in code. Secrets are retrieved at runtime as needed, not stored in the application. CloudTrail logs all access for auditing. Supports secret versioning for gradual rollout of rotated credentials. The JSON structure allows storing related credentials together.

HashiCorp Vault

# SECURE - HashiCorp Vault integration

import hvac
import os

class VaultClient:
    def __init__(self):
        vault_url = os.getenv("VAULT_ADDR", "https://vault.example.com:8200")
        vault_token = os.getenv("VAULT_TOKEN")

        if not vault_token:
            raise ValueError("VAULT_TOKEN not configured")

        self.client = hvac.Client(url=vault_url, token=vault_token)

        if not self.client.is_authenticated():
            raise Exception("Failed to authenticate with Vault")

    def get_database_password(self):
        secret_path = "secret/data/database"

        response = self.client.secrets.kv.v2.read_secret_version(path="database")

        return response['data']['data']['password']

    def get_api_key(self):
        response = self.client.secrets.kv.v2.read_secret_version(path="api")
        return response['data']['data']['api_key']

# Install: pip install hvac

Why this works: HashiCorp Vault provides enterprise-grade secret management with dynamic secrets, lease management, and fine-grained access policies. The VAULT_TOKEN comes from environment (not code), allowing authentication without hardcoded credentials. Vault supports secret versioning, automatic rotation, detailed audit logs, and encryption as a service. The KV v2 API retrieves secrets on-demand. Vault's dynamic secrets can auto-generate database credentials that expire automatically.

Framework-Specific Guidance

Django

# SECURE - Django with environment variables

# settings.py

import os
from pathlib import Path

# Read from environment variables

SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY')
DEBUG = os.environ.get('DEBUG', 'False') == 'True'

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': os.environ.get('DB_NAME'),
        'USER': os.environ.get('DB_USER'),
        'PASSWORD': os.environ.get('DB_PASSWORD'),
        'HOST': os.environ.get('DB_HOST', 'localhost'),
        'PORT': os.environ.get('DB_PORT', '5432'),
    }
}

# Email configuration

EMAIL_HOST_USER = os.environ.get('EMAIL_USER')
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_PASSWORD')

# AWS S3 settings
AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.environ.get('AWS_STORAGE_BUCKET_NAME')

# Alternative: Use django-environ
from environ import Env

env = Env()
env.read_env()  # Reads .env file

SECRET_KEY = env('DJANGO_SECRET_KEY')
DEBUG = env.bool('DEBUG', default=False)
DATABASES = {
    'default': env.db()  # Reads DATABASE_URL
}

# Install: pip install django-environ

Flask

# SECURE - Flask with environment variables
# config.py

import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY') or os.urandom(32)
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')

    # API keys
    STRIPE_API_KEY = os.environ.get('STRIPE_API_KEY')
    SENDGRID_API_KEY = os.environ.get('SENDGRID_API_KEY')

    # AWS
    AWS_ACCESS_KEY_ID = os.environ.get('AWS_ACCESS_KEY_ID')
    AWS_SECRET_ACCESS_KEY = os.environ.get('AWS_SECRET_ACCESS_KEY')

class DevelopmentConfig(Config):
    DEBUG = True
    SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL')

class ProductionConfig(Config):
    DEBUG = False

# app.py

from flask import Flask
from dotenv import load_dotenv

load_dotenv()

app = Flask(__name__)
app.config.from_object('config.ProductionConfig')

# Access configuration

stripe_key = app.config['STRIPE_API_KEY']

FastAPI

# SECURE - FastAPI with Pydantic Settings
# config.py

from pydantic import BaseSettings, Field

class Settings(BaseSettings):
    # Database
    db_host: str = Field(..., env='DB_HOST')
    db_name: str = Field(..., env='DB_NAME')
    db_user: str = Field(..., env='DB_USER')
    db_password: str = Field(..., env='DB_PASSWORD')

    # API Keys
    api_key: str = Field(..., env='API_KEY')
    api_secret: str = Field(..., env='API_SECRET')

    # JWT
    jwt_secret: str = Field(..., env='JWT_SECRET')

    class Config:
        env_file = '.env'
        env_file_encoding = 'utf-8'

# main.py
from fastapi import FastAPI, Depends
from functools import lru_cache

app = FastAPI()

@lru_cache()
def get_settings():
    return Settings()

@app.get("/")
async def root(settings: Settings = Depends(get_settings)):
    # Use settings.api_key, etc.
    return {"status": "ok"}

# Install: pip install pydantic[dotenv]

SQLAlchemy

# SECURE - SQLAlchemy with environment variables
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
import os

class Database:
    def __init__(self):
        # Build connection string from environment variables
        db_user = os.environ.get('DB_USER')
        db_password = os.environ.get('DB_PASSWORD')
        db_host = os.environ.get('DB_HOST', 'localhost')
        db_port = os.environ.get('DB_PORT', '5432')
        db_name = os.environ.get('DB_NAME')

        if not all([db_user, db_password, db_name]):
            raise ValueError("Database credentials not configured")

        # PostgreSQL
        database_url = f"postgresql://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}"

        # Or use DATABASE_URL directly:
        # database_url = os.environ.get('DATABASE_URL')

        self.engine = create_engine(database_url, echo=False)
        self.SessionLocal = sessionmaker(bind=self.engine)

    def get_session(self):
        return self.SessionLocal()

Encryption Keys Management

# SECURE - Key management with AWS KMS

import boto3
import base64

class EncryptionService:
    def __init__(self):
        self.kms_client = boto3.client('kms')
        self.key_id = os.environ.get('KMS_KEY_ID')

        if not self.key_id:
            raise ValueError("KMS_KEY_ID not configured")

    def encrypt(self, plaintext: str) -> str:
        response = self.kms_client.encrypt(
            KeyId=self.key_id,
            Plaintext=plaintext.encode()
        )

        # Return base64-encoded ciphertext
        return base64.b64encode(response['CiphertextBlob']).decode()

    def decrypt(self, ciphertext: str) -> str:
        ciphertext_blob = base64.b64decode(ciphertext)

        response = self.kms_client.decrypt(
            CiphertextBlob=ciphertext_blob
        )

        return response['Plaintext'].decode()

# Alternative: Use Fernet with key from environment

from cryptography.fernet import Fernet

class FernetEncryption:
    def __init__(self):
        # Key should be in environment variable
        key = os.environ.get('ENCRYPTION_KEY')

        if not key:
            raise ValueError("ENCRYPTION_KEY not configured")

        self.cipher = Fernet(key.encode())

    def encrypt(self, data: str) -> bytes:
        return self.cipher.encrypt(data.encode())

    def decrypt(self, token: bytes) -> str:
        return self.cipher.decrypt(token).decode()

# Generate key once: Fernet.generate_key()

# Store in environment: export ENCRYPTION_KEY=generated_key

Testing with Test Credentials

# SECURE - Use test credentials for unit tests

import pytest
import os
from unittest.mock import patch

class TestDatabaseConnection:

    @pytest.fixture
    def mock_env_vars(self, monkeypatch):
        """Provide test credentials via environment variables"""
        monkeypatch.setenv("DB_HOST", "localhost")
        monkeypatch.setenv("DB_NAME", "test_db")
        monkeypatch.setenv("DB_USER", "test_user")
        monkeypatch.setenv("DB_PASSWORD", "test_password")

    def test_connection(self, mock_env_vars):
        """Test database connection with test credentials"""
        db = DatabaseConnection()

        assert db.db_host == "localhost"
        assert db.db_user == "test_user"

    @patch.dict(os.environ, {
        "API_KEY": "test_api_key",
        "API_SECRET": "test_secret"
    })
    def test_api_client(self):
        """Test API client with mocked credentials"""
        client = ApiClient()
        assert client.api_key == "test_api_key"

# Using testcontainers for integration tests

from testcontainers.postgres import PostgresContainer
import psycopg2

def test_with_postgres_container():
    with PostgresContainer("postgres:13") as postgres:
        # Container provides test credentials
        conn = psycopg2.connect(
            host=postgres.get_container_host_ip(),
            port=postgres.get_exposed_port(5432),
            database=postgres.POSTGRES_DB,
            user=postgres.POSTGRES_USER,
            password=postgres.POSTGRES_PASSWORD
        )

        # Test database operations
        cursor = conn.cursor()
        cursor.execute("SELECT 1")
        assert cursor.fetchone()[0] == 1

# Install: pip install pytest testcontainers

.gitignore Best Practices

# Add these patterns to .gitignore

# Environment files

.env
.env.local
.env.*.local

# Configuration files with secrets

config/secrets.py
config/local_settings.py

# Django

local_settings.py
db.sqlite3

# Flask

instance/

# Credentials

*.pem
*.key
credentials.json
service-account.json

# IDE

.vscode/
.idea/
*.swp

Creating .env.example Template

# .env.example (committed to version control)

# Copy this to .env and fill in real values

# Database

DB_HOST=localhost
DB_NAME=mydb
DB_USER=admin
DB_PASSWORD=your_password_here

# API Keys

API_KEY=your_api_key_here
API_SECRET=your_api_secret_here

# AWS

AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key

# Django

DJANGO_SECRET_KEY=your_secret_key_here
DEBUG=False

Detecting Hard-coded Secrets

Using detect-secrets

# Install detect-secrets

pip install detect-secrets

# Scan repository

detect-secrets scan > .secrets.baseline

# Audit findings

detect-secrets audit .secrets.baseline

# Add pre-commit hook

# .pre-commit-config.yaml:

repos:

  - repo: https://github.com/Yelp/detect-secrets
    rev: v1.4.0
    hooks:

      - id: detect-secrets
        args: ['--baseline', '.secrets.baseline']

Using TruffleHog

# Install truffleHog

pip install truffleHog

# Scan repository

trufflehog --regex --entropy=True .

# Scan Git history

trufflehog --regex --entropy=True file:///path/to/repo

Verification

After implementing the recommended secure patterns, verify the fix through multiple approaches:

  • Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
  • Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
  • Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
  • Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
  • Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
  • Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
  • Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
  • Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced

Cloud-Native Secrets Management (Production-Grade)

For production applications, use dedicated secrets management services instead of environment variables:

AWS Secrets Manager

import boto3
from botocore.exceptions import ClientError
import json

class AWSSecretsManager:
    """
    Retrieve secrets from AWS Secrets Manager.

    Prerequisites:

    - pip install boto3
    - EC2/Lambda must have IAM role with secretsmanager:GetSecretValue permission
    - For local development, configure AWS credentials via aws configure
    """

    def __init__(self, region_name="us-east-1"):
        self.client = boto3.session.Session().client(
            service_name='secretsmanager',
            region_name=region_name
        )

    def get_secret(self, secret_name: str) -> dict:
        """
        Retrieve secret from AWS Secrets Manager.

        Args:
            secret_name: Name or ARN of the secret (e.g., "prod/database/credentials")

        Returns:
            dict: Parsed JSON secret value

        Raises:
            Exception: If secret not found or access denied
        """
        try:
            response = self.client.get_secret_value(SecretId=secret_name)

            # Secrets can be string or binary
            if 'SecretString' in response:
                return json.loads(response['SecretString'])
            else:
                # Binary secret (less common)
                import base64
                return json.loads(base64.b64decode(response['SecretBinary']))

        except ClientError as e:
            error_code = e.response['Error']['Code']
            if error_code == 'ResourceNotFoundException':
                raise Exception(f"Secret '{secret_name}' not found in Secrets Manager")
            elif error_code == 'AccessDeniedException':
                raise Exception(f"Access denied to secret '{secret_name}'. Check IAM permissions.")
            else:
                raise Exception(f"Error retrieving secret: {e}")

# Usage example - Database credentials

secrets_manager = AWSSecretsManager(region_name="us-west-2")
db_creds = secrets_manager.get_secret("prod/database/postgres")

# db_creds contains: {"host": "db.example.com", "username": "app_user", "password": "..."}

import psycopg2
connection = psycopg2.connect(
    host=db_creds["host"],
    database=db_creds["database"],
    user=db_creds["username"],
    password=db_creds["password"]
)

# Usage example - API keys

api_secrets = secrets_manager.get_secret("prod/external-apis")
stripe_api_key = api_secrets["stripe_key"]
sendgrid_api_key = api_secrets["sendgrid_key"]

Caching for Performance:

from functools import lru_cache

@lru_cache(maxsize=128)
def get_cached_secret(secret_name: str, region: str = "us-east-1") -> dict:
    """Cache secrets for 1 hour to reduce API calls (default LRU cache)"""
    manager = AWSSecretsManager(region_name=region)
    return manager.get_secret(secret_name)

# Or use AWS Secrets Manager Caching Client (recommended)

# pip install aws-secretsmanager-caching

from aws_secretsmanager_caching import SecretCache

cache = SecretCache()  # Caches secrets for 1 hour by default
secret_json = cache.get_secret_string('prod/database/credentials')

Kubernetes Secrets

For applications running in Kubernetes:

import os

class KubernetesSecrets:
    """
    Read secrets from Kubernetes Secrets mounted as volumes or environment variables.

    Secrets are defined in Kubernetes manifests and automatically injected:

    apiVersion: v1
    kind: Secret
    metadata:
      name: database-credentials
    type: Opaque
    data:
      password: BASE64_ENCODED_PASSWORD
      username: BASE64_ENCODED_USERNAME
    """

    @staticmethod
    def get_from_volume(secret_name: str, mount_path: str = "/etc/secrets") -> str:
        """
        Read secret mounted as a volume.

        Kubernetes pod spec:
          volumes:

          - name: db-secret
            secret:
              secretName: database-credentials
          volumeMounts:

          - name: db-secret
            mountPath: /etc/secrets/db
            readOnly: true

        Args:
            secret_name: Name of the secret file (matches key in Secret)
            mount_path: Base path where secrets are mounted

        Returns:
            str: Secret value
        """
        secret_file = os.path.join(mount_path, secret_name)
        try:
            with open(secret_file, 'r') as f:
                return f.read().strip()
        except FileNotFoundError:
            raise Exception(f"Secret '{secret_name}' not found at {secret_file}")

    @staticmethod
    def get_from_env(env_var_name: str) -> str:
        """
        Read secret from environment variable injected by Kubernetes.

        Kubernetes pod spec:
          env:

          - name: DB_PASSWORD
            valueFrom:
              secretKeyRef:
                name: database-credentials
                key: password

        Args:
            env_var_name: Name of the environment variable

        Returns:
            str: Secret value
        """
        value = os.getenv(env_var_name)
        if not value:
            raise Exception(f"Environment variable '{env_var_name}' not set")
        return value

# Usage example - Volume-mounted secrets (recommended for large secrets)

k8s_secrets = KubernetesSecrets()
db_password = k8s_secrets.get_from_volume("password", mount_path="/etc/secrets/db")
db_username = k8s_secrets.get_from_volume("username", mount_path="/etc/secrets/db")

# Usage example - Environment variable secrets (simpler for small secrets)

db_password = k8s_secrets.get_from_env("DB_PASSWORD")
api_key = k8s_secrets.get_from_env("STRIPE_API_KEY")

# Complete database connection example

import psycopg2

connection = psycopg2.connect(
    host=os.getenv("DB_HOST"),  # Non-secret config can use env vars
    database=os.getenv("DB_NAME"),
    user=k8s_secrets.get_from_env("DB_USERNAME"),
    password=k8s_secrets.get_from_env("DB_PASSWORD")
)

External Secrets Operator (Advanced):

# For syncing secrets from AWS/Azure/GCP into Kubernetes

# Install: https://external-secrets.io/

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secrets-manager
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-west-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets-sa

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
  target:
    name: database-credentials  # Kubernetes Secret name
  data:

  - secretKey: password  # Key in K8s Secret
    remoteRef:
      key: prod/database/credentials  # AWS Secrets Manager secret
      property: password  # JSON key within the secret

Best Practices for Cloud Secrets Management

Use IAM Roles/Service Accounts (No Access Keys)

  • AWS: Assign IAM role to EC2/ECS/Lambda with secretsmanager:GetSecretValue
  • Kubernetes: Use ServiceAccount with IRSA (IAM Roles for Service Accounts)
  • Never hard-code AWS access keys to access Secrets Manager!

Enable Secret Rotation

  • AWS Secrets Manager supports automatic rotation (Lambda-based)
  • Rotate database passwords, API keys every 30-90 days
  • Application should handle rotation gracefully (cache TTL < rotation interval)

Audit Secret Access

  • Enable AWS CloudTrail for Secrets Manager API calls
  • Monitor for unusual access patterns
  • Alert on GetSecretValue from unexpected sources

Separate Secrets by Environment

    prod/database/credentials
    staging/database/credentials
    dev/database/credentials
  • Never share secrets across environments
  • Use IAM policies to restrict access by environment

Cache Secrets (Reduce API Calls)

  • Cache for 15-60 minutes depending on rotation frequency
  • Reduces cost and latency
  • Use libraries like aws-secretsmanager-caching

Additional Resources