AWS Parameter Store vs. Secrets Manager: How to Store API Keys for Free

You need to securely store an API key or database password for your Lambda function, but AWS Secrets Manager's per-secret pricing adds up fast — especially across dozens of microservices or non-production environments. AWS Systems Manager (SSM) Parameter Store offers a free tier for standard parameters that covers the majority of non-rotating secret use cases, with no monthly per-secret charge.

TL;DR

DimensionSSM Parameter Store (Standard)AWS Secrets Manager
CostFree (Standard tier)~$0.40/secret/month + API call fees (verify current pricing)
Automatic Rotation❌ Not built-in✅ Native rotation via Lambda
Encryption✅ KMS (SecureString type)✅ KMS (always encrypted)
Max Value Size4 KB (Standard) / 8 KB (Advanced)64 KB
Versioning✅ Limited history✅ Full version staging
Cross-account AccessLimited✅ Native resource policy support
Best ForStatic config, API keys, feature flagsRotating DB credentials, OAuth tokens

How Parameter Store Works: The Architecture

Parameter Store is a capability of AWS Systems Manager. It stores values in a hierarchical namespace (like a filesystem path), integrates natively with IAM for access control, and optionally encrypts values using AWS KMS. Your application retrieves values at runtime via the SSM API — no secrets ever live in environment variables or source code.

sequenceDiagram participant Dev as Developer / CI participant SSM as SSM Parameter Store participant KMS as AWS KMS participant App as Lambda / ECS Task Dev->>SSM: put-parameter (SecureString value) SSM->>KMS: Encrypt value with CMK KMS-->>SSM: Ciphertext stored SSM-->>Dev: Parameter stored OK App->>SSM: GetParameter (WithDecryption=true) SSM->>KMS: Decrypt ciphertext KMS-->>SSM: Plaintext value SSM-->>App: Return plaintext to authorized caller App->>App: Cache in memory, use in-process
  1. Developer/CI Pipeline writes the secret once using the AWS CLI or Console — the value is encrypted with a KMS key and stored in Parameter Store.
  2. Lambda / ECS Task assumes an IAM Role that grants ssm:GetParameter on the specific parameter path.
  3. At runtime, the application calls the SSM API (GetParameter with WithDecryption: true).
  4. SSM calls KMS to decrypt the value transparently, returning the plaintext only to the authorized caller.
  5. The application uses the secret in-memory — it is never written to disk or logs.

Parameter Types: Which One to Use

TypeEncrypted?Use Case
StringNoNon-sensitive config: region names, feature flags, URLs
StringListNoComma-separated non-sensitive values
SecureStringYes (KMS)API keys, passwords, tokens — anything sensitive

Rule of thumb: If you would be embarrassed to see it in a CloudTrail log, use SecureString.

Naming Convention: Use Hierarchical Paths

Parameter Store supports path-based naming. This is not just cosmetic — it enables wildcard IAM policies and bulk retrieval with GetParametersByPath.

/myapp/production/database/password
/myapp/production/stripe/api_key
/myapp/staging/stripe/api_key
/shared/datadog/api_key

This structure lets you grant a production service access to /myapp/production/* without exposing staging secrets — a clean least-privilege boundary.

Step 1: Store a Secret (AWS CLI)

Use the aws ssm put-parameter command. The --key-id flag is optional; omitting it uses the AWS-managed default KMS key for SSM (aws/ssm), which is free to use.

# Store an encrypted API key using the default SSM KMS key
aws ssm put-parameter \
  --name "/myapp/production/stripe/api_key" \
  --value "sk_live_XXXXXXXXXXXX" \
  --type "SecureString" \
  --description "Stripe live API key for production" \
  --region us-east-1

# To use a customer-managed KMS key instead:
aws ssm put-parameter \
  --name "/myapp/production/stripe/api_key" \
  --value "sk_live_XXXXXXXXXXXX" \
  --type "SecureString" \
  --key-id "arn:aws:kms:us-east-1:123456789012:key/your-key-id" \
  --region us-east-1

Step 2: IAM Policy — Least Privilege Access

Grant your Lambda function or ECS task role only the permissions it needs. The policy below allows reading any parameter under /myapp/production/ and decrypting with the associated KMS key.

🔽 Click to expand — IAM Policy JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSSMRead",
      "Effect": "Allow",
      "Action": [
        "ssm:GetParameter",
        "ssm:GetParameters",
        "ssm:GetParametersByPath"
      ],
      "Resource": "arn:aws:ssm:us-east-1:123456789012:parameter/myapp/production/*"
    },
    {
      "Sid": "AllowKMSDecrypt",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:us-east-1:123456789012:key/your-key-id"
    }
  ]
}

Note: If you use the default aws/ssm managed key, you do not need the explicit kms:Decrypt statement — IAM permissions on the parameter are sufficient. For customer-managed KMS keys, the explicit kms:Decrypt grant is required.

Step 3: Retrieve the Secret at Runtime

Below are examples in Python (boto3) and Node.js. The critical pattern is fetch-once-and-cache — retrieve the value on cold start, cache it in memory, and reuse it across invocations. This avoids SSM API call latency on every request and reduces API call costs.

🔽 Click to expand — Python (boto3) Lambda Example
import boto3
import os

# Module-level cache — persists across warm Lambda invocations
_ssm_client = boto3.client('ssm', region_name='us-east-1')
_param_cache = {}

def get_parameter(name: str) -> str:
    """Fetch from cache or SSM. Decrypts SecureString automatically."""
    if name not in _param_cache:
        response = _ssm_client.get_parameter(
            Name=name,
            WithDecryption=True
        )
        _param_cache[name] = response['Parameter']['Value']
    return _param_cache[name]

def lambda_handler(event, context):
    api_key = get_parameter('/myapp/production/stripe/api_key')
    # Use api_key — never log it
    return {'statusCode': 200, 'body': 'OK'}
🔽 Click to expand — Node.js (AWS SDK v3) Lambda Example
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';

const ssmClient = new SSMClient({ region: 'us-east-1' });
const paramCache = {};

async function getParameter(name) {
  if (!paramCache[name]) {
    const command = new GetParameterCommand({
      Name: name,
      WithDecryption: true,
    });
    const response = await ssmClient.send(command);
    paramCache[name] = response.Parameter.Value;
  }
  return paramCache[name];
}

export const handler = async (event) => {
  const apiKey = await getParameter('/myapp/production/stripe/api_key');
  // Use apiKey — never log it
  return { statusCode: 200, body: 'OK' };
};

Bulk Retrieval: GetParametersByPath

If your application needs multiple parameters at startup, use GetParametersByPath to fetch an entire namespace in one API call instead of making N individual calls.

aws ssm get-parameters-by-path \
  --path "/myapp/production/" \
  --with-decryption \
  --recursive \
  --region us-east-1

Standard vs. Advanced Parameters: Know the Boundary

FeatureStandard (Free)Advanced (Paid)
Max value size4 KB8 KB
Parameter policies (TTL/expiry)
Higher throughputStandardHigher (check docs for current limits)
CostFreePer-parameter/month charge applies

For most API key storage use cases, Standard tier is sufficient. Advanced parameters are useful when you need parameter expiration policies — a lightweight alternative to full rotation.

When to Upgrade to Secrets Manager

graph TD A[Need to store a secret?] --> B{Requires auto-rotation?} B -- Yes --> C[Use Secrets Manager] B -- No --> D{Cross-account sharing needed?} D -- Yes --> C D -- No --> E{Value larger than 4KB?} E -- Yes --> F{OK with Advanced tier cost?} F -- Yes --> G[SSM Advanced Parameter] F -- No --> C E -- No --> H[SSM Standard Parameter - FREE] style H fill:#2e7d32,color:#fff style C fill:#1565c0,color:#fff

Use this decision tree to choose the right service:

  1. If your secret requires automatic rotation (e.g., RDS passwords rotated every 30 days), Secrets Manager is the right tool — its native Lambda rotation integration is purpose-built for this.
  2. If you need cross-account secret sharing with resource-based policies, Secrets Manager handles this more cleanly.
  3. If your value exceeds 4 KB and you don't want Advanced Parameters, Secrets Manager supports up to 64 KB.
  4. For everything else — static API keys, third-party tokens, feature flags — Parameter Store Standard is the right choice.
Analogy: Parameter Store is a secure filing cabinet with a combination lock — reliable, free, and perfect for documents you update manually. Secrets Manager is a bank vault with an automated document-shredding and replacement service built in. If your documents don't need automatic replacement, you're paying for a feature you'll never use.

Security Best Practices Checklist

  • ✅ Always use SecureString for sensitive values — never String
  • ✅ Scope IAM policies to specific parameter paths, not *
  • ✅ Enable CloudTrail to audit all ssm:GetParameter calls
  • ✅ Use hierarchical naming (/app/env/service/key) from day one
  • ✅ Cache parameters in Lambda memory — avoid per-invocation API calls
  • ✅ Never log decrypted parameter values
  • ✅ Rotate manually by updating the parameter value and redeploying — or use Advanced Parameter policies for expiry notifications

Glossary

TermDefinition
SecureStringA Parameter Store type that encrypts the value at rest using AWS KMS before storage.
KMS (Key Management Service)AWS service that creates and manages cryptographic keys used to encrypt/decrypt data.
Parameter HierarchyA slash-delimited naming convention (e.g., /app/env/key) enabling path-based IAM scoping and bulk retrieval.
WithDecryptionA boolean flag in the SSM GetParameter API that instructs SSM to call KMS and return the plaintext value.
Cold Start CacheThe pattern of fetching external config once during Lambda initialization and storing it in module-level variables for reuse across warm invocations.

Next Steps

Related Posts

Comments

Popular posts from this blog

EC2 No Internet Access in Custom VPC: Attaching an Internet Gateway and Fixing Route Tables

IAM User vs. IAM Role: Why Your EC2 Instance Should Never Use a User

EC2 SSH Connection Timeout: The Exact Security Group Rules You Need to Fix It