Intermediate
Introduction: Why Parameter Store with KMS Matters
If you’re still storing database credentials in .env files, passing secrets through environment variables baked into container definitions, or — worse — committing them to version control, this article is for you.
AWS Systems Manager Parameter Store provides a centralized, auditable, and secure way to manage configuration data and secrets. When combined with SecureString parameters encrypted by AWS KMS (Key Management Service) and organized in a hierarchical path structure, you get a production-grade configuration management system that scales cleanly across multiple environments — dev, staging, and production — without duplication headaches or security gaps.
This article is written for intermediate-level AWS practitioners — developers and DevOps engineers who already know the basics of AWS but want a concrete, working approach to secrets and configuration management. We’ll cover real CLI commands, Python code, IAM policies, KMS key setup, and the common mistakes that trip people up in production.
Prerequisites
- An AWS account with permissions to create SSM parameters, KMS keys, and IAM policies
- AWS CLI v2 installed and configured (
aws configure) - Python 3.8+ with
boto3installed (pip install boto3) - Basic understanding of IAM policies and ARN structures
- Familiarity with at least one deployment environment (ECS, Lambda, EC2)
Understanding the Hierarchy: Designing Your Parameter Path Structure
Parameter Store supports hierarchical paths using forward slashes, up to 15 levels deep. This is the foundation of clean multi-environment configuration. The key insight is to treat your parameter paths like a filesystem.
Here’s the structure I recommend and use in production:
/{environment}/{service-name}/{parameter-type}/{parameter-name}
For a concrete example, consider a web application called “order-api” with database and API credentials across three environments:
/dev/order-api/database/host
/dev/order-api/database/port
/dev/order-api/database/username
/dev/order-api/database/password # SecureString
/dev/order-api/database/connection_string # SecureString
/dev/order-api/stripe/api_key # SecureString
/dev/order-api/config/log_level
/dev/order-api/config/feature_flag_v2
/staging/order-api/database/host
/staging/order-api/database/password # SecureString
/prod/order-api/database/host
/prod/order-api/database/password # SecureString
This hierarchy gives you three critical capabilities:
- Bulk retrieval: Fetch all parameters for a service in one API call using
GetParametersByPath - Granular IAM control: Restrict access by environment path (e.g., developers can read
/dev/*but not/prod/*) - Environment parity: Same parameter names across environments, only the prefix changes
Setting Up a Custom KMS Key for Parameter Encryption
Parameter Store SecureString parameters are encrypted by default using the AWS managed key alias/aws/ssm. However, in any serious multi-environment setup, you should create custom KMS keys — ideally one per environment. Here’s why:
- You can’t control the key policy on the AWS managed key
- You can’t distinguish access to dev secrets vs. prod secrets at the KMS layer
- You can’t use the AWS managed key for cross-account access
- CloudTrail logging for custom keys gives you cleaner audit trails
Create a KMS Key Per Environment
# Create a KMS key for production secrets
aws kms create-key \
--description "SSM Parameter Store encryption key for prod environment" \
--tags TagKey=Environment,TagValue=prod TagKey=ManagedBy,TagValue=devops \
--region us-east-1
# Note the KeyId from the output, then create an alias
aws kms create-alias \
--alias-name alias/ssm-prod \
--target-key-id <key-id-from-output> \
--region us-east-1
# Repeat for dev and staging
aws kms create-key \
--description "SSM Parameter Store encryption key for dev environment" \
--tags TagKey=Environment,TagValue=dev TagKey=ManagedBy,TagValue=devops \
--region us-east-1
aws kms create-alias \
--alias-name alias/ssm-dev \
--target-key-id <key-id-from-output> \
--region us-east-1
Creating and Managing SecureString Parameters
Creating Parameters via AWS CLI
Let’s create a full set of parameters for the dev environment:
# Plain String parameter — no encryption needed for non-sensitive config
aws ssm put-parameter \
--name "/dev/order-api/config/log_level" \
--value "DEBUG" \
--type String \
--tags "Key=Environment,Value=dev" "Key=Service,Value=order-api"
# String parameter for non-sensitive infra values
aws ssm put-parameter \
--name "/dev/order-api/database/host" \
--value "dev-db.cluster-abc123.us-east-1.rds.amazonaws.com" \
--type String \
--tags "Key=Environment,Value=dev" "Key=Service,Value=order-api"
aws ssm put-parameter \
--name "/dev/order-api/database/port" \
--value "5432" \
--type String \
--tags "Key=Environment,Value=dev" "Key=Service,Value=order-api"
# SecureString parameter — encrypted with our custom KMS key
aws ssm put-parameter \
--name "/dev/order-api/database/password" \
--value "dev-s3cret-passw0rd!" \
--type SecureString \
--key-id alias/ssm-dev \
--tags "Key=Environment,Value=dev" "Key=Service,Value=order-api"
aws ssm put-parameter \
--name "/dev/order-api/stripe/api_key" \
--value "sk_test_abc123xyz" \
--type SecureString \
--key-id alias/ssm-dev \
--tags "Key=Environment,Value=dev" "Key=Service,Value=order-api"
Important: Tags can only be applied during put-parameter when creating a new parameter. If you use --overwrite to update an existing parameter, the --tags flag is ignored. To update tags on existing parameters, use aws ssm add-tags-to-resource.
Retrieving Parameters
# Get a single parameter (decrypted)
aws ssm get-parameter \
--name "/dev/order-api/database/password" \
--with-decryption \
--query "Parameter.Value" \
--output text
# Get all parameters under a path (recursive, decrypted)
aws ssm get-parameters-by-path \
--path "/dev/order-api" \
--recursive \
--with-decryption
# Get multiple specific parameters at once
aws ssm get-parameters \
--names "/dev/order-api/database/host" "/dev/order-api/database/password" \
--with-decryption
Updating Parameters
# Update an existing parameter (--overwrite is required)
aws ssm put-parameter \
--name "/dev/order-api/database/password" \
--value "new-s3cret-passw0rd!" \
--type SecureString \
--key-id alias/ssm-dev \
--overwrite
Python Application Code: Loading Configuration from Parameter Store
Here’s a production-ready Python utility class that loads all parameters for a given environment and service, flattening them into a dictionary your application can use:
import boto3
from botocore.exceptions import ClientError
class SSMConfigLoader:
"""
Loads hierarchical configuration from AWS SSM Parameter Store.
Supports SecureString decryption via KMS.
"""
def __init__(self, environment: str, service_name: str, region: str = "us-east-1"):
self.environment = environment
self.service_name = service_name
self.path_prefix = f"/{environment}/{service_name}"
self.client = boto3.client("ssm", region_name=region)
def load_all(self, decrypt: bool = True) -> dict:
"""
Loads all parameters under /{environment}/{service_name}/
Returns a flat dict: {'database/host': 'value', 'database/password': 'decrypted_value'}
"""
parameters = {}
paginator = self.client.get_paginator("get_parameters_by_path")
try:
page_iterator = paginator.paginate(
Path=self.path_prefix,
Recursive=True,
WithDecryption=decrypt,
PaginationConfig={"MaxItems": 100, "PageSize": 10}
)
for page in page_iterator:
for param in page.get("Parameters", []):
# Strip the prefix to get a clean key
# e.g., /dev/order-api/database/host -> database/host
key = param["Name"].removeprefix(self.path_prefix + "/")
parameters[key] = param["Value"]
except ClientError as e:
error_code = e.response["Error"]["Code"]
if error_code == "AccessDeniedException":
raise PermissionError(
f"No permission to read parameters under {self.path_prefix}. "
f"Check IAM policy and KMS key permissions."
) from e
raise
return parameters
def get_single(self, parameter_suffix: str, decrypt: bool = True) -> str:
"""
Gets a single parameter value.
parameter_suffix: e.g., 'database/password'
"""
full_name = f"{self.path_prefix}/{parameter_suffix}"
try:
response = self.client.get_parameter(
Name=full_name,
WithDecryption=decrypt
)
return response["Parameter"]["Value"]
except self.client.exceptions.ParameterNotFound:
raise KeyError(f"Parameter not found: {full_name}")
# Usage example
if __name__ == "__main__":
import os
env = os.environ.get("APP_ENV", "dev")
config = SSMConfigLoader(environment=env, service_name="order-api")
# Load everything at startup
all_params = config.load_all()
print(f"Loaded {len(all_params)} parameters for {env}/order-api")
db_host = all_params.get("database/host")
db_password = all_params.get("database/password") # Already decrypted
log_level = all_params.get("config/log_level", "INFO")
print(f"DB Host: {db_host}")
print(f"Log Level: {log_level}")
# Never print passwords — this is just to show it works
print(f"DB Password loaded: {'Yes' if db_password else 'No'}")
A critical design decision here: load parameters once at application startup and cache them in memory. Don’t call Parameter Store on every request. The API has throughput limits (40 TPS for GetParameter in standard tier, higher with advanced tier), and hitting it per-request will throttle your application under load.
IAM Policies: Environment-Scoped Access Control
This is where the hierarchical path structure pays off. You can write IAM policies that grant access by environment path and by KMS key.
Developer Role: Read Access to Dev Only
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadDevParameters",
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/dev/*"
},
{
"Sid": "DecryptDevSecrets",
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/dev-key-id-here"
}
]
}
Production Application Role: Read Access to Prod, Specific Service Only
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadProdOrderApiParameters",
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/prod/order-api/*"
},
{
"Sid": "DecryptProdSecrets",
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:123456789012:key/prod-key-id-here"
}
]
}
Key point: Both the SSM permission and the KMS permission are required to read SecureString parameters. If you grant SSM access but forget the KMS Decrypt permission, you’ll get an AccessDeniedException — and the error message won’t always make it clear that KMS is the problem.
Quick Reference: Required Permissions
| Action | SSM Permission | KMS Permission |
| Read String parameter | ssm:GetParameter |
None |
| Read SecureString parameter | ssm:GetParameter |
kms:Decrypt |
| Write String parameter
|