← Back to Tutorials
Tutorial 07 Intermediate

AWS Secrets Manager: Stop Hardcoding Credentials

Hardcoded credentials—API keys, database passwords, and access tokens embedded directly in source code—represent one of the most dangerous yet common security vulnerabilities. Learn how to implement AWS Secrets Manager for secure credential management with automatic rotation.

20 min implementation
12 min read
Secrets & Credential Management

Why Hardcoded Credentials Are Dangerous

Every time you commit hardcoded credentials to version control, you create a permanent security vulnerability. Even if you later remove the credentials, they remain in Git history forever. Version control systems like GitHub, GitLab, and Bitbucket are actively scanned by automated tools seeking exposed credentials.

The Three Deadly Sins of Credential Management

1

Source Code Exposure

Credentials committed to Git remain in history forever. Automated tools constantly scan public repositories for exposed API keys, database passwords, and access tokens.

2

Rotation Impossibility

Hardcoded credentials make rotation nearly impossible. Changing a password requires updating every instance across your codebase, redeploying applications, and coordinating timing across services.

3

Privilege Escalation Risk

Hardcoded credentials often grant broader access than necessary because developers use convenient, high-privilege accounts. A single compromised credential can provide administrative access.

⚠️
Never Do This: Hardcoded credentials in source code are a critical vulnerability.
DANGEROUS: Hardcoded Credentials
// NEVER DO THIS - Hardcoded credentials
const config = {
    database_url: "postgresql://admin:EMAIL ADDRESS/main",
    api_key: "sk-1234567890abcdef",
    jwt_secret: "my-super-secret-key"
};

Secrets Manager vs. Parameter Store

  • Secrets Manager: Built-in automatic rotation, KMS encryption, cross-region replication, ~$0.40/secret/month. Best for database credentials, API keys, OAuth tokens.
  • Parameter Store: No automatic rotation, encrypted at rest, regional only, ~$0.05/10K requests. Best for application configuration, non-sensitive data.
  • Hardcoding: Never use for credentials. Git history exposure, impossible rotation, no audit logging.
Recommendation: Use Secrets Manager for database credentials, API keys, and OAuth tokens. Use Parameter Store for application configuration and non-sensitive data. Never hardcode credentials.
1

Create Your First Secret

~5 minutes

AWS Secrets Manager stores your credentials securely with encryption at rest using AWS KMS. You can create secrets for database credentials, API keys, or any sensitive configuration.

Prerequisites

  • AWS CLI configured with appropriate IAM permissions
  • Access to AWS Management Console
  • Existing database or service credentials to migrate

Console Steps

1.1 Navigate to Secrets Manager

  • Open the AWS Management Console
  • Search for "Secrets Manager" in the services search bar
  • Click on "AWS Secrets Manager"

1.2 Create New Secret

  • Click Store a new secret
  • Select secret type:
    • Credentials for Amazon RDS database: For database credentials with automatic rotation
    • Other type of secret: For API keys, OAuth tokens, or custom credentials
  • Enter your credentials in key/value pairs or as JSON

1.3 Configure Secret Details

  • Secret name: prod/database/postgresql
  • Description: "Production database credentials for main application"
  • KMS encryption key: Choose aws/secretsmanager (default) or custom key
  • Add tags for environment, team, and cost allocation
Create Database Secret via CLI
# Create a database secret with JSON credentials
aws secretsmanager create-secret \
    --name "prod/database/postgresql" \
    --description "Production PostgreSQL credentials" \
    --secret-string '{
        "username": "app_user",
        "password": "YourSecurePassword123!",
        "host": "prod-db.cluster-xyz.us-west-2.rds.amazonaws.com",
        "port": 5432,
        "dbname": "production"
    }' \
    --tags '[
        {"Key": "Environment", "Value": "production"},
        {"Key": "Team", "Value": "backend"},
        {"Key": "Application", "Value": "main-app"}
    ]'
Create API Key Secret
# Create an API key secret
aws secretsmanager create-secret \
    --name "prod/external-api/stripe" \
    --description "Stripe API keys for payment processing" \
    --secret-string '{
        "publishable_key": "pk_live_abc123...",
        "secret_key": "sk_live_xyz789...",
        "webhook_secret": "whsec_def456..."
    }'
Success! Your secret is now stored securely in AWS Secrets Manager with encryption at rest using AWS KMS.
2

Migrate from Hardcoded Credentials

~8 minutes

Replace hardcoded credentials in your application with secure calls to Secrets Manager. This involves updating your application code and deployment configuration.

Install AWS SDK

Install AWS SDK
# Node.js
npm install @aws-sdk/client-secrets-manager

# Python
pip install boto3

# Go
go get github.com/aws/aws-sdk-go-v2/service/secretsmanager

# Java (Gradle)
implementation 'software.amazon.awssdk:secretsmanager'

Update Application Code

Node.js - Secure Implementation
// SECURE: Dynamic credential retrieval
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

async function getDbCredentials() {
    const client = new SecretsManagerClient({ region: 'us-west-2' });

    try {
        const response = await client.send(
            new GetSecretValueCommand({
                SecretId: 'prod/database/postgresql'
            })
        );
        return JSON.parse(response.SecretString);
    } catch (error) {
        console.error('Failed to retrieve credentials:', error);
        throw error;
    }
}

// Usage in your application
async function initializeDatabase() {
    const credentials = await getDbCredentials();

    const pool = new Pool({
        host: credentials.host,
        user: credentials.username,
        password: credentials.password,
        database: credentials.dbname,
        port: credentials.port
    });

    return pool;
}
Python - Secure Implementation
# SECURE: Python implementation
import boto3
import json
from botocore.exceptions import ClientError

def get_secret(secret_name, region_name='us-west-2'):
    """Retrieve secret from AWS Secrets Manager."""
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        response = client.get_secret_value(SecretId=secret_name)
    except ClientError as e:
        raise e

    return json.loads(response['SecretString'])

# Usage
credentials = get_secret('prod/database/postgresql')
connection_string = (
    f"postgresql://{credentials['username']}:{credentials['password']}"
    f"@{credentials['host']}:{credentials['port']}/{credentials['dbname']}"
)
Shell Script - Environment Variables
#!/bin/bash
# Deployment script that retrieves secrets

# Retrieve secret and extract values
SECRET_JSON=$(aws secretsmanager get-secret-value \
    --secret-id "prod/database/postgresql" \
    --query 'SecretString' \
    --output text)

# Parse JSON and export as environment variables
export DB_HOST=$(echo $SECRET_JSON | jq -r '.host')
export DB_USER=$(echo $SECRET_JSON | jq -r '.username')
export DB_PASS=$(echo $SECRET_JSON | jq -r '.password')
export DB_NAME=$(echo $SECRET_JSON | jq -r '.dbname')

# Start your application
node app.js
💡
Security Note: Always use IAM roles instead of access keys when possible. For EC2, Lambda, or ECS, assign appropriate IAM roles rather than embedding AWS credentials.
3

Set Up Automatic Rotation

~4 minutes

Automatic rotation is one of Secrets Manager's most powerful features. It regularly updates your credentials without requiring application downtime or manual intervention.

Console Steps

3.1 Enable Rotation for RDS Database

  • Open your secret in the Secrets Manager console
  • Click Edit rotation
  • Enable Automatic rotation
  • Set rotation interval (recommended: 30-90 days)
  • Choose rotation strategy:
    • Single user: One set of credentials, brief downtime during rotation
    • Alternating users: Two sets of credentials, zero downtime
Enable Rotation via CLI
# Enable automatic rotation for RDS database secret
aws secretsmanager rotate-secret \
    --secret-id "prod/database/postgresql" \
    --rotation-rules '{"AutomaticallyAfterDays": 30}'

# Verify rotation configuration
aws secretsmanager describe-secret \
    --secret-id "prod/database/postgresql" \
    --query '{RotationEnabled: RotationEnabled, RotationRules: RotationRules}'

3.2 Custom Rotation for API Keys

For non-database secrets like API keys, create a custom Lambda rotation function:

Custom Rotation Lambda (Python)
# Lambda function for custom API key rotation
import json
import boto3
import requests

def lambda_handler(event, context):
    """Handle rotation steps for custom secrets."""
    secret_arn = event['SecretId']
    step = event['Step']

    secrets_client = boto3.client('secretsmanager')

    if step == 'createSecret':
        # Generate new API key from service
        new_api_key = generate_new_api_key()
        # Store pending version
        secrets_client.put_secret_value(
            SecretId=secret_arn,
            SecretString=json.dumps({'api_key': new_api_key}),
            VersionStages=['AWSPENDING']
        )

    elif step == 'setSecret':
        # Activate new API key with external service
        activate_api_key(secret_arn)

    elif step == 'testSecret':
        # Test new API key functionality
        test_api_key(secret_arn)

    elif step == 'finishSecret':
        # Move AWSPENDING to AWSCURRENT
        finish_rotation(secrets_client, secret_arn)

def generate_new_api_key():
    """Call your service's API to generate new key."""
    response = requests.post(
        'https://api.yourservice.com/keys',
        headers={'Authorization': 'Bearer admin_token'}
    )
    return response.json()['api_key']
Rotation Active! Your credentials will now automatically rotate according to your schedule, significantly reducing the window of exposure for compromised credentials.
4

Production-Ready Integration

~3 minutes

For production applications, implement caching, error handling, and retry logic to ensure reliable access to secrets while minimizing AWS API calls.

Implement Secret Caching

Production-Ready Secrets Client with Caching
// Production-ready secrets manager client with caching
const { SecretsManagerClient, GetSecretValueCommand } = require('@aws-sdk/client-secrets-manager');

class SecretsCache {
    constructor(region, ttlMinutes = 5) {
        this.client = new SecretsManagerClient({ region });
        this.cache = new Map();
        this.ttl = ttlMinutes * 60 * 1000; // Convert to milliseconds
    }

    async getSecret(secretId, forceRefresh = false) {
        const cached = this.cache.get(secretId);

        // Return cached value if valid and not forcing refresh
        if (!forceRefresh && cached && (Date.now() - cached.timestamp) < this.ttl) {
            return cached.value;
        }

        // Fetch fresh value from AWS
        try {
            const response = await this.client.send(
                new GetSecretValueCommand({ SecretId: secretId })
            );
            const secret = JSON.parse(response.SecretString);

            // Cache the result
            this.cache.set(secretId, {
                value: secret,
                timestamp: Date.now()
            });

            return secret;
        } catch (error) {
            // Return stale cache if AWS call fails
            if (cached) {
                console.warn('Using stale cache due to AWS error:', error.message);
                return cached.value;
            }
            throw error;
        }
    }

    // Force refresh all secrets (call during rotation events)
    async refreshAll() {
        const promises = Array.from(this.cache.keys()).map(secretId =>
            this.getSecret(secretId, true)
        );
        await Promise.all(promises);
    }
}

// Global instance with singleton pattern
const secretsCache = new SecretsCache('us-west-2', 5);
module.exports = secretsCache;

IAM Permissions

IAM Policy for Secrets Access
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "secretsmanager:GetSecretValue"
            ],
            "Resource": [
                "arn:aws:secretsmanager:us-west-2:*:secret:prod/database/*",
                "arn:aws:secretsmanager:us-west-2:*:secret:prod/external-api/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "kms:Decrypt"
            ],
            "Resource": "arn:aws:kms:us-west-2:*:key/*",
            "Condition": {
                "StringEquals": {
                    "kms:ViaService": "secretsmanager.us-west-2.amazonaws.com"
                }
            }
        }
    ]
}

Docker Integration

Container Startup Script
#!/bin/bash
# start.sh - Startup script for containerized applications
set -e

# Wait for AWS metadata service (for ECS/EC2)
until aws sts get-caller-identity > /dev/null 2>&1; do
    echo "Waiting for AWS credentials..."
    sleep 2
done

# Retrieve and export database credentials
echo "Retrieving database credentials..."
DB_SECRET=$(aws secretsmanager get-secret-value \
    --secret-id "prod/database/postgresql" \
    --query 'SecretString' \
    --output text)

export DB_HOST=$(echo "$DB_SECRET" | jq -r '.host')
export DB_USER=$(echo "$DB_SECRET" | jq -r '.username')
export DB_PASS=$(echo "$DB_SECRET" | jq -r '.password')
export DB_NAME=$(echo "$DB_SECRET" | jq -r '.dbname')

echo "Starting application..."
exec node app.js

Cross-Region Replication

Enable Cross-Region Replication
# Replicate critical secrets to multiple regions for disaster recovery
aws secretsmanager replicate-secret-to-regions \
    --secret-id "prod/database/postgresql" \
    --add-replica-regions '[
        {
            "Region": "us-east-1",
            "KmsKeyId": "alias/aws/secretsmanager"
        },
        {
            "Region": "eu-west-1",
            "KmsKeyId": "alias/aws/secretsmanager"
        }
    ]'
Production Ready! Your application now securely retrieves credentials with caching, error handling, and proper IAM permissions.

Validate Your Configuration

Complete these checks to ensure your secrets management is working correctly and securely:

Validation Script

secrets-validation.sh
#!/bin/bash
# Secrets Manager Implementation Validation Script

echo "=== Secrets Manager Validation ==="
echo ""

# Test 1: Verify secret exists and is accessible
echo "Testing secret accessibility..."
SECRET_ARN=$(aws secretsmanager describe-secret \
    --secret-id "prod/database/postgresql" \
    --query 'ARN' \
    --output text 2>/dev/null)

if [ -n "$SECRET_ARN" ]; then
    echo "✓ Secret accessible: $SECRET_ARN"
else
    echo "✗ Cannot access secret!"
    exit 1
fi
echo ""

# Test 2: Verify rotation configuration
echo "Checking rotation configuration..."
ROTATION_ENABLED=$(aws secretsmanager describe-secret \
    --secret-id "prod/database/postgresql" \
    --query 'RotationEnabled' \
    --output text)

if [ "$ROTATION_ENABLED" == "True" ]; then
    echo "✓ Automatic rotation is enabled"
    ROTATION_DAYS=$(aws secretsmanager describe-secret \
        --secret-id "prod/database/postgresql" \
        --query 'RotationRules.AutomaticallyAfterDays' \
        --output text)
    echo "  Rotation interval: $ROTATION_DAYS days"
else
    echo "⚠ Consider enabling automatic rotation"
fi
echo ""

# Test 3: Check encryption configuration
echo "Verifying encryption..."
KMS_KEY=$(aws secretsmanager describe-secret \
    --secret-id "prod/database/postgresql" \
    --query 'KmsKeyId' \
    --output text)

if [ -n "$KMS_KEY" ] && [ "$KMS_KEY" != "None" ]; then
    echo "✓ Encrypted with KMS key: $KMS_KEY"
else
    echo "✓ Using default AWS managed key"
fi
echo ""

# Test 4: Validate secret format
echo "Validating secret format..."
SECRET_VALUE=$(aws secretsmanager get-secret-value \
    --secret-id "prod/database/postgresql" \
    --query 'SecretString' \
    --output text)

if echo "$SECRET_VALUE" | jq -e '.username and .password and .host' > /dev/null 2>&1; then
    echo "✓ Secret contains required database fields"
else
    echo "✗ Secret format is invalid or incomplete"
fi
echo ""

# Test 5: Check for hardcoded credentials in codebase
echo "Scanning for hardcoded credentials..."
if [ -d "." ]; then
    HARDCODED_FOUND=$(grep -r -i -E "(password|secret|token|api_key)\s*[:=]\s*['\"][^'\"]+['\"]" . \
        --include="*.js" --include="*.py" --include="*.java" --include="*.ts" \
        --exclude-dir=node_modules --exclude-dir=.git --exclude-dir=venv 2>/dev/null | wc -l)
    
    if [ "$HARDCODED_FOUND" -eq 0 ]; then
        echo "✓ No obvious hardcoded credentials found"
    else
        echo "⚠ Found $HARDCODED_FOUND potential hardcoded credentials - review manually"
    fi
fi
echo ""

echo "Secrets Manager validation complete!"

Common Mistakes to Avoid

Hardcoding credentials "temporarily." Temporary hardcoded credentials often become permanent. Use Secrets Manager from the start, even in development.

Not implementing caching. Calling Secrets Manager on every request increases latency and costs. Implement caching with appropriate TTL.

Overly broad IAM permissions. Grant access only to specific secrets your application needs, not all secrets in the account.

Skipping automatic rotation. Manual rotation rarely happens. Enable automatic rotation for all database credentials.

No error handling for rotation. Applications must handle credential refresh during rotation events gracefully.

Inconsistent naming conventions. Use consistent patterns like environment/service/resource for easy management and IAM policies.

Stop Managing Secrets Manually

Tracking secrets across multiple environments, applications, and teams is complex. AWSight automatically monitors your secrets management posture, detects hardcoded credentials, tracks rotation compliance, and alerts you to exposed secrets before they become breaches.

References