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

FactorSSM SecureStringSecrets Manager
Storage costFree (standard tier); $0.05/10K API calls advanced tier$0.40/secret/month + $0.05/10K API calls
Automatic rotationNo — manual onlyYes — Lambda-based rotation on schedule
Cross-account sharingNot supported without replicationSupported via resource policy
AWSPREVIOUS for zero-downtime rotationNo staging labelsAWSPENDING/AWSCURRENT/AWSPREVIOUS
Max value size4 KB (standard), 8 KB (advanced)64 KB
VersioningManual (overwrite or versioned via PutParameter --overwrite)Automatic version IDs
Best for MCP serverNon-rotating config secrets: webhook URLs, OAuth tokens, API keys that are changed infrequently via CI pipelineDatabase passwords, RDS credentials, OAuth client secrets that need regular rotation

Failure modes reference

FailureSymptomFix
WithDecryption omitted or falseParameter.Value contains base64 ciphertext; JSON parse fails; webhook URL is invalidAlways set WithDecryption=true when reading SecureString parameters
Principal lacks kms:Decrypt in identity policy (SCP blocks)AccessDeniedException on GetParameter even though key policy allows itAdd 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 ARNInvalidKeyId: KMS key not foundSSM 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-accountAccessDenied on GetParameter from other accountDelete 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 LambdaRotation Lambda calling KMS directly gets AccessDeniedExceptionRotation Lambda should use ssm:GetParameter or secretsmanager:GetSecretValue, not direct KMS decrypt; ViaService condition is correct and intentional