<< BACK

Secrets Manager

Structuring secrets as validated JSON, KMS encryption, guarding against empty deploys, and when to choose Secrets Manager over SSM Parameter Store.

DATE:
AUG.22.2025
READ:
10 MIN

Secrets Manager vs SSM Parameter Store

Both services store configuration data. They overlap enough to cause confusion, but the decision criteria are straightforward.

+---------------+--------------------+------------------+
| Feature       | Secrets Manager    | SSM SecureString |
+---------------+--------------------+------------------+
| Cost          | $0.40/secret/month | Free (standard)  |
+---------------+--------------------+------------------+
| Rotation      | Built-in Lambda    | Manual           |
+---------------+--------------------+------------------+
| Cross-account | Resource policy    | Role assumption  |
+---------------+--------------------+------------------+
| Max size      | 64 KB              | 8 KB (advanced)  |
+---------------+--------------------+------------------+
| Versioning    | Staging labels     | Version numbers  |
+---------------+--------------------+------------------+

Use Secrets Manager when you need automatic rotation or cross-account sharing via resource policies. Use SSM Parameter Store for everything else. Most teams over-adopt Secrets Manager and pay for capabilities they never use.

Secret structure

A single Secrets Manager secret holds a JSON document with multiple key-value pairs. One secret containing DB_HOST, DB_USER, DB_PASSWORD, DB_PORT, and JWT_SECRET costs $0.40/month. Five separate secrets cost $2.00/month. Structure your secrets as JSON objects.

++
secret = secretsmanager.Secret(
    self, "DatabaseSecrets",
    secret_name="/acmecorp/development",
    encryption_key=kms_key,
    secret_object_value={
        key: SecretValue.unsafe_plain_text(value)
        for key, value in dev_secrets.items()
    },
)
++

The secret_object_value parameter accepts a dictionary and serializes it to JSON. Each key becomes retrievable individually via SecretValue.secrets_manager() with a json_field argument, or you parse the full JSON client-side.

Namespace your secrets by environment: /acmecorp/development, /acmecorp/staging, /acmecorp/production. The path convention keeps IAM policies clean and prevents accidental cross-environment reads.

Validation before deploy

The worst failure mode is deploying empty or partial secrets over live credentials. A missing DB_PASSWORD key means your application starts, connects to nothing, and fails at runtime instead of at deploy time.

Guard against this with Pydantic validation during CDK synthesis:

++
class AppSettings(BaseSettings):
    class Config:
        env_file = ".env"
        extra = "allow"
        case_sensitive = True

settings = AppSettings()
dev = {
    k.replace("DEV_", ""): v
    for k, v in settings.model_dump().items()
    if k.startswith("DEV_")
}

REQUIRED_KEYS = [
    "DB_HOST", "DB_USER", "DB_PASSWORD",
    "DB_PORT", "JWT_SECRET",
]
missing = [k for k in REQUIRED_KEYS if k not in dev]
if missing:
    raise ValueError(
        f"Refusing to synth — missing keys: {missing}"
    )
++

The check runs before CloudFormation even sees the template. If your .env file is missing a required key, synthesis fails loudly. No ambiguity, no partial deploys.

KMS encryption

Every Secrets Manager secret is encrypted at rest. The question is whether you use the AWS-managed key (aws/secretsmanager) or a customer-managed CMK.

You need a CMK when:

  • Cross-account access — the consuming account needs kms:Decrypt permission on the key, which requires a key policy you control.
  • Custom rotation schedule — CMKs support annual automatic rotation. The AWS-managed key rotates on a fixed schedule you cannot change.
  • Audit trail — CMK usage appears in CloudTrail with your key ARN, making it possible to trace which service decrypted which secret and when.
++
kms_key = kms.Key(
    self, "SecretsKmsKey",
    enable_key_rotation=True,
    removal_policy=RemovalPolicy.RETAIN,
)
++

Set removal_policy to RETAIN. Deleting a KMS key is irreversible after the waiting period, and any secret encrypted with it becomes permanently unreadable. There is no recovery path.

The rotation protocol

Secrets Manager rotation follows a four-step Lambda protocol:

  1. createSecret — Generate the new credential and store it under the AWSPENDING staging label.
  2. setSecret — Apply the new credential to the target service (database, API, etc.).
  3. testSecret — Validate the pending credential works by authenticating against the target.
  4. finishSecret — Move AWSCURRENT to AWSPREVIOUS, promote AWSPENDING to AWSCURRENT.

The staging labels AWSCURRENT, AWSPENDING, and AWSPREVIOUS allow rollback. If testSecret fails, AWSCURRENT remains untouched and the application never sees the broken credential.

This is the same pattern used for IAM access key rotation. See the IAM Key Rotation post for a concrete implementation that walks through each step with working code.

Cost optimization

The math is simple:

  • One secret with 10 JSON keys: $0.40/month.
  • Ten separate secrets with one key each: $4.00/month.

Combine related credentials into single secrets. A database connection needs host, port, user, and password — that is one secret, not four. An application environment needs database credentials, API keys, and signing secrets — still one secret if they deploy together.

Beyond storage costs, every GetSecretValue API call costs $0.05 per 10,000 requests. Client-side caching eliminates redundant calls. The AWS Secrets Manager caching library holds decrypted values in memory with a configurable TTL. A Lambda function that runs 100,000 times per month and fetches the same secret each time pays $0.50 in API calls without caching, and $0.00 with it.

++
from aws_secretsmanager_caching import SecretCache

cache = SecretCache()
secret_string = cache.get_secret_string(
    "/acmecorp/production"
)
++

The cache respects rotation. When AWSCURRENT changes, the next cache miss after TTL expiry picks up the new value automatically.

Secrets management is not a feature you build once. It is a discipline — validate before deploy, encrypt with keys you control, rotate on a schedule, and never pay for ten secrets when one will do.