Guide · AWS SSM Parameter Store
MCP Server SSM SecureString — KMS CMK encryption, key policy, PutParameter, decrypt IAM condition
SSM SecureString parameters encrypt the stored value with a KMS key, making them suitable for low-sensitivity credentials that don't need Secrets Manager's rotation machinery: API tokens, service passwords, OAuth client secrets for development environments, and Slack webhook URLs. SecureString is cheaper than Secrets Manager for static credentials (no $0.40/secret/month storage fee, no rotation Lambda cost) but requires careful IAM design — the same principal that can call ssm:GetParameter with WithDecryption=true must also have kms:Decrypt on the encrypting key. The separation of the SSM parameter permission from the KMS decryption permission is the primary security control: you can allow a principal to see that a SecureString parameter exists (via ssm:DescribeParameters) without allowing it to read the plaintext value.
TL;DR
Create SecureString parameters with --type SecureString --key-id arn:aws:kms:...:key/KEY_ID. Read them with GetParameter + WithDecryption: true — the caller needs both ssm:GetParameter on the parameter ARN and kms:Decrypt on the KMS key. Use kms:ViaService condition in key policies to restrict decryption to SSM API calls only. Use the AWS-managed key (alias/aws/ssm) only if cross-account access and key policy customization are not needed — otherwise use a CMK.
Creating SecureString parameters with a CMK
The --key-id parameter in PutParameter specifies which KMS key encrypts the stored value. If omitted, the AWS-managed key alias/aws/ssm is used — this key cannot be shared cross-account and its key policy cannot be customized. For MCP servers, always specify a CMK to retain control of the key policy.
# Create a SecureString parameter with a CMK
aws ssm put-parameter \
--name "/mcp/prod/probe-collector/slack-webhook-url" \
--value "https://hooks.slack.com/services/T.../B.../..." \
--type SecureString \
--key-id "arn:aws:kms:us-east-1:ACCOUNT_ID:key/mrk-abc123" \
--description "Slack webhook for probe-collector alerts" \
--overwrite
# Verify the parameter exists (without decrypting the value)
aws ssm describe-parameters \
--filters "Key=Name,Values=/mcp/prod/probe-collector/slack-webhook-url"
# Returns metadata including KMS key ID, type, last modified — NOT the value
# Read with decryption
aws ssm get-parameter \
--name "/mcp/prod/probe-collector/slack-webhook-url" \
--with-decryption \
--query 'Parameter.Value' \
--output text
# Read WITHOUT decryption (value is base64-encoded ciphertext)
aws ssm get-parameter \
--name "/mcp/prod/probe-collector/slack-webhook-url"
# Returns: "Value": "AQICAHg..." (ciphertext — not the plaintext URL)
KMS key policy for SSM SecureString
The KMS key policy must explicitly allow the IAM principals that create and read SecureString parameters. A common pattern is three distinct grants: administrators who manage the key, writers who can create/update parameters (need kms:GenerateDataKey and kms:Encrypt), and readers who can decrypt (need kms:Decrypt). Using kms:ViaService scopes the grant to SSM API calls, preventing direct KMS use from other services.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "KeyAdministration",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::ACCOUNT_ID:role/platform-admin"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "SSMParameterWrite",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::ACCOUNT_ID:role/mcp-deployer",
"arn:aws:iam::ACCOUNT_ID:role/ci-pipeline"
]
},
"Action": [
"kms:GenerateDataKey",
"kms:Encrypt",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "ssm.us-east-1.amazonaws.com"
}
}
},
{
"Sid": "SSMParameterRead",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::ACCOUNT_ID:role/mcp-execution-prod",
"arn:aws:iam::ACCOUNT_ID:role/mcp-execution-staging"
]
},
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "ssm.us-east-1.amazonaws.com"
}
}
}
]
}
The kms:ViaService condition is the key control here. Without it, any principal with kms:Decrypt in the key policy could call the KMS Decrypt API directly with any ciphertext encrypted under this key — not just SSM parameters. By restricting to ssm.us-east-1.amazonaws.com, the grant is scoped: principals can only decrypt ciphertext that SSM presents on their behalf.
IAM identity policies for SecureString access
In addition to the KMS key policy, the IAM principal needs identity policy statements for both the SSM API and the KMS key. The KMS statement in the identity policy is technically redundant (the key policy already grants it) but is required as an explicit allow in many organizational SCP configurations that deny KMS actions not explicitly allowed in the identity policy.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SSMParameterReadAccess",
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": [
"arn:aws:ssm:us-east-1:ACCOUNT_ID:parameter/mcp/prod/*"
]
},
{
"Sid": "KMSDecryptForSSM",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": [
"arn:aws:kms:us-east-1:ACCOUNT_ID:key/mrk-abc123"
],
"Condition": {
"StringEquals": {
"kms:ViaService": "ssm.us-east-1.amazonaws.com"
}
}
}
]
}
Reading SecureString in Node.js MCP server code
import { SSMClient, GetParameterCommand, GetParametersByPathCommand } from "@aws-sdk/client-ssm";
const ssm = new SSMClient({ region: process.env.AWS_REGION });
// Read a single SecureString parameter
async function getSecureParam(name) {
const resp = await ssm.send(new GetParameterCommand({
Name: name,
WithDecryption: true, // REQUIRED for SecureString; decryption uses caller's KMS grant
}));
return resp.Parameter.Value;
}
// Read all SecureString params in a path (mixing String and SecureString)
async function loadSecureConfig(path) {
const config = {};
let nextToken;
do {
const resp = await ssm.send(new GetParametersByPathCommand({
Path: path,
Recursive: true,
WithDecryption: true, // decrypts SecureString params; no-op for String params
NextToken: nextToken,
}));
for (const param of resp.Parameters ?? []) {
const name = param.Name.replace(path, "");
config[name] = param.Value;
}
nextToken = resp.NextToken;
} while (nextToken);
return config;
}
// Usage
const webhookUrl = await getSecureParam("/mcp/prod/probe-collector/slack-webhook-url");
const allConfig = await loadSecureConfig("/mcp/prod/probe-collector/");
SecureString vs Secrets Manager: when to use which
| Factor | SSM SecureString | Secrets Manager |
|---|---|---|
| Storage cost | Free (standard tier); $0.05/10K API calls advanced tier | $0.40/secret/month + $0.05/10K API calls |
| Automatic rotation | No — manual only | Yes — Lambda-based rotation on schedule |
| Cross-account sharing | Not supported without replication | Supported via resource policy |
| AWSPREVIOUS for zero-downtime rotation | No staging labels | AWSPENDING/AWSCURRENT/AWSPREVIOUS |
| Max value size | 4 KB (standard), 8 KB (advanced) | 64 KB |
| Versioning | Manual (overwrite or versioned via PutParameter --overwrite) | Automatic version IDs |
| Best for MCP server | Non-rotating config secrets: webhook URLs, OAuth tokens, API keys that are changed infrequently via CI pipeline | Database passwords, RDS credentials, OAuth client secrets that need regular rotation |
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| WithDecryption omitted or false | Parameter.Value contains base64 ciphertext; JSON parse fails; webhook URL is invalid | Always set WithDecryption=true when reading SecureString parameters |
| Principal lacks kms:Decrypt in identity policy (SCP blocks) | AccessDeniedException on GetParameter even though key policy allows it | Add explicit kms:Decrypt statement to the role's identity policy; SCPs in some orgs require both key policy and identity policy to allow an action |
| Wrong region in KMS key ARN | InvalidKeyId: KMS key not found | SSM and KMS must be in the same region; check region suffix in both the parameter ARN and the key ARN |
| Parameter created with aws/ssm managed key; later tried to share cross-account | AccessDenied on GetParameter from other account | Delete and recreate parameter with a CMK; aws/ssm key cannot be shared or have its policy modified |
| kms:ViaService blocks direct KMS decrypt in rotation Lambda | Rotation Lambda calling KMS directly gets AccessDeniedException | Rotation Lambda should use ssm:GetParameter or secretsmanager:GetSecretValue, not direct KMS decrypt; ViaService condition is correct and intentional |