A hardcoded credential is the single most preventable security incident in an AWS SDK codebase, and it keeps happening because environment variables feel safe enough - until a .env file gets committed, an env dump lands in a log aggregator, or a CI job prints its full environment on failure.
The fix is to stop treating secrets as configuration and start treating them as data your application fetches at runtime, from a service designed to store them: AWS Secrets Manager or SSM Parameter Store's SecureString type.
Fetch a secret once at startup, cache it in memory, and never write it to a log - including on the error path, where a naive catch block often prints the whole exception context.
# --- Python (boto3) ---import boto3import logginglogger = logging.getLogger("app")sm = boto3.client("secretsmanager", region_name="us-east-1")def load_db_password() -> str: try: resp = sm.get_secret_value(SecretId="prod/db/password") except Exception as exc: # Log the error type and secret id, never the exception's full context # if it could carry response data. logger.error("failed to load secret prod/db/password: %s", type(exc).__name__) raise # Log that a fetch happened, with the version id - never the SecretString. logger.info("loaded secret prod/db/password version=%s", resp["VersionId"]) return resp["SecretString"]DB_PASSWORD = load_db_password() # cached once, not re-fetched per request
// --- TypeScript (AWS SDK v3) ---import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";const sm = new SecretsManagerClient({ region: "us-east-1" });async function loadDbPassword(): Promise<string> { let resp; try { resp = await sm.send(new GetSecretValueCommand({ SecretId: "prod/db/password" })); } catch (err) { // Log the error name and secret id, never the raw error object. console.error("failed to load secret prod/db/password:", (err as Error).name); throw err; } // Log that a fetch happened, with the version id - never SecretString. console.log("loaded secret prod/db/password version=", resp.VersionId); return resp.SecretString!;}const dbPassword = await loadDbPassword(); // cached once, not re-fetched per request
What this demonstrates:
The secret is fetched once and held in memory, not re-read from disk or an env var on every request.
The success log line names the secret id and version, never SecretString.
The error path logs the exception type, not the raw exception object, which can sometimes carry response bodies.
Caching avoids hammering Secrets Manager with a GetSecretValue call per request, which also has a per-call cost.
Both encrypt at rest with KMS and both are fetched at runtime rather than baked into a deploy artifact. The difference is operational weight.
Aspect
Secrets Manager
Parameter Store SecureString
Automatic rotation
Built in, with Lambda rotation functions
Not built in
Versioning
Full version history via VersionId/VersionStage
Basic version history
Cost
Per-secret monthly charge
No extra charge for standard parameters
Best for
Database credentials, anything needing rotation
API keys, config values, simpler secrets
Parameter Store's SecureString type is covered in depth in the Systems Manager section - see Parameter Store: Standard vs Advanced Parameters for the create/read pattern with KMS-backed encryption.
An env var holding a raw secret is readable by anything with process-inspection access, appears in crash dumps, and is a common accidental leak point in CI logs and container orchestration dashboards.
Using Secrets Manager or SecureString does not mean avoiding environment variables entirely - it is common to put the secret's identifier (SecretId or parameter name) in an env var, then fetch the actual value at startup. The env var is safe to log; the value it points to never is.
Never print/console.log a response object from GetSecretValue or GetParameter with WithDecryption=True.
Review structured logging middleware (request/response loggers, APM agents) for whether it serializes full objects by default - many do, and will happily capture a secret buried in a return value.
Redact known-sensitive fields (SecretString, Value, password, token) before anything reaches a logger, even at debug level.
Treat CloudTrail data events and application logs as different trust boundaries - CloudTrail records that a GetSecretValue call happened, not its result, which is exactly the split you want.
Logging the whole SDK response "just for debugging." - Fix: log only the id and version fields; never the payload.
Putting the raw secret value in an environment variable at deploy time. - Fix: put the secret's identifier in the env var, and fetch the value from Secrets Manager or Parameter Store at startup.
Re-fetching the secret on every request. - Fix: cache it in memory after the first fetch, and only re-fetch on a rotation event or a defined TTL.
Assuming KMS encryption at rest means logging is safe. - Fix: encryption at rest protects the stored value; it does nothing once your own code decrypts and prints it.
Committing a .env file with real values "temporarily." - Fix: keep .env in .gitignore from day one, and use Secrets Manager/Parameter Store for anything that isn't purely local dev config.
Forgetting rotation invalidates a cached secret. - Fix: if using Secrets Manager rotation, either re-fetch on a schedule shorter than the rotation window or catch the auth failure and refresh once.
Should I ever put a raw secret value in an environment variable?
Avoid it beyond isolated local development. In shared, staging, or production environments, put the secret's identifier (a SecretId or parameter name) in the env var, and fetch the actual value from Secrets Manager or Parameter Store at runtime.
What's the difference between Secrets Manager and SecureString parameters?
Both encrypt with KMS and are fetched at runtime. Secrets Manager adds built-in rotation and richer versioning at a per-secret cost; SecureString parameters are lighter-weight and free for values that don't need automatic rotation.
Is it safe to log the response from GetSecretValue?
No. The response includes SecretString in plaintext. Log only Name and VersionId, never the full response object.
Does KMS encryption at rest protect me if I log the value myself?
No. KMS protects the stored, encrypted value. Once your application calls the API and decrypts it, that protection ends - what your code does with the plaintext value afterward is entirely on you.
How often should I re-fetch a cached secret?
Cache after the first fetch and re-fetch only on a defined schedule shorter than any rotation window, or when an authentication failure suggests the cached value is stale.
What should I do if a secret already leaked into source control?
Rotate the secret immediately in Secrets Manager or by replacing the parameter value - assume anything committed to version control is compromised, since history persists even after a later commit removes it.
Can I redact secrets automatically in my logs?
Yes, with a redaction step in your logging pipeline that strips known-sensitive field names (SecretString, password, token, Authorization) before anything is written, regardless of log level.