Secret Manager
What is AWS Secrets Manager & Why Use It
AWS Secrets Manager is a managed service for storing, retrieving, and rotating secrets (e.g. database credentials, API tokens). It offers:
- Encryption at rest and in transit
- Fine-grained access control (IAM)
- Automated secret rotation (via Lambda)
- Auditability (via CloudTrail)
- Centralized secret management across your AWS environment
- Integration with various AWS services (RDS, etc.) ([AWS Documentation][1])
So in a Django context, instead of storing credentials in settings.py
, .env
files, or config repos, you can fetch them at runtime (or at startup) from Secrets Manager.
That said, doing this properly requires care: you must balance security, performance, resilience, and maintainability.
Key Concepts & Capabilities
Before diving into Django details, here are key features of Secrets Manager to understand:
Feature | Description |
---|---|
Secret | A named object (often JSON) storing key/value pairs (e.g. {"username": "...", "password": "...", "host": "..."} ) |
Version / Version Stages | Each secret can have multiple versions (e.g. “AWSCURRENT”, “AWSPREVIOUS”) |
Secret Rotation | You can hook up a Lambda that periodically changes the secret and updates dependent services |
Access control | IAM policies can restrict which identities (roles, users) can access which secrets |
Caching & Rate Limits | API calls have latency and quotas, so often you’ll want to cache secrets locally |
Failure / fallback | If Secrets Manager is unavailable or permissions fail, you must decide what to do (fail, fallback) |
Also, you should understand IAM roles, network access (if your Django service runs in a VPC), and AWS SDK (boto3) for fetching.
Integration Strategies in Django
There are multiple ways to integrate AWS Secrets Manager into a Django project. Each has tradeoffs. I’ll walk through them, and then propose a robust pattern.
Where to fetch secrets: startup vs runtime
At Django startup (in settings module) Load secrets when
settings.py
is imported (or insettings_base
, etc.). This ensures all configuration is known at start time. Pros: Simpler, easier to reason about. Cons: If secrets fail to load, the app fails to start. Also, if you rotate secrets, app restart may be required, unless you build refresh logic.Lazy / on-demand fetch Only fetch the secret when a component first needs it (e.g. when connecting to the DB). Use caching so you don’t hit the Secrets API on every request. Pros: More resilient to transient failures; you can refresh. Cons: More complexity, possibly slight runtime overhead on first access.
Middleware / dynamic refresh More advanced: periodically re-fetch the secret while the app is running (for secret rotation without downtime). This is trickier and must be done carefully to avoid mid-request inconsistencies.
Secret structure & naming conventions
You’ll usually store secrets as JSON structured objects. For instance:
{ "username": "dbuser", "password": "supersecret", "engine": "postgres", "host": "mydb.xxxx.region.rds.amazonaws.com", "port": 5432, "dbname": "myappdb" }
You might have separate secrets for DB credentials, for third-party API keys, for other secrets (e.g.
django/SECRET_KEY
). Use naming or path conventions, e.g.project/prod/db
,project/prod/django
, etc.Use versioning and rotation carefully. When rotating, ensure the new version is “promoted” before old clients fail.
Caching & client reuse
- Boto3 clients should be reused rather than recreating per call (to avoid overhead).
- Implement caching (in-memory, or a small TTL) so you don’t hit Secrets Manager for every request.
- If you support refresh, you might invalidate the cache contextually (e.g. on DB connection failure).
Permissions and access
- Whatever service is running Django (EC2, ECS, Lambda, etc.) should use an IAM role (instance profile or task role) with minimal permissions to read the required secrets.
- Use least privilege: only allow
secretsmanager:GetSecretValue
(and perhapsListSecrets
) on the secrets your app needs. - If you use secret rotation, you may need
secretsmanager:RotateSecret
or permissions to invoke the rotation Lambda, depending on configuration.
Network & environment
- If your Django app is running inside a VPC, ensure it has outbound access to Secrets Manager (which is a public endpoint or via VPC endpoints).
- Optionally, use VPC interface endpoints (AWS PrivateLink) for Secrets Manager, so traffic stays within your VPC.
- Ensure DNS / routing works so your app can call AWS APIs.
Libraries & Packages to Help
You don’t have to write everything from scratch. Several Django / Python libraries exist to help integrate with Secrets Manager:
Package | What it provides / pros & cons |
---|---|
django-secrets-manager (LeeHanYeong) | A library to help fetch secrets into Django settings. ([GitHub][2]) |
django-simple-secrets | Provides a simple interface, caching, lazy loading for AWS Secrets Manager. ([PyPI][3]) |
govtech-csg-xcg-secretsmanager | Offers integration for fetching DB credentials and rotating Django SECRET_KEY . Supports custom DB backends to transparently fetch. ([GitHub][4]) |
django-aws-secrets-env-setup | A helper that sets environment variables from AWS Secrets Manager, so you can treat them as “normal” env variables. ([GitHub][5]) |
These libraries relieve some boilerplate (caching, error handling, rotation hooks). But they also add dependencies, so evaluate maintenance and stability.
Best Practices & Recommendations
Here’s a set of guidelines and tips to follow when integrating Secrets Manager with Django.
Use the IAM role / instance profile approach Don’t embed AWS access keys in your code. Let your runtime environment assume an IAM role that has permission to access required secrets.
Minimize secret requests with caching Don’t call AWS Secrets API per request. Use a local cache with an expiration (e.g. 15 min or more). Use client reuse (keep boto3 client), and exponential backoff / retry for transient failures.
Graceful fallback & error handling
- If fetching secret fails, either fail fast (so you see the error) or fallback to a safe default or earlier-known secret.
- For database credentials, if the secret changes mid-flight, catch failures, clear cache, re-fetch, retry.
- Handle timeouts, throttling (Secrets Manager has API limits).
Secret rotation & zero downtime
- Use AWS’s built-in rotation or custom Lambda to rotate secrets.
- Ensure the Django app can pick up new credentials without manual redeploy (or gracefully swap).
- You may need to build refresh logic at the database connector layer.
Versioning and staging Use version stages (
AWSCURRENT
,AWSPENDING
) so new secret versions can be tested before promoting. If your app is multi-instance, ensure all instances see the new secret smoothly.Limit secret scopes Don’t bundle all secrets into one JSON blob unless they always rotate together. Use separate secrets for independent concerns (DB credentials, third-party API keys, etc).
Audit & monitoring
- Enable CloudTrail to log secrets access.
- Monitor
GetSecretValue
calls, errors, throttling. - On rotation failures, alert early.
Local development & CI support
- For local dev, you may fallback to local
.env
or AWS credentials in~/.aws/credentials
. - Consider a “mock” or stub secret fetcher in non-production environments.
- Secure secrets (e.g. not commit them in tests or repos).
- For local dev, you may fallback to local
Avoid doing heavy logic in
settings.py
While it’s common to fetch secrets insidesettings.py
, that logic runs at startup and can break introspection, builds, or non-web commands. Keep it lean, with timeouts and fallback. (Many discussions on whether it’s OK). ([Stack Overflow][6])Test rotation / failover Simulate secret changes, and verify that your application recovers gracefully (e.g. reconnect using new credentials).
Example Integration Pattern
Here’s a sample pattern for integrating AWS Secrets Manager for database credentials in Django.
1. Create a Secret in AWS
Name: myapp/prod/db
Value (JSON):
{
"username": "dbuser",
"password": "supersecret",
"engine": "postgres",
"host": "mydb.xxxx.rds.amazonaws.com",
"port": 5432,
"dbname": "myappdb"
}
Ensure your Django app’s IAM role has secretsmanager:GetSecretValue
permission for arn:aws:secretsmanager:region:acct-id:secret:myapp/prod/db
.
2. Write a small helper to fetch & cache the secret
# secrets.py
import json
import threading
import time
import boto3
from botocore.exceptions import ClientError
= threading.Lock()
_lock = {}
_cache = 300 # seconds
_CACHE_TTL
def get_secret(secret_name, region_name=None):
"""Fetch secret and cache it."""
= time.time()
now = _cache.get(secret_name)
entry if entry and now < entry["expires_at"]:
return entry["value"]
# Acquire lock to refresh
with _lock:
= _cache.get(secret_name)
entry if entry and now < entry["expires_at"]:
return entry["value"]
# Actually fetch
= boto3.session.Session()
session = session.client("secretsmanager", region_name=region_name)
client = client.get_secret_value(SecretId=secret_name)
resp = resp.get("SecretString")
secret_str if secret_str is None:
# You may need to handle binary secrets
raise RuntimeError("Secret has no string value")
= json.loads(secret_str)
data = {
_cache[secret_name] "value": data,
"expires_at": time.time() + _CACHE_TTL
}return data
This code:
- Caches fetched secrets for 5 minutes
- Uses locking to avoid duplicate fetches in concurrent threads
- Raises errors if things go wrong
You can extend it (retry, fallback, clear cache) as needed.
3. Use in settings.py
# settings.py (or settings/base.py)
from .secrets import get_secret
# Optionally get AWS region from env
= os.getenv("AWS_REGION", "ap-southeast-1")
AWS_REGION = os.getenv("DB_SECRET_NAME", "myapp/prod/db")
DB_SECRET_NAME
= get_secret(DB_SECRET_NAME, region_name=AWS_REGION)
_db_secrets
= {
DATABASES "default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": _db_secrets["dbname"],
"USER": _db_secrets["username"],
"PASSWORD": _db_secrets["password"],
"HOST": _db_secrets["host"],
"PORT": _db_secrets.get("port", 5432),
"OPTIONS": {
# e.g. ssl mode if you require TLS
"sslmode": "require",
},
}
}
# Similarly, load SECRET_KEY from a separate secret
= get_secret("myapp/prod/django", region_name=AWS_REGION)["SECRET_KEY"]
DJANGO_SECRET_KEY = DJANGO_SECRET_KEY SECRET_KEY
You could also fetch other secrets (e.g. third-party APIs) similarly.
4. Handle rotation / secret change gracefully
When rotation happens:
- If a new credential version becomes current, your next fetch (after cache expiry) will see it
- But if your DB connection is already open with old credentials, you may need to detect failures (e.g. “authentication failed”) and on such exception, clear the cache, re-fetch, and retry connection
- Optionally, you can implement a background thread that periodically refreshes the cache (if your application is long-running).
Alternatively, libraries like govtech-csg-xcg-secretsmanager provide custom DB backends that perform such logic transparently. ([GitHub][4])
Tradeoffs, Challenges & Gotchas
- Latency / cold start overhead: The first fetch adds latency. Mitigate via caching or fetching early (e.g. at app bootstrap).
- Secret Manager availability / throttling: If the AWS API is temporarily unavailable or exceeds rate limits, your app must cope (timeouts, fallback).
- Secret rotation complexity: If secrets change (e.g. DB password), existing connections may break. You need handling logic (clear cache, reconnect).
- Testing / local development: In local dev or CI, you may not have Secrets Manager access. Provide fallback (e.g. local env files).
- Doing too much in settings: If your
settings.py
imports dependencies or has network calls, tools (e.g.manage.py
ordjango-admin
) might fail or misbehave in contexts (e.g.collectstatic
) - Thread safety & concurrency: If your app has multiple threads or processes, ensure your caching logic is thread/process-safe.
- Secrets growth & cost: If you have many secrets or frequent rotations, watch costs and API usage.
- IAM misconfiguration: Lack of correct IAM permissions is a usual cause of runtime errors.
Summary / Checklist
Here’s a distilled checklist for using AWS Secrets Manager in Django:
- Define secrets in AWS (structure as JSON, name them well)
- Grant minimal IAM permissions to your app’s runtime role
- Implement or use a library to fetch and cache secrets
- Integrate secret fetch into
settings.py
(or lazy load) - Use retries, error handling, fallback mechanisms
- Plan for secret rotation and graceful application behavior on credential change
- For local/dev, provide alternative (env vars or mock)
- Monitor usage / audit accesses via CloudTrail
- Test secret rotation and failure recovery
- Keep secret logic simple and maintainable