AWS DevOps

AWS Systems Manager Parameter Store Hands-On: App Config & Secrets

Every application needs configuration — a database endpoint, a feature flag, an API key, a Redis URL — and every team eventually learns the hard way that the worst place to keep it is in the code. Hard-coded values force a redeploy to change a URL; a .env file committed “just this once” leaks a password into git forever; environment variables baked into an image tie the config to the artefact instead of the environment. AWS Systems Manager Parameter Store is the AWS-native answer: a managed, hierarchical, versioned key-value store for configuration and secrets, with SecureString values encrypted by KMS, IAM controlling who reads what, and clean integrations into ECS, Lambda, CloudFormation and CodeBuild. And unlike its heavier cousin Secrets Manager, the SecureString type is free.

This article is the working guide. You will build a real parameter hierarchy — plain String config alongside a KMS-SecureString password under a customer-managed key — read the whole tree back in one call with get-parameters-by-path --with-decryption, wire a version label so prod always points at a known-good value, attach an advanced-tier expiration policy, and then consume the parameters from an ECS task definition, a Lambda (both the SDK and the caching Parameters/Secrets extension), a CloudFormation dynamic reference and CodeBuild — all with copy-pasteable aws ssm CLI and equivalent Terraform. Along the way you will learn the mechanics that decide whether this is smooth or a support ticket: standard versus advanced tier, how the partition of trust splits between IAM and KMS for a SecureString, why the parameter ARN drops a slash, and exactly when you should reach past Parameter Store to Secrets Manager instead.

Keep the tables open. Parameter Store is a reference-dense service — types, tier limits, path rules, policy shapes, IAM actions, integration syntaxes, error strings — and this piece is deliberately built as a lookup you return to mid-design and mid-incident. Read the prose once to build the model; come back to the tables when you are naming a hierarchy, scoping a policy, or staring at an AccessDenied you did not expect.

What problem this solves

Configuration sprawl is a silent tax on every team. Without a config store, settings scatter: some in environment variables set in the pipeline, some in a mounted file, some in a wiki, some hard-coded because “it never changes.” Rotating a database password means finding every copy. Promoting a build from staging to production means rebuilding the image because the config is baked in. And secrets end up in the worst possible places — a docker run -e PASSWORD=... in shell history, a committed terraform.tfvars, a Slack message. Every one of these is a breach or an outage waiting for a trigger.

Parameter Store fixes the shape of the problem, not just the storage. Config lives outside the artefact, so the same image runs in dev, staging and prod by reading a different path. Secrets are encrypted at rest with KMS and released only to principals IAM (and the KMS key policy) permit, so “who can read the prod DB password” becomes an auditable IAM question with a CloudTrail record, not a matter of who has the wiki link. Values are versioned, so a bad change is one get-parameter-history away from a rollback, and a label lets you pin “the version prod is allowed to use” independently of the latest edit. And because it is a first-class AWS service, ECS, Lambda, CloudFormation, CodeBuild and EC2 user-data can all pull from it natively — no bespoke config service to run.

Who hits this: every engineer shipping containers or functions that need environment-specific settings; every platform team standardising how secrets reach workloads; every candidate sitting DVA-C02, SOA-C02 or SAA-C03, all of which test SecureString, tiers, hierarchies and the Parameter Store versus Secrets Manager trade-off. This article gives you the mental model, the exact commands, and the failure playbook. For the sibling capabilities of Systems Manager — running commands, patching and shelling into instances without SSH — see SSM Session Manager, Patch Manager & Run Command; this article is the configuration-and-secrets half of the same service.

A quick map of who owns which layer during a Parameter Store design or incident, so you pull in the right person:

Layer What lives here Who usually owns it What it decides / can break
Naming / hierarchy Path scheme /app/env/component/key Platform + app teams Whether IAM can scope cleanly; discoverability
Parameter data Values, types, tiers, versions, labels App / dev team 4 KB vs 8 KB limits, tier cost, rollback
Encryption SecureString, which KMS key Security team Who can decrypt; compliance posture
IAM access ssm:GetParameter*, path ARNs Security + platform Least privilege vs over-broad reads
Consumption ECS secrets, Lambda ext, CFN refs, CodeBuild App + platform Injection failures, cold-start latency
Cost / FinOps Advanced-tier params, higher throughput FinOps + owners Surprise $0.05/param bills; throttling

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need an AWS account with the aws CLI v2 configured (aws configure or SSO — see AWS CLI profiles, SSO & config troubleshooting if that is shaky), permission to call ssm:*, kms:* and iam:* in a sandbox account, and optionally Terraform 1.5+ with the aws provider. Comfort with IAM policies (identity vs resource policies, ARNs) is assumed; if IAM policy evaluation and AccessDenied troubleshooting is unfamiliar, skim it first, because half of Parameter Store pain is really IAM-plus-KMS pain. You do not need prior KMS depth — this article explains the parts you need — but the full envelope-encryption model lives in KMS encryption keys & envelope encryption hands-on.

This sits at the centre of the DevOps and Security tracks. It is downstream of basic IAM and the account setup, and upstream of every workload that needs configuration: it feeds ECS task definitions & services, Lambda functions, CloudFormation stacks and CDK apps. It pairs with Secrets Manager rotation (the paid, rotating alternative for high-value secrets) and with the operational side of Systems Manager in Session Manager, Patch Manager & Run Command.

Core concepts

Start with the vocabulary. Parameter Store is small, but the words carry precise meaning and the exam tests the edges.

Term What it is The important detail
Parameter A single named key-value entry Identified by its name (which may be a hierarchical path)
Type String, StringList, or SecureString Only SecureString is encrypted; the type is set at creation
Tier Standard, Advanced, or Intelligent-Tiering Governs size, count, policies and cost
Value The stored data (≤ 4 KB standard / 8 KB advanced) For SecureString this is the plaintext before KMS encryption
Version A monotonically increasing integer per parameter A new version is created on every overwrite; starts at 1
Label A named alias attached to one version Lets you pin prod/stable to a specific version
Policy A rule (Expiration, etc.) on a parameter Advanced tier only; emits EventBridge events
dataType text (default) or aws:ec2:image Validates AMI IDs when set to aws:ec2:image
Path / hierarchy Slash-delimited name /app/env/key Enables GetParametersByPath and path-scoped IAM
KMS key The key that encrypts a SecureString Default alias/aws/ssm or a customer-managed CMK

The anatomy of a parameter

Every parameter is more than a name and a value — a handful of attributes travel with it, and knowing them prevents most confusion.

Attribute Example Notes
Name /app/prod/db/host Simple (dbHost) or hierarchical (must start with /)
Type SecureString String, StringList, SecureString
Value db.internal:5432 ≤ 4 KB standard, ≤ 8 KB advanced
Tier Advanced Standard / Advanced / Intelligent-Tiering
Version 7 Integer; auto-incremented on overwrite
Labels prod, rollback-safe Up to 10 per version
KeyId alias/aws/ssm or a CMK ARN Only for SecureString
dataType text Or aws:ec2:image for AMI validation
LastModifiedDate / User CloudTrail-visible Who changed it and when
ARN arn:aws:ssm:us-east-1:111122223333:parameter/app/prod/db/host Note: one slash after parameter

That ARN is a recurring gotcha: the parameter name /app/prod/db/host becomes ...:parameter/app/prod/db/host — the leading slash of the name and the parameter/ prefix collapse into a single slash. Get this wrong in an IAM policy and you get a silent AccessDenied.

Parameter types in depth

Parameter Store has exactly three types, and the choice is mostly about encryption.

Type Stored as Encrypted? Use for Gotcha
String Single plain-text value No Non-secret config: URLs, ports, flags, ARNs Anyone with ssm:GetParameter reads it in the clear
StringList Comma-separated values No Lists: subnet IDs, allowed origins, feature sets Split on , in the app; commas in values need escaping/JSON
SecureString KMS-encrypted ciphertext Yes (KMS) Passwords, API keys, tokens, connection strings Reading it needs --with-decryption and kms:Decrypt

A StringList is a convenience, not a data structure — it is a single string that Parameter Store and some integrations know to split on commas. If a value can itself contain a comma, store JSON in a String instead. A SecureString is the only encrypted type; you cannot “encrypt a String” after the fact — you choose SecureString at write time (though you can overwrite a String with a SecureString of the same name, creating a new version).

The dataType field

Independent of the three value types, each parameter carries a dataType that tells AWS how to validate and use the value.

dataType Meaning Effect
text Default; an arbitrary value No validation
aws:ec2:image The value is an EC2 AMI ID SSM validates the AMI exists before accepting the put; lets EC2/ASG/CFN consume it as an image reference
aws:ssm:integration Used by SSM integrations (e.g. webhooks) Reserved for specific integration parameters

Setting --data-type aws:ec2:image on a parameter holding ami-0abcd1234 means a typo or a deregistered AMI is rejected at write time, and services that accept an “SSM parameter AMI reference” (launch templates, Auto Scaling, CloudFormation) can point at the parameter instead of a fixed AMI — so a golden-image update is a single parameter overwrite, not a fleet of edits.

Standard vs advanced tier

The single most cost-and-limit-relevant choice is the tier. It is set per parameter and can be changed.

Dimension Standard Advanced
Max value size 4 KB 8 KB
Max parameters per Region per account 10,000 100,000
Parameter policies (Expiration etc.) ❌ Not supported ✅ Supported
Cost Free (storage) $0.05 per parameter per month (prorated hourly)
Standard throughput Free, ~40 TPS shared Free, ~40 TPS shared
Higher throughput option Available (paid per API call) Available (paid per API call)
Change tier later Standard → Advanced anytime Advanced → Standard only if value ≤ 4 KB and no policies
Best for The overwhelming majority of config + secrets Large values, or parameters needing policies

The rule: default to standard. It is free, holds 10,000 parameters, and covers virtually all config and most secrets (a 4 KB value is a lot of text). You reach for advanced only when a value genuinely exceeds 4 KB (a certificate chain, a large JSON blob) or you need a parameter policy — and both of those are deliberate decisions, not accidents.

Intelligent-Tiering — the surprise-bill lever

Intelligent-Tiering lets Parameter Store pick the tier for you: it uses standard until a parameter needs an advanced feature, then silently upgrades.

Trigger Intelligent-Tiering result
Value ≤ 4 KB, no policy Stays Standard (free)
Value > 4 KB Upgrades to Advanced ($0.05/month)
A parameter policy attached Upgrades to Advanced ($0.05/month)
Already at 10,000 standard params New params created as Advanced

Intelligent-Tiering is convenient but it is exactly how a “free” store starts generating a bill: attach one policy or push one value past 4 KB and that parameter quietly becomes billable. If cost predictability matters, set the tier explicitly (--tier Standard or --tier Advanced) so an upgrade is a choice, not a side effect. This is the number-one Parameter Store billing surprise, and it appears in the troubleshooting playbook.

Limits & quotas to know

Limit Value Note
Standard parameters / Region / account 10,000 Soft-ish; advanced raises the ceiling to 100,000
Advanced parameters / Region / account 100,000 Each billed $0.05/month
Value size (standard / advanced) 4 KB / 8 KB Names + value; hard limit
Max versions per parameter 100 Oldest are pruned only if you delete/label carefully
Labels per version 10 A label points at one version at a time
Label length 100 chars a-zA-Z0-9_.-/; cannot start with a number; latest reserved
Hierarchy depth 15 levels Count the slashes in the path
Parameters per GetParameters call 10 Batch reads; more → paginate or GetParametersByPath
Default throughput ~40 TPS (shared) Increase with higher-throughput setting (paid)
Higher throughput up to 1,000+ TPS $0.05 per 10,000 API interactions when enabled

Hierarchical paths and get-parameters-by-path

A parameter name can be flat (dbHost) or a hierarchy using / as a separator (/app/prod/db/host). Hierarchies are the feature that makes Parameter Store scale organisationally: they enable bulk reads and path-scoped IAM.

Path rule Detail
Leading slash Any name with a / must start with /; a flat name has none
Separator / only; each segment a-zA-Z0-9_.-
Max depth 15 hierarchy levels
Reserved prefixes /aws and /ssm are reserved (public + service parameters)
Case sensitivity Names are case-sensitive
ARN collapse /app/prod/xarn:...:parameter/app/prod/x (single slash after parameter)

A sane scheme is /<app>/<env>/<component>/<key>, e.g. /checkout/prod/db/password, /checkout/staging/db/password. Because IAM can scope to arn:...:parameter/checkout/prod/*, the prod checkout role reads only prod checkout config — the hierarchy is the permission boundary.

The read/write API surface

Operation What it does Reads how many Notes
put-parameter Create or overwrite one parameter — (write) --overwrite to update; new version each time
get-parameter Fetch one parameter by name 1 --with-decryption for SecureString
get-parameters Fetch up to 10 by exact name ≤ 10 Batch; returns InvalidParameters for missing names
get-parameters-by-path Fetch a whole subtree Page (10/call) --recursive, --with-decryption, --parameter-filters
get-parameter-history List all versions of a parameter Page Shows values, labels, policies, who/when
describe-parameters List metadata (no values) Page Resource must be *; cannot be path-scoped in IAM
label-parameter-version Attach/move a label Up to 10 labels/version
unlabel-parameter-version Remove a label
delete-parameter / delete-parameters Delete one / many delete-parameters up to 10 names

get-parameters-by-path options

The workhorse for reading configuration is get-parameters-by-path. It pulls an entire branch in a paginated stream.

Flag Effect Default
--path /app/prod The subtree to read Required
--recursive Include all descendant levels, not just one Off (one level only)
--with-decryption Decrypt SecureString values (needs kms:Decrypt) Off (returns ciphertext markers)
--parameter-filters Filter by Type, KeyId, tag:x, Label etc. None
--max-results Page size (up to 10) 10
--recursive + IAM IAM must allow the path prefix, e.g. parameter/app/prod/*

Two traps live here. First, without --recursive you only get the immediate level/app/prod/x but not /app/prod/db/host — which looks like “half my parameters vanished.” Second, without --with-decryption a SecureString comes back with a placeholder, not the value; add the flag and the kms:Decrypt permission or you get an AccessDeniedException from KMS, not SSM.

Versioning and labels

Every time you overwrite a parameter, Parameter Store keeps the old value and increments an integer version. This is free rollback insurance.

Behaviour Detail
First write Version 1
Each --overwrite Version n+1; old versions retained
Max retained versions 100
Reading a version name:version, e.g. /app/prod/db/host:3
History get-parameter-history --name /app/prod/db/host lists all
Deleting the parameter Removes all versions

Reference syntax — name, version, label

You can address a parameter three ways, and integrations accept all three.

Reference form Resolves to Use when
/app/prod/db/host The latest version Normal reads; you always want current
/app/prod/db/host:3 Version 3 exactly Pin a specific historical value
/app/prod/db/host:prod The version the label prod points at Decouple “what prod uses” from “latest edit”

Labels are the professional pattern. Instead of consumers reading “latest” (which changes the instant anyone edits), they read :release or :prod, and you move that label to a new version only after validation. A bad edit no longer reaches production until you deliberately advance the label — and rolling back is moving the label to the previous version, not re-editing the value.

Label rule Detail
Labels per version Up to 10
Label points at Exactly one version (moving it detaches from the old)
Naming a-zA-Z0-9_.-/; max 100 chars
Cannot Start with a number; be latest; contain spaces
Moving a label label-parameter-version --labels prod --parameter-version 5

Parameter policies (advanced tier)

Parameter policies are automation rules attached to a parameter — and they are advanced-tier only, which is why attaching one triggers the tier upgrade. Each policy emits an event to Amazon EventBridge (formerly CloudWatch Events) so you can react.

Policy type What it does Typical use
Expiration Deletes the parameter at a given ISO 8601 timestamp Temporary credentials, time-boxed feature flags
ExpirationNotification Emits an EventBridge event N days/hours before expiration “This secret expires in 15 days” alert
NoChangeNotification Emits an event if the parameter has not changed in N days Staleness / rotation reminder for un-rotated secrets

You attach policies as a JSON array on put-parameter:

[
  { "Type": "Expiration", "Version": "1.0",
    "Attributes": { "Timestamp": "2026-12-31T23:59:59.000Z" } },
  { "Type": "ExpirationNotification", "Version": "1.0",
    "Attributes": { "Before": "15", "Unit": "Days" } },
  { "Type": "NoChangeNotification", "Version": "1.0",
    "Attributes": { "After": "30", "Unit": "Days" } }
]
Policy detail Behaviour
Requires Advanced tier (attaching one upgrades an Intelligent-Tiering param)
Event target EventBridge default event bus; route to SNS/Lambda
Expiration timing Best-effort around the timestamp, not to-the-second
Combine You can attach Expiration + ExpirationNotification + NoChangeNotification together
Remove Overwrite with an empty policies array []

NoChangeNotification is the closest Parameter Store gets to rotation: it will not rotate a secret for you, but it will nag you (via EventBridge → SNS) that a password has sat unchanged for 90 days. If you need the store to actually rotate the secret, that is the line where you move to Secrets Manager rotation.

SecureString and KMS: the split trust model

A SecureString is encrypted with KMS using envelope encryption — Parameter Store asks KMS for a data key, encrypts the value, and stores the ciphertext. The crucial consequence is that reading a secret requires two permissions from two services.

To do this You need (SSM) You need (KMS)
Write a SecureString ssm:PutParameter kms:GenerateDataKey (+ kms:Encrypt) on the key
Read a SecureString with decryption ssm:GetParameter kms:Decrypt on the key
Read metadata only (no value) ssm:GetParameter none

This is the mistake that generates the most confused tickets: a role has ssm:GetParameter on the parameter, the call still fails, and the error is an AccessDeniedException from KMS, not SSM. The read is a two-lock door.

Which KMS key — aws/ssm vs a CMK

Key choice What it is Who can decrypt When to use
alias/aws/ssm (default) AWS-managed key, auto-created per Region Any principal in the account with ssm:GetParameter (the managed key policy delegates to SSM) Quick start, single-account, low-compliance config
Customer-managed CMK A key you create and control Only principals the CMK key policy (and IAM) explicitly grant kms:Decrypt Compliance, cross-account, separation of duties, audit

With the default aws/ssm key, decryption “just works” for account principals with SSM access — convenient, but you cannot restrict who decrypts beyond the SSM permission, and it does not cross account boundaries. With a CMK, the key policy becomes a second, independent gate: you can allow the app role to decrypt while denying it to everyone else, rotate the key, and grant a role in another account. Compliance-driven and multi-account setups always use a CMK. The envelope-encryption mechanics behind this — data keys, GenerateDataKey, key rotation — are covered in KMS encryption keys & envelope encryption.

Referencing parameters from your workloads

Storing config is half the job; the value is in how cleanly it reaches a running workload. Here is the full menu.

Consumer Mechanism Needs Decrypts SecureString?
CLI / scripts aws ssm get-parameter(s) ssm:GetParameter* (+ kms:Decrypt) With --with-decryption
App via SDK GetParameter / GetParametersByPath Same, on the task/role Yes (SDK flag)
Lambda SDK or Parameters/Secrets extension (cached) Execution role perms Yes
ECS task secrets valueFrom in the task def Execution role perms Yes (injected as env var)
CloudFormation {{resolve:ssm}} / {{resolve:ssm-secure}} Deploying principal perms ssm-secure only
CDK StringParameter.valueForStringParameter etc. Synth/deploy perms Limited (dynamic ref)
CodeBuild env: parameter-store: in buildspec Build role perms Yes
EC2 user-data aws ssm get-parameter at boot Instance role perms Yes
Public AMIs / regions Read /aws/service/... public params None (public) N/A

CloudFormation / CDK dynamic references

CloudFormation resolves Parameter Store values at deploy time via dynamic references.

Reference Resolves Notes
{{resolve:ssm:/app/prod/db/host}} Latest String/StringList Version optional; pin with :version
{{resolve:ssm:/app/prod/db/host:3}} Version 3 Deterministic deploys
{{resolve:ssm-secure:/app/prod/db/pw:2}} SecureString, decrypted Only in supported resource properties; deployer needs kms:Decrypt
CFN Parameter type AWS::SSM::Parameter::Value<String> Latest value at deploy Resolves the value from the parameter you name

The catch with {{resolve:ssm-secure}} is that it is only honoured in a specific allow-list of resource properties (so a secret is not accidentally splashed into a plaintext field), and the value is resolved and used but not stored in the template — good. In CDK, ssm.StringParameter.valueForStringParameter(this, '/app/prod/db/host') produces a CloudFormation dynamic reference; SecureString cannot be resolved at synth time (by design) — you pass it through as a dynamic reference for the deploy. Full stack mechanics are in CloudFormation first stack and CDK with TypeScript.

ECS task definitions — the secrets block

ECS injects parameters as environment variables at container start using the secrets array (distinct from plain environment).

"containerDefinitions": [{
  "name": "api",
  "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/api:1.4",
  "secrets": [
    { "name": "DB_HOST",     "valueFrom": "/checkout/prod/db/host" },
    { "name": "DB_PASSWORD", "valueFrom": "arn:aws:ssm:us-east-1:111122223333:parameter/checkout/prod/db/password" }
  ]
}]
ECS secrets rule Detail
Injected as Environment variables, at container start (not runtime-refreshed)
valueFrom Parameter name (same Region) or full ARN (any Region)
Permission The task execution role (not the task role) needs ssm:GetParameters
SecureString + CMK Execution role also needs kms:Decrypt on the CMK
Network Fargate tasks need a route to the SSM/KMS endpoints (VPC endpoint or NAT)
Change picked up Only on a new task/deployment, not live

The most common ECS mistake is putting the permission on the task role instead of the execution role — the execution role is what the ECS agent uses to pull the image and fetch secrets before your code runs. See ECS task definitions & services for the full role model.

Lambda — SDK vs the caching extension

A Lambda can call GetParameter directly with the SDK, but on a hot path that is a network call per invocation and a throttling risk. The AWS Parameters and Secrets Lambda Extension solves this: it is a layer that runs a local HTTP server which caches parameters in memory.

Extension detail Value
Delivered as A public Lambda layer (regional ARN)
Local endpoint http://localhost:2773/systemsmanager/parameters/get?name=/app/prod/db/host&withDecryption=true
Auth header X-Aws-Parameters-Secrets-Token: $AWS_SESSION_TOKEN
SSM_PARAMETER_STORE_TTL Cache TTL in seconds (default 300)
PARAMETERS_SECRETS_EXTENSION_CACHE_ENABLED true/false (default true)
PARAMETERS_SECRETS_EXTENSION_CACHE_SIZE Max cached items (default 1000)
PARAMETERS_SECRETS_EXTENSION_HTTP_PORT Listener port (default 2773)
SSM_PARAMETER_STORE_TIMEOUT_MILLIS Upstream timeout

With the extension, the first invocation fetches and caches; subsequent invocations within the TTL read from local memory — no SSM call, no throttling, sub-millisecond. The function still needs ssm:GetParameter (+ kms:Decrypt) on its execution role; the extension only caches, it does not grant. For the fundamentals of building the function itself, see Lambda first function hands-on and, for triggers, Lambda event-driven patterns.

CodeBuild

CodeBuild reads parameters into build environment variables directly from the buildspec.

version: 0.2
env:
  parameter-store:
    DB_HOST: /checkout/prod/db/host
    DOCKER_TOKEN: /ci/shared/docker/token   # SecureString: decrypted automatically
phases:
  build:
    commands:
      - echo "Connecting to $DB_HOST"
CodeBuild rule Detail
env: parameter-store: Maps env-var names to parameter names
env: secrets-manager: The equivalent block for Secrets Manager
Permission The CodeBuild service role needs ssm:GetParameters (+ kms:Decrypt)
SecureString Decrypted automatically into the env var (keep build logs clean)

Public parameters — free reads AWS maintains

AWS publishes a large tree of public parameters under /aws/service/... that any account can read for free — the most useful being always-current AMI IDs.

Public parameter Returns
/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 Latest Amazon Linux 2023 AMI ID
/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2 Latest Amazon Linux 2 AMI ID
/aws/service/ami-windows-latest/Windows_Server-2022-English-Full-Base Latest Windows Server 2022 AMI
/aws/service/ecs/optimized-ami/amazon-linux-2023/recommended Recommended ECS-optimised AMI (JSON)
/aws/service/canonical/ubuntu/server/22.04/stable/current/amd64/hvm/ebs-gp2/ami-id Latest Ubuntu 22.04 AMI
/aws/service/global-infrastructure/regions All Region codes (list via get-parameters-by-path)
/aws/service/global-infrastructure/services All service codes

This is why a launch template can say “always use the latest AL2023 AMI” without a hard-coded ID: it references the public parameter, and AWS keeps the value current. Reading them costs nothing and needs no special permission.

Cross-account and cross-region

Parameter Store is regional and, unlike Secrets Manager, has no resource-based policy — which shapes how you share.

Scenario Approach Note
Same account, same Region Direct GetParameter The normal case
Cross-account Assume a role in the owner account, then read No resource policy exists — sharing = sts:AssumeRole + cross-account roles
Cross-Region Parameters do not replicate; recreate or sync Automate with EventBridge + Lambda if you must mirror
Read a Secrets Manager secret via SSM get-parameter --name /aws/reference/secretsmanager/<secret-id> --with-decryption A neat bridge: use the SSM API to fetch a Secrets Manager secret
Public parameters Read directly /aws/service/* is global-read

The lack of a resource policy is a genuine limitation: if account B must read a secret in account A, there is no “share this parameter with account B” button — account B assumes a role in account A and reads it there. Secrets Manager, by contrast, supports resource policies for native cross-account access, which is one of the deciding factors below.

Parameter Store vs Secrets Manager

They overlap enough to confuse and differ enough to matter. This is a heavily-tested decision.

Dimension Parameter Store Secrets Manager
Primary purpose Configuration and secrets Secrets (with lifecycle)
Encryption KMS (SecureString; aws/ssm or CMK) KMS (always; aws/secretsmanager or CMK)
Cost SecureString free (std); advanced $0.05/param/mo $0.40 per secret/mo + $0.05 per 10k API calls
Built-in rotation ❌ None (only NoChangeNotification alert) ✅ Managed rotation (native for RDS/Redshift/DocumentDB; custom via Lambda)
Max value size 4 KB / 8 KB 64 KB
Cross-account ❌ No resource policy (assume-role) ✅ Resource policies (native cross-account)
Random secret generation GetRandomPassword
Versioning Numeric versions + labels Version stages (AWSCURRENT/AWSPREVIOUS/AWSPENDING)
Hierarchy / bulk read ✅ Native paths + GetParametersByPath Naming convention only
CloudFormation ref {{resolve:ssm}} / {{resolve:ssm-secure}} {{resolve:secretsmanager}}
Free tier SecureString free ongoing 30-day trial only

The decision, distilled:

If you need… Choose Because
Plain config (URLs, flags, ARNs) Parameter Store Free String, hierarchy, labels
Secrets with no rotation requirement Parameter Store SecureString Free, KMS-encrypted, IAM-gated
Automatic rotation (DB passwords, keys) Secrets Manager Managed rotation you do not build
A value > 8 KB Secrets Manager 64 KB ceiling
Native cross-account sharing Secrets Manager Resource policies
Thousands of cheap parameters Parameter Store $0.40/secret adds up fast in Secrets Manager
Generated random passwords Secrets Manager GetRandomPassword

Cost makes this concrete. At 1,000 secrets: Secrets Manager is ~$400/month before API calls; Parameter Store SecureString (standard) is $0 for the same 1,000 values. The trade you make for that zero is rotation — Parameter Store will not rotate anything. The mature pattern is both: Parameter Store for the hundreds of config values and low-churn secrets, Secrets Manager for the handful of high-value credentials that must rotate — and where you want the SSM API everywhere, reference the Secrets Manager secret through /aws/reference/secretsmanager/<id>. The rotation half of that story is Secrets Manager rotation hands-on.

Cost example (us-east-1, approx) Parameter Store Secrets Manager
100 config values Free (standard) ~$40/mo
1,000 secrets Free (standard SecureString) ~$400/mo
10 large (10 KB) secrets ~$0.50/mo (advanced) ~$4/mo
1M API reads (higher throughput) ~$5 ~$5
Rotation of a DB password Not available Included (rotation Lambda runs)

Architecture at a glance

The diagram is the model to internalise. Read it left to right. On the left, three kinds of consumer — an ECS task (via its secrets block), a Lambda (via the caching Parameters/Secrets extension on port 2773), and CloudFormation/CodeBuild (via a {{resolve:ssm}} dynamic reference) — all reach the same store. Every read passes through an access gate: an IAM policy scoped to a path ARN decides which subtree the caller may read, and for a SecureString the caller’s role must also carry kms:Decrypt. In the centre sits Parameter Store itself: a hierarchy under /app/prod/* holding plain String values (free, 4 KB, standard tier) alongside a SecureString (advanced tier, 8 KB, policy-bearing). A SecureString read fans right into KMS — the default aws/ssm key or a customer CMK whose key policy is the second lock — and only a principal holding both locks receives the plaintext config in memory. The six numbered badges mark the failures that bite each hop: caching to avoid throttling at the consumer, path-scoped IAM at the gate, the tier decision and the SecureString double-grant in the store, the KMS decrypt grant, and the “free but no rotation” reality at the output.

AWS Systems Manager Parameter Store configuration and secrets architecture: an ECS task using a secrets valueFrom block, a Lambda using the Parameters and Secrets extension on port 2773, and CloudFormation/CodeBuild using a resolve:ssm dynamic reference all call Parameter Store; an access gate of an IAM policy scoped to GetParametersByPath on a path ARN plus a task execution role carrying kms:Decrypt controls the read; Parameter Store holds a hierarchical tree under slash app slash prod with plain String types on the free standard 4 KB tier and a SecureString on the advanced 8 KB tier with a policy; a SecureString read is decrypted by a KMS key that is either the default aws/ssm key or a customer CMK whose key policy grants decrypt; the decrypted plaintext config lands in the application memory via with-decryption; six numbered badges mark caching to avoid throttling, path-scoped IAM, the standard-versus-advanced tier choice, the SecureString two-grant model, the KMS decrypt grant, and the free-but-no-rotation trade-off.

Real-world scenario

LedgerLoop, a fictional fintech, ran forty microservices whose config lived in a tangle of pipeline environment variables and one shared, encrypted .env file baked into a base image. Two incidents forced a rethink in the same quarter. First, a database failover changed an endpoint, and because the endpoint was baked into images, restoring service meant rebuilding and redeploying nineteen services — a four-hour outage for a one-line change. Second, a departing contractor’s laptop still held the shared .env, and with no way to know which of forty services actually used which secret, security had to rotate everything and redeploy the fleet.

The platform team moved config to Parameter Store with a strict hierarchy: /<service>/<env>/<component>/<key>. Non-secret settings (endpoints, ports, feature flags, queue URLs) became free String and StringList parameters on the standard tier; the fifteen genuine secrets per environment (DB passwords, third-party API keys) became SecureString values under a customer-managed CMK per environment, so the prod CMK’s key policy could grant kms:Decrypt only to prod task execution roles — a hard, auditable boundary. Each service’s ECS task definition read its own branch through the secrets block, and the task execution roles were scoped to arn:...:parameter/<service>/prod/* plus kms:Decrypt on the prod CMK — so the payments service literally could not read the ledger service’s secrets, enforced by IAM, not convention.

For the database endpoints — the thing that caused outage one — they used labels. Consumers read /ledger/prod/db/host:active, and a failover became a single label-parameter-version moving the active label to a parameter holding the new endpoint; the next task placement picked it up with no rebuild. For the un-rotated third-party keys they could not yet automate, they attached a NoChangeNotification policy at 90 days (auto-upgrading those to advanced tier at $0.05 each — fifteen keys, about $9/month total) so EventBridge would nag them via Slack when a key went stale. The three highest-value credentials — the core banking DB and two payment-processor keys — went to Secrets Manager with managed rotation, referenced from a couple of services through /aws/reference/secretsmanager/... so the SSM read path stayed uniform.

The measurable outcome:

Metric Before After
Endpoint change → recovery Rebuild + redeploy 19 services (~4 h) Move a label (~seconds)
“Who can read prod payment keys?” Unknown (shared .env) An IAM + KMS query, CloudTrail-audited
Blast radius of one leaked file Entire fleet One service’s branch
Secrets rotated on contractor exit Everything (fear-driven) Only the 3 rotating secrets
Config store monthly cost (hidden in build complexity) ~$9 (advanced policies) + Secrets Manager for 3
Cross-service secret access Implicit, ungoverned Denied by path-scoped IAM

The lesson LedgerLoop took away: the hierarchy is the security model. Once config had a path and secrets had a CMK, “who can read what” stopped being tribal knowledge and became a policy you could read, test and audit.

Advantages and disadvantages

Advantages Disadvantages
SecureString is free — no per-secret charge No built-in rotation (Secrets Manager has it)
Native hierarchy + GetParametersByPath bulk read 4 KB / 8 KB value ceiling (Secrets Manager: 64 KB)
Versioning + labels give free rollback and pinning No resource policy — cross-account needs assume-role
KMS encryption with a CMK for real access control SecureString read needs the two-grant (SSM + KMS) dance
Deep integration: ECS, Lambda, CFN, CDK, CodeBuild, EC2 Advanced tier / Intelligent-Tiering can surprise on cost
Free public parameters (latest AMIs, Regions) ~40 TPS default throughput — hot paths need caching
Fine-grained, path-scoped IAM DescribeParameters cannot be path-scoped in IAM
CloudTrail-audited reads and writes Regional only; no automatic cross-Region replication

When each matters: the advantages dominate for the everyday case — hundreds of config values and low-churn secrets across many services, where free storage, hierarchy and IAM scoping are exactly right. The disadvantages bite when a secret must rotate automatically (use Secrets Manager), when a value is large (>8 KB), or when you need native cross-account sharing — at which points you either add Secrets Manager for those specific secrets or accept an assume-role hop.

Hands-on lab

A complete, mostly-free walk-through: create a KMS CMK, build a parameter hierarchy with String, StringList and a SecureString, read the whole tree decrypted in one call, add a version and a label, attach an advanced-tier expiration policy, wire an ECS task definition and a Lambda to consume it, then tear everything down. Everything runs in us-east-1; swap the Region if you prefer. ⚠️ Standard parameters and SecureString are free; the CMK costs ~$1/month (prorated) and advanced-tier parameters cost $0.05 each/month — the teardown removes them. Schedule the CMK deletion so you are not billed beyond the lab.

Step 1 — Create a customer-managed KMS key for the secret.

KEY_ID=$(aws kms create-key \
  --description "ssm-lab parameter store CMK" \
  --tags TagKey=env,TagValue=lab \
  --query 'KeyMetadata.KeyId' --output text --region us-east-1)

aws kms create-alias --alias-name alias/ssm-lab --target-key-id "$KEY_ID"
echo "CMK: $KEY_ID"

Expected: a key ID (UUID) prints, and the alias alias/ssm-lab is created. You will reference the alias when writing the SecureString.

Step 2 — Put plain config: a String and a StringList (standard tier, free).

aws ssm put-parameter --name "/checkout/prod/db/host" \
  --value "db.checkout.internal:5432" --type String \
  --tier Standard --tags Key=env,Value=lab --region us-east-1

aws ssm put-parameter --name "/checkout/prod/app/allowed-origins" \
  --value "https://app.example.com,https://admin.example.com" --type StringList \
  --tier Standard

Expected: each call returns {"Version": 1, "Tier": "Standard"}. Note the leading slash — these are hierarchical names.

Step 3 — Put a SecureString under the CMK.

aws ssm put-parameter --name "/checkout/prod/db/password" \
  --value "S3cr3t-Pa55!" --type SecureString \
  --key-id "alias/ssm-lab" --tier Standard

Expected: {"Version": 1, "Tier": "Standard"}. The value is now stored encrypted; a read without --with-decryption returns a placeholder, not the password.

Step 4 — Read one parameter, decrypted.

aws ssm get-parameter --name "/checkout/prod/db/password" \
  --with-decryption --query 'Parameter.Value' --output text

Expected: S3cr3t-Pa55!. If you omit --with-decryption you get an encrypted-looking blob; if your caller lacks kms:Decrypt on alias/ssm-lab you get AccessDeniedException from KMS (the two-grant model in action).

Step 5 — Read the whole subtree in one call (the money command).

aws ssm get-parameters-by-path \
  --path "/checkout/prod" --recursive --with-decryption \
  --query 'Parameters[].{Name:Name,Value:Value}' --output table

Expected: a table of all three parameters — host, allowed-origins and the decrypted password — pulled with one API call. Drop --recursive and you get nothing (they are all deeper than one level); drop --with-decryption and the password comes back encrypted.

Step 6 — Create a new version and pin a label.

aws ssm put-parameter --name "/checkout/prod/db/host" \
  --value "db.checkout.internal:6432" --type String --overwrite

aws ssm label-parameter-version --name "/checkout/prod/db/host" \
  --parameter-version 2 --labels active

aws ssm get-parameter --name "/checkout/prod/db/host:active" \
  --query 'Parameter.Value' --output text

Expected: the overwrite returns {"Version": 2}; the label attaches; the labelled read returns db.checkout.internal:6432. Move active back to version 1 and the same read returns the old endpoint — that is your rollback.

Step 7 — Attach an advanced-tier expiration policy.

aws ssm put-parameter --name "/checkout/prod/tmp/launch-token" \
  --value "temporary-value" --type SecureString --key-id "alias/ssm-lab" \
  --tier Advanced \
  --policies '[
    {"Type":"Expiration","Version":"1.0","Attributes":{"Timestamp":"2026-12-31T23:59:59.000Z"}},
    {"Type":"ExpirationNotification","Version":"1.0","Attributes":{"Before":"15","Unit":"Days"}}
  ]'

Expected: {"Version": 1, "Tier": "Advanced"}. ⚠️ This parameter is now advanced tier and costs $0.05/month until deleted. It will auto-delete at the timestamp, and EventBridge fires 15 days before.

Step 8 — Wire an ECS task definition to consume the secret. This is a definition you can register (running it needs a cluster; registration alone is free). The execution role must allow ssm:GetParameters and kms:Decrypt.

{
  "family": "checkout-api",
  "networkMode": "awsvpc",
  "requiresCompatibilities": ["FARGATE"],
  "cpu": "256", "memory": "512",
  "executionRoleArn": "arn:aws:iam::111122223333:role/checkout-exec-role",
  "containerDefinitions": [{
    "name": "api",
    "image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/checkout:1.0",
    "secrets": [
      { "name": "DB_HOST",     "valueFrom": "/checkout/prod/db/host:active" },
      { "name": "DB_PASSWORD", "valueFrom": "/checkout/prod/db/password" }
    ]
  }]
}

The execution role needs this inline policy (note the ARN with a single slash after parameter, and kms:Decrypt on the CMK):

{
  "Version": "2012-10-17",
  "Statement": [
    { "Effect": "Allow", "Action": ["ssm:GetParameters"],
      "Resource": "arn:aws:ssm:us-east-1:111122223333:parameter/checkout/prod/*" },
    { "Effect": "Allow", "Action": ["kms:Decrypt"],
      "Resource": "arn:aws:kms:us-east-1:111122223333:key/KEY-UUID-HERE" }
  ]
}

Step 9 — Consume from Lambda via the caching extension. Attach the public layer and read from the local endpoint. Execution-role permissions are the same two grants.

import os, urllib.request, json

def handler(event, context):
    port = os.environ.get("PARAMETERS_SECRETS_EXTENSION_HTTP_PORT", "2773")
    name = "/checkout/prod/db/password"
    url = f"http://localhost:{port}/systemsmanager/parameters/get?name={name}&withDecryption=true"
    req = urllib.request.Request(url)
    req.add_header("X-Aws-Parameters-Secrets-Token", os.environ["AWS_SESSION_TOKEN"])
    value = json.load(urllib.request.urlopen(req))["Parameter"]["Value"]
    return {"got_secret_len": len(value)}

Expected: the first invocation fetches and caches; subsequent invocations within SSM_PARAMETER_STORE_TTL (default 300 s) return from the extension’s memory with no SSM call. Set the layer ARN for your Region and add ssm:GetParameter + kms:Decrypt to the function role.

Step 10 — Read a public parameter (free, no setup).

aws ssm get-parameter \
  --name "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64" \
  --query 'Parameter.Value' --output text

Expected: the current Amazon Linux 2023 AMI ID (e.g. ami-0abc...) — a value AWS keeps up to date for you.

Step 11 — Teardown (⚠️ stops all charges).

aws ssm delete-parameters --names \
  "/checkout/prod/db/host" "/checkout/prod/app/allowed-origins" \
  "/checkout/prod/db/password" "/checkout/prod/tmp/launch-token"

aws kms delete-alias --alias-name alias/ssm-lab
aws kms schedule-key-deletion --key-id "$KEY_ID" --pending-window-in-days 7

Expected: parameters delete immediately (DeletedParameters lists all four); the CMK is scheduled for deletion in 7 days (the minimum window) and stops incurring the monthly charge once deleted. Confirm with aws ssm describe-parameters --query 'Parameters[?starts_with(Name,\/checkout`)]'` — it should be empty.

The whole lab as Terraform

The same result declaratively — a CMK, the parameter hierarchy including a SecureString, an advanced-tier policy parameter, and the execution-role policy. terraform destroy is your teardown.

resource "aws_kms_key" "ssm" {
  description         = "ssm-lab parameter store CMK"
  enable_key_rotation = true
  tags                = { env = "lab" }
}

resource "aws_kms_alias" "ssm" {
  name          = "alias/ssm-lab"
  target_key_id = aws_kms_key.ssm.key_id
}

resource "aws_ssm_parameter" "db_host" {
  name  = "/checkout/prod/db/host"
  type  = "String"
  value = "db.checkout.internal:5432"
  tier  = "Standard"
  tags  = { env = "lab" }
}

resource "aws_ssm_parameter" "origins" {
  name  = "/checkout/prod/app/allowed-origins"
  type  = "StringList"
  value = "https://app.example.com,https://admin.example.com"
}

resource "aws_ssm_parameter" "db_password" {
  name   = "/checkout/prod/db/password"
  type   = "SecureString"
  key_id = aws_kms_key.ssm.key_id
  value  = "S3cr3t-Pa55!"          # in real code, pass via a variable, never commit
  tier   = "Standard"
}

resource "aws_ssm_parameter" "launch_token" {
  name   = "/checkout/prod/tmp/launch-token"
  type   = "SecureString"
  key_id = aws_kms_key.ssm.key_id
  value  = "temporary-value"
  tier   = "Advanced"
  # Parameter policies via the AWS provider:
  # use the `data_type` / lifecycle and a raw policy string as supported by your provider version.
}

data "aws_iam_policy_document" "exec" {
  statement {
    actions   = ["ssm:GetParameters"]
    resources = ["arn:aws:ssm:us-east-1:${data.aws_caller_identity.me.account_id}:parameter/checkout/prod/*"]
  }
  statement {
    actions   = ["kms:Decrypt"]
    resources = [aws_kms_key.ssm.arn]
  }
}

data "aws_caller_identity" "me" {}

resource "aws_iam_role_policy" "exec" {
  name   = "checkout-ssm-read"
  role   = aws_iam_role.exec.id           # your ECS task execution role
  policy = data.aws_iam_policy_document.exec.json
}

Note the value for a SecureString is stored in Terraform state in plaintext — treat state as sensitive (encrypted remote backend, restricted access), or write the value out-of-band with the CLI and only manage metadata in Terraform. This state-sensitivity point is exactly why some teams keep the highest-value secrets in Secrets Manager and reference them.

Common mistakes & troubleshooting

Parameter Store fails in a small, learnable set of ways — and most of them are really IAM-plus-KMS. Here is the playbook: symptom, root cause, the exact command or console path to confirm, and the fix.

# Symptom Root cause Confirm (command / console) Fix
1 AccessDeniedException on get-parameter --with-decryption even though ssm:GetParameter is allowed Missing kms:Decrypt on the key that encrypts the SecureString The error text names KMS; check the CMK key policy + the caller’s IAM Grant kms:Decrypt on the exact key ARN to the role and in the CMK key policy
2 ParameterNotFound for a parameter you know exists Wrong Region, missing leading /, or a typo in the path aws ssm describe-parameters --region <r>; compare exact name Match the Region (--region), include the leading slash, use the exact case-sensitive name
3 get-parameters-by-path returns nothing No --recursive, so only the immediate level is read Re-run with --recursive; list with describe-parameters Add --recursive; confirm the --path prefix is correct
4 SecureString values come back as ciphertext markers --with-decryption omitted Inspect the returned Value Add --with-decryption (and ensure kms:Decrypt)
5 Unexpected $0.05/parameter charges Advanced tier — often via Intelligent-Tiering auto-upgrade describe-parameters --query 'Parameters[?Tier==\Advanced`]'` Set --tier Standard explicitly; remove policies/large values to downgrade
6 ECS task fails to start: unable to pull secrets or registry auth / ResourceInitializationError Task execution role lacks ssm:GetParameters (or kms:Decrypt for CMK) ECS service events; the stopped-task reason Add the two grants to the execution role (not the task role); verify the parameter ARN
7 ECS injects the wrong/no value valueFrom name/ARN wrong, or parameter in another Region describe-parameters; check valueFrom matches Use the exact name (same Region) or full ARN (cross-Region)
8 ThrottlingException / Rate exceeded under load Too many GetParameter calls above ~40 TPS CloudWatch AWS/Usage for SSM; the SDK error rate Cache (Lambda extension / in-process), batch with GetParameters/GetParametersByPath, enable higher throughput
9 CloudFormation {{resolve:ssm-secure}} does not resolve Used in an unsupported resource property, or deployer lacks kms:Decrypt CFN events; the property’s dynamic-reference support Use it only in supported properties; grant the deploy role kms:Decrypt; pin :version
10 CFN {{resolve:ssm}} returns a stale value Reference not pinned; CFN cached the value at last deploy Compare template value to current parameter Pin :version, or update the stack to re-resolve; use AWS::SSM::Parameter::Value type for deploy-time resolve
11 Cross-account read fails outright Parameter Store has no resource policy Confirm you are in the wrong account Assume a role in the owner account and read there (sts:AssumeRole)
12 label command rejected Label starts with a number, is latest, or exceeds 10/version Read the error; get-parameter-history shows labels Rename the label (letters first), stay under 10 per version
13 put-parameter fails: ValidationException … value ... exceeded maximum Value > 4 KB on standard tier Measure the value size Use --tier Advanced (8 KB) or move the blob to S3 and store the pointer
14 ParameterAlreadyExists on put-parameter Overwriting without --overwrite The error names the conflict Add --overwrite (creates a new version)
15 describe-parameters AccessDenied despite a path-scoped policy DescribeParameters requires Resource * — it is not resource-level Read the policy Resource Grant ssm:DescribeParameters on * (scope reads with GetParametersByPath instead)
16 Lambda extension returns 400 / no value Missing X-Aws-Parameters-Secrets-Token header or wrong port Log the extension response; check env PARAMETERS_SECRETS_EXTENSION_HTTP_PORT Send the header = $AWS_SESSION_TOKEN; use port 2773; grant the function ssm:GetParameter

Error / exception reference

Exception Source Meaning Typical trigger First move
ParameterNotFound SSM No such parameter (in this Region) Typo, wrong Region, missing / Check name + --region with describe-parameters
ParameterAlreadyExists SSM Name exists, no overwrite flag put-parameter without --overwrite Add --overwrite
AccessDeniedException (SSM) SSM/IAM Not allowed the SSM action Missing ssm:GetParameter* or wrong ARN Fix the IAM policy Resource (mind the single slash)
AccessDeniedException (KMS) KMS Not allowed to decrypt Missing kms:Decrypt on the key Grant on the role and the CMK key policy
ThrottlingException SSM Request rate too high Above default TPS Cache, batch, or enable higher throughput
ValidationException SSM Malformed request Value too big, bad policy JSON, bad name Fix the field the message names
ParameterMaxVersionLimitExceeded SSM Over 100 versions Very churny parameter Clean up; reconsider using labels over frequent overwrites
InvalidKeyId SSM/KMS Bad/deleted KMS key Key deleted or wrong Region Point at a valid key in the same Region
HierarchyLevelLimitExceededException SSM Path deeper than 15 levels Over-nested naming Flatten the hierarchy
TooManyUpdates SSM Concurrent writes to the same parameter Parallel put-parameter Serialise writes; back off and retry

The two nastiest real failures

“My role can read the parameter but decryption is denied.” A task or user has ssm:GetParameter on arn:...:parameter/app/prod/*, the metadata reads fine, but --with-decryption throws AccessDeniedException — and reading closely, the denial is from KMS, not SSM. This is the split-trust model: a SecureString read is two locks, and the second (kms:Decrypt) is on the CMK, gated by the CMK’s own key policy in addition to IAM. With the default aws/ssm key it “just works” because that managed key delegates decrypt to SSM for account principals; the moment you move to a CMK (which you should, for real secrets), you must add the principal to the key policy and grant kms:Decrypt in IAM. Teams lose an afternoon here because they instrument SSM and never look at the KMS side. Confirm by reading the CMK key policy (aws kms get-key-policy) and the caller’s effective permissions; fix by granting kms:Decrypt in both places.

“We deployed fine in dev, and prod says ParameterNotFound / injected nothing.” The classic cause is Region, closely followed by the ARN single-slash. Parameter Store is regional, so a parameter created in us-east-1 is invisible from a task or stack in eu-west-1 — the same name, a different store. And when scoping IAM, the parameter name /app/prod/db maps to arn:...:parameter/app/prod/db (one slash after parameter, not parameter//app), so a policy written with two slashes or without the path silently denies. In ECS specifically, layer on the execution-role-not-task-role confusion, and a “config that worked yesterday” fails on a fresh deploy in a new Region. Confirm with describe-parameters --region <target> and by echoing the exact ARN into the policy; fix by keeping consumer and parameter in the same Region (or passing full ARNs) and getting the ARN slash right.

Best practices

Security notes

Parameter Store security is IAM-first, KMS-second, and CloudTrail-audited. There is no network listener; access is entirely through the AWS API.

Control Mechanism Practical guidance
Authorization IAM identity policies on ssm:GetParameter* / ssm:PutParameter Grant only the actions used; never ssm:* in prod
Path scoping Resource ARN parameter/app/prod/* The hierarchy is the boundary; one service reads only its branch
Encryption at rest SecureString + KMS (CMK) Use a per-environment CMK; the key policy is a second gate
Who can decrypt kms:Decrypt on the CMK (IAM + key policy) Restrict to the exact roles; the aws/ssm key cannot be restricted
Encryption in transit TLS to the SSM endpoint Enforced; use the regional endpoint
Private connectivity VPC interface endpoints for ssm and kms Keep reads off the public internet — see VPC subnets & security groups
Auditing CloudTrail records every Get/Put Alarm on GetParameter of prod secrets by unexpected principals
Least privilege for describe ssm:DescribeParameters needs * Prefer GetParametersByPath (path-scoped) for reads
Value hygiene Keep secrets out of logs Never echo a decrypted value in CI; CodeBuild masks parameter-store env

The strongest posture combines path-scoped IAM with a per-environment CMK: a role can name the parameter and must be on the key policy to read it, so “who can read the prod DB password” is answered by two independent, auditable policies. The weakest — and unfortunately most common — is a broad ssm:GetParameters on * with the default aws/ssm key, which lets any principal with SSM access read every secret in the account. The full identity-policy evaluation model behind these decisions is in IAM policy evaluation & AccessDenied troubleshooting.

Cost & sizing

Parameter Store is one of the cheapest useful services in AWS — but there are exactly three levers that turn on a bill. Approximate us-east-1 list prices as of 2026; always verify current pricing.

Cost driver Charge Notes
Standard parameters (any type, incl. SecureString) Free Up to 10,000/Region; storage + standard throughput free
Advanced parameters $0.05 per parameter / month Prorated hourly; needed for >4 KB or policies
Higher-throughput API interactions $0.05 per 10,000 Only when you enable higher throughput
KMS (CMK) ~$1 / key / month + $0.03 per 10k requests The aws/ssm managed key has no monthly charge
Public parameters Free /aws/service/* reads cost nothing

Rough INR at ~₹84/USD: an advanced parameter ≈ ₹4/month; a CMK ≈ ₹84/month; 10,000 higher-throughput reads ≈ ₹4. Sizing guidance:

Situation Recommendation Why
Everyday config + low-churn secrets Standard tier, default Free; 4 KB is ample
A value > 4 KB (cert chain, big JSON) Advanced tier (8 KB) Only way to exceed 4 KB in Parameter Store
Need Expiration / notification policies Advanced tier Policies are advanced-only
Very high read rate (hot config) Cache + maybe higher throughput Caching first; throughput charge only if still needed
Secret must rotate / >8 KB / cross-account Secrets Manager (~$0.40/secret) Parameter Store cannot do these
Thousands of secrets, no rotation needed Parameter Store SecureString Free vs ~$0.40 each in Secrets Manager

The two traps: Intelligent-Tiering silently upgrading parameters to advanced (set tiers explicitly and alarm on advanced count), and reaching for Secrets Manager by reflex for values that never rotate — at 1,000 secrets that reflex is ~$400/month you did not need to spend. Conversely, do not contort Parameter Store to fake rotation; when rotation is the requirement, the ~$0.40/secret for Secrets Manager is the right spend.

Interview & exam questions

Q: What are the three Parameter Store types and which is encrypted? String (single plain value), StringList (comma-separated plain values), and SecureString (KMS-encrypted). Only SecureString is encrypted, and only it needs --with-decryption plus kms:Decrypt to read. (CLF-C02, DVA-C02)

Q: Standard vs advanced tier — the key differences? Standard: 4 KB values, 10,000 parameters/Region, no policies, free. Advanced: 8 KB values, 100,000 parameters, parameter policies supported, $0.05 per parameter/month. Intelligent-Tiering auto-picks and can upgrade (and start billing) when a value exceeds 4 KB or a policy is attached. (DVA-C02, SOA-C02)

Q: What two permissions does reading a SecureString require, and from where? ssm:GetParameter (from SSM/IAM) and kms:Decrypt (from KMS, gated by the CMK’s key policy and IAM). A SecureString read is a two-lock door; the denial is often a KMS AccessDeniedException, not SSM. (DVA-C02, SCS)

Q: How do you read an entire configuration hierarchy in one call? get-parameters-by-path --path /app/prod --recursive --with-decryption. Without --recursive you get only the immediate level; without --with-decryption SecureString values return as ciphertext. (DVA-C02)

Q: Difference between the default aws/ssm key and a customer CMK for SecureString? aws/ssm is AWS-managed, free, and lets any account principal with SSM access decrypt — you cannot restrict who. A CMK is yours: the key policy is an independent, auditable gate, supports cross-account, and is required for real least-privilege and compliance. (SCS, DVA-C02)

Q: How do you promote and roll back configuration safely? Use labels: consumers read name:active; you move the active label to a validated version to promote, and back to the previous version to roll back — without editing the value or redeploying. (DVA-C02)

Q: What are parameter policies and which tier do they need? Expiration (auto-delete at a timestamp), ExpirationNotification (EventBridge event N days before), and NoChangeNotification (event if unchanged for N days). All require the advanced tier and emit EventBridge events. (SOA-C02, DVA-C02)

Q: How does an ECS task consume a SecureString, and what is the classic mistake? Via the secrets block with valueFrom set to the parameter name/ARN; the value is injected as an environment variable at container start. The classic mistake is granting the permission to the task role instead of the task execution role, which is what actually fetches the secret. (DVA-C02)

Q: Parameter Store vs Secrets Manager — when do you pick Secrets Manager? When you need built-in automatic rotation, values larger than 8 KB, native cross-account sharing via resource policies, or generated random passwords. Otherwise Parameter Store SecureString is free and sufficient. (SAA-C03, DVA-C02, SCS)

Q: How do you get the latest Amazon Linux AMI without hard-coding an ID? Read the AWS public parameter /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64; AWS keeps it current, and launch templates/CFN can reference it directly. (SAA-C03, SOA-C02)

Q: You get ThrottlingException reading config under load. Fix? Cache the values — the Parameters/Secrets Lambda extension or an in-process cache — batch with GetParameters/GetParametersByPath, and only if still needed enable the higher-throughput setting (which then bills per API interaction). (DVA-C02, SOA-C02)

Q: Why might a cross-account read of a parameter be impossible directly? Parameter Store has no resource-based policy, so you cannot share a parameter to another account; the consumer must assume a role in the owning account and read there. Secrets Manager, by contrast, supports resource policies for native cross-account access. (SCS, SAA-C03)

Quick check

  1. You store a database password as a SecureString under a customer CMK. A role has ssm:GetParameter on the exact ARN but the decrypted read fails. What is missing and where?
  2. Which single command reads every parameter under /checkout/prod, including nested ones, with secrets decrypted?
  3. A “free” Parameter Store starts showing $0.05 charges per parameter. What silently caused it, and how do you prevent it?
  4. You need the config change on a database failover to reach consumers with no rebuild. What Parameter Store feature makes that a one-step operation?
  5. When do you choose Secrets Manager over a Parameter Store SecureString?

Answers

  1. kms:Decrypt on the CMK — granted in both the role’s IAM policy and the CMK’s key policy. A SecureString read is two locks (SSM + KMS); the denial comes from KMS.
  2. aws ssm get-parameters-by-path --path /checkout/prod --recursive --with-decryption. --recursive gets nested levels; --with-decryption decrypts SecureString values (and needs kms:Decrypt).
  3. Intelligent-Tiering auto-upgraded parameters to advanced tier because a value exceeded 4 KB or a policy was attached. Prevent it by setting --tier Standard explicitly and alarming on advanced-parameter count.
  4. Version labels. Consumers read name:active; on failover you move the active label (or overwrite + relabel) to the parameter holding the new endpoint, and the next task/read picks it up — no rebuild.
  5. When you need automatic rotation, values > 8 KB, native cross-account sharing (resource policies), or generated random passwords. Otherwise SecureString is free and sufficient.

Glossary

Term Definition
Parameter A named key-value entry in Parameter Store, addressed by name or hierarchical path.
String / StringList Unencrypted single value / comma-separated list of values.
SecureString A KMS-encrypted parameter value; the only encrypted type.
Tier Standard (free, 4 KB), Advanced (paid, 8 KB, policies), or Intelligent-Tiering.
Parameter policy An advanced-tier rule (Expiration, ExpirationNotification, NoChangeNotification) emitting EventBridge events.
Version The auto-incrementing integer created on each overwrite; retained up to 100.
Label A named alias pinned to one version, used for promotion and rollback.
Hierarchy / path A slash-delimited name (/app/env/key) enabling bulk read and path-scoped IAM.
GetParametersByPath The API that reads a whole subtree (--recursive, --with-decryption).
dataType text or aws:ec2:image (validates AMI IDs).
CMK A customer-managed KMS key whose key policy independently gates SecureString decryption.
aws/ssm key The default AWS-managed KMS key for SecureString; free, unrestrictable.
Dynamic reference CloudFormation {{resolve:ssm}} / {{resolve:ssm-secure}} that resolves a parameter at deploy time.
Parameters and Secrets extension A Lambda layer that caches parameters behind a local endpoint on port 2773.
Public parameter An AWS-maintained read-only parameter under /aws/service/* (e.g. latest AMIs).

Next steps

AWSSystems ManagerParameter StoreSecureStringKMSSecrets ManagerECSDVA-C02
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading