Self-contained CLI demos for AWS Key Management Service (KMS) and Secrets Manager. Run these in your own AWS account to practice encryption, envelope encryption, post-quantum cryptography, and secrets management.
Companion material for the AWS Security Engineering course. These demos are designed to be run independently -- no infrastructure or CloudFormation required.
Requirements:
- An AWS account with appropriate permissions (see Permissions section)
- AWS CLI v2 installed and configured (
aws --versionshould show 2.x) - OpenSSL (pre-installed on macOS and most Linux)
- Python 3 (for JSON parsing in some steps)
- No infrastructure needed -- just CLI commands
Region: All demos default to ap-southeast-1 (Singapore). To use a different region, set this variable before running any commands:
export AWS_REGION=ap-southeast-1 # Change to your preferred regionThen replace --region ap-southeast-1 with --region $AWS_REGION in each command, or simply omit the --region flag entirely (the CLI will use your AWS_REGION env variable).
Cost: Minimal. KMS keys cost $1/month each. API calls cost $0.03 per 10,000 requests. All demos include cleanup steps to delete keys (7-day pending deletion). Total cost for running all demos: less than $0.10 if you clean up promptly.
Platform notes:
- macOS: The demos use
base64 -D(macOS native). If you installed GNU coreutils via Homebrew, usegbase64 -dinstead. - Linux: Replace
base64 -Dwithbase64 -dthroughout.
Direct encryption of small data (< 4KB) using the KMS API. The key never leaves KMS.
Time: ~5 minutes
Create a KMS key for the demo:
KEY_ID=$(aws kms create-key \
--description "Demo encryption key" \
--region ap-southeast-1 \
--query 'KeyMetadata.KeyId' --output text)
echo "Key ID: $KEY_ID"
# Create an alias for convenience
aws kms create-alias \
--alias-name alias/demo-key \
--target-key-id $KEY_ID \
--region ap-southeast-1# Create a plaintext message
echo -n "Confidential: salary budget is 2.4M" > /tmp/plaintext-msg.txt
# Encrypt it with KMS
aws kms encrypt \
--key-id alias/demo-key \
--plaintext fileb:///tmp/plaintext-msg.txt \
--region ap-southeast-1 \
--output text --query CiphertextBlob | base64 -D > /tmp/encrypted.bin
# Look at the encrypted output -- it's unintelligible
xxd /tmp/encrypted.bin | head# Decrypt it back
aws kms decrypt \
--ciphertext-blob fileb:///tmp/encrypted.bin \
--region ap-southeast-1 \
--output text --query Plaintext | base64 -D
# Output: "Confidential: salary budget is 2.4M"Key takeaway: The encryption key never left KMS. You sent the data to KMS, it encrypted it, and sent back the ciphertext. This is called direct encryption and works for data up to 4KB.
rm /tmp/plaintext-msg.txt /tmp/encrypted.bin
# schedule-key-deletion requires the key ID (not an alias)
aws kms schedule-key-deletion \
--key-id $KEY_ID \
--pending-window-in-days 7 \
--region ap-southeast-1
aws kms delete-alias --alias-name alias/demo-key --region ap-southeast-1For data larger than 4KB, you use envelope encryption: KMS generates a data key, you encrypt your data locally with that key, then throw away the plaintext key. This is exactly what S3, EBS, and RDS do behind the scenes.
Time: ~10 minutes
Create a KMS key (or reuse the one from Demo 1):
KEY_ID=$(aws kms create-key \
--description "Envelope encryption demo key" \
--region ap-southeast-1 \
--query 'KeyMetadata.KeyId' --output text)
echo "Key ID: $KEY_ID"
aws kms create-alias \
--alias-name alias/demo-envelope-key \
--target-key-id $KEY_ID \
--region ap-southeast-1echo "This is AnyCompany's confidential financial report for Q4 2025." > /tmp/plaintext.txtaws kms generate-data-key \
--key-id alias/demo-envelope-key \
--key-spec AES_256 \
--region ap-southeast-1 \
--output json > /tmp/datakey.jsonThe response contains two fields:
Plaintext-- the data key in plaintext (base64-encoded)CiphertextBlob-- the same data key encrypted by KMS
python3 -c "
import sys, json, base64
d = json.load(open('/tmp/datakey.json'))
open('/tmp/datakey-plaintext.bin','wb').write(base64.b64decode(d['Plaintext']))
open('/tmp/datakey-encrypted.b64','w').write(d['CiphertextBlob'])
"openssl enc -aes-256-cbc -pbkdf2 \
-in /tmp/plaintext.txt \
-out /tmp/encrypted-report.enc \
-pass file:/tmp/datakey-plaintext.binThis is the critical step. The plaintext key must never be stored.
rm /tmp/datakey-plaintext.bin /tmp/datakey.json
echo "Plaintext key destroyed. Only the encrypted data key remains."ls -la /tmp/encrypted-report.enc /tmp/datakey-encrypted.b64You store these two files together: the encrypted data + the encrypted data key.
First, ask KMS to decrypt the data key:
base64 -D -i /tmp/datakey-encrypted.b64 -o /tmp/datakey-encrypted.bin
aws kms decrypt \
--ciphertext-blob fileb:///tmp/datakey-encrypted.bin \
--region ap-southeast-1 \
--output text --query Plaintext | base64 -D > /tmp/datakey-plaintext.binThen use the recovered key to decrypt the file:
openssl enc -aes-256-cbc -pbkdf2 -d \
-in /tmp/encrypted-report.enc \
-out /tmp/decrypted-report.txt \
-pass file:/tmp/datakey-plaintext.bin
cat /tmp/decrypted-report.txt
# Output: "This is AnyCompany's confidential financial report for Q4 2025."Key takeaway: This is exactly what every AWS service does when you enable encryption. S3, EBS, RDS -- they all generate a data key, encrypt your data locally, and store the encrypted data key alongside it. The plaintext data key only exists in memory, briefly.
rm /tmp/datakey-plaintext.bin /tmp/datakey-encrypted.b64 /tmp/datakey-encrypted.bin \
/tmp/encrypted-report.enc /tmp/plaintext.txt /tmp/decrypted-report.txt
# schedule-key-deletion requires the key ID (not an alias)
aws kms schedule-key-deletion \
--key-id $KEY_ID \
--pending-window-in-days 7 \
--region ap-southeast-1
aws kms delete-alias --alias-name alias/demo-envelope-key --region ap-southeast-1KMS supports post-quantum digital signatures using ML-DSA (FIPS 204), standardized by NIST in August 2024. These signatures are resistant to attacks from future quantum computers.
Time: ~7 minutes
PQC_KEY_ID=$(aws kms create-key \
--key-spec ML_DSA_87 \
--key-usage SIGN_VERIFY \
--description "Post-quantum demo key" \
--region ap-southeast-1 \
--query 'KeyMetadata.KeyId' --output text)
echo "PQC Key ID: $PQC_KEY_ID"aws kms describe-key --key-id $PQC_KEY_ID --region ap-southeast-1 \
--query 'KeyMetadata.{KeyId:KeyId,KeySpec:KeySpec,KeyUsage:KeyUsage,SigningAlgorithms:SigningAlgorithms}' \
--output tableNote: ML_DSA_87 provides 256-bit classical security. Other options: ML_DSA_44 (128-bit) and ML_DSA_65 (192-bit).
echo -n "Hello post-quantum world" > /tmp/pqc-message.bin
aws kms sign \
--key-id $PQC_KEY_ID \
--message fileb:///tmp/pqc-message.bin \
--message-type RAW \
--signing-algorithm ML_DSA_SHAKE_256 \
--region ap-southeast-1 \
--output json > /tmp/pqc-signature.json# Extract the signature to a binary file
python3 -c "
import json, base64
sig = json.load(open('/tmp/pqc-signature.json'))['Signature']
open('/tmp/pqc-sig.bin','wb').write(base64.b64decode(sig))
"
# Verify
aws kms verify \
--key-id $PQC_KEY_ID \
--message fileb:///tmp/pqc-message.bin \
--message-type RAW \
--signing-algorithm ML_DSA_SHAKE_256 \
--signature fileb:///tmp/pqc-sig.bin \
--region ap-southeast-1 \
--output table
# Expected: SignatureValid = Trueecho -n "test" > /tmp/pqc-test.txt
aws kms encrypt --key-id $PQC_KEY_ID \
--plaintext fileb:///tmp/pqc-test.txt \
--region ap-southeast-1
# Error: InvalidKeyUsageException -- this key is SIGN_VERIFY onlyKey takeaway: Post-quantum signatures are available in KMS today. If you sign firmware, code, or long-lived documents, start planning your PQC migration now. NIST standardized ML-DSA in August 2024.
aws kms schedule-key-deletion \
--key-id $PQC_KEY_ID \
--pending-window-in-days 7 \
--region ap-southeast-1
rm /tmp/pqc-message.bin /tmp/pqc-signature.json /tmp/pqc-sig.bin /tmp/pqc-test.txt 2>/dev/nullCreate a secret, retrieve it via CLI, rotate it, and see the new value. This is how applications should handle credentials -- never hardcode them.
Time: ~5 minutes
aws secretsmanager create-secret \
--name demo/api-credentials \
--description "Demo API credentials" \
--secret-string '{"api_key":"sk-demo-abc123","api_secret":"super-secret-value","endpoint":"https://api.example.com"}' \
--region ap-southeast-1# Get the full secret
aws secretsmanager get-secret-value \
--secret-id demo/api-credentials \
--region ap-southeast-1 \
--query 'SecretString' --output text | python3 -m json.toolThis is what your application does at startup or on every request -- fetch credentials from Secrets Manager, never store them on disk.
In production, rotation is automated with Lambda. Here we'll simulate it manually:
# Update the secret with a new API key
aws secretsmanager put-secret-value \
--secret-id demo/api-credentials \
--secret-string '{"api_key":"sk-demo-xyz789-rotated","api_secret":"new-secret-after-rotation","endpoint":"https://api.example.com"}' \
--region ap-southeast-1
# Retrieve again -- the new value is returned immediately
aws secretsmanager get-secret-value \
--secret-id demo/api-credentials \
--region ap-southeast-1 \
--query 'SecretString' --output text | python3 -m json.toolaws secretsmanager list-secret-version-ids \
--secret-id demo/api-credentials \
--region ap-southeast-1 \
--query 'Versions[*].{VersionId:VersionId,Stages:VersionStages}' \
--output tableYou'll see AWSCURRENT (the active version) and AWSPREVIOUS (the old version). Secrets Manager keeps both so you can roll back if needed.
Key takeaway: Applications fetch secrets at runtime -- no hardcoded credentials, no config files with passwords. When you rotate, the application picks up the new value on the next request. Zero code changes.
aws secretsmanager delete-secret \
--secret-id demo/api-credentials \
--force-delete-without-recovery \
--region ap-southeast-1Every S3 bucket is encrypted by default (SSE-S3). But you can upgrade to KMS encryption for audit trails, key rotation control, and access policies tied to the key. This demo shows the difference.
Time: ~5 minutes
# Create a KMS key for S3
S3_KEY_ID=$(aws kms create-key \
--description "S3 encryption demo key" \
--region ap-southeast-1 \
--query 'KeyMetadata.KeyId' --output text)
echo "Key ID: $S3_KEY_ID"
aws kms create-alias \
--alias-name alias/demo-s3-key \
--target-key-id $S3_KEY_ID \
--region ap-southeast-1
# Create a bucket with KMS encryption and S3 Bucket Key enabled
BUCKET_NAME="kms-demo-$(aws sts get-caller-identity --query Account --output text)-$(date +%s)"
aws s3api create-bucket \
--bucket $BUCKET_NAME \
--region ap-southeast-1 \
--create-bucket-configuration LocationConstraint=ap-southeast-1
aws s3api put-bucket-encryption \
--bucket $BUCKET_NAME \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "alias/demo-s3-key"
},
"BucketKeyEnabled": true
}]
}'
echo "Bucket: $BUCKET_NAME"# Upload a test file
echo "This file is encrypted with my KMS key" > /tmp/s3-demo.txt
aws s3 cp /tmp/s3-demo.txt s3://$BUCKET_NAME/demo.txt --region ap-southeast-1
# Check the encryption -- notice SSEKMSKeyId shows your key
aws s3api head-object \
--bucket $BUCKET_NAME \
--key demo.txt \
--region ap-southeast-1 \
--query '{Encryption:ServerSideEncryption,KMSKeyId:SSEKMSKeyId,BucketKeyEnabled:BucketKeyEnabled}'# After ~5 minutes, you can see the KMS Decrypt calls in CloudTrail
# This is the audit trail you get with KMS encryption (not available with SSE-S3)
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--max-results 5 \
--region ap-southeast-1 \
--query 'Events[*].{Time:EventTime,Event:EventName,User:Username}' \
--output tableKey takeaways:
- SSE-S3 (default): AWS manages everything. No audit trail of key usage. Free.
- SSE-KMS: You control the key. Every access is logged in CloudTrail. You can revoke access by disabling the key. $1/month per key + $0.03/10k requests.
- S3 Bucket Key: Reduces KMS API calls (and cost) by caching a bucket-level data key. Always enable this with SSE-KMS.
aws s3 rm s3://$BUCKET_NAME --recursive
aws s3api delete-bucket --bucket $BUCKET_NAME --region ap-southeast-1
# schedule-key-deletion requires the key ID (not an alias)
aws kms schedule-key-deletion \
--key-id $S3_KEY_ID \
--pending-window-in-days 7 \
--region ap-southeast-1
aws kms delete-alias --alias-name alias/demo-s3-key --region ap-southeast-1
rm /tmp/s3-demo.txtMinimum IAM permissions needed to run all demos:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "KMSDemos",
"Effect": "Allow",
"Action": [
"kms:CreateKey", "kms:CreateAlias", "kms:DeleteAlias",
"kms:DescribeKey", "kms:ScheduleKeyDeletion",
"kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey",
"kms:Sign", "kms:Verify"
],
"Resource": "*"
},
{
"Sid": "SecretsManagerDemo",
"Effect": "Allow",
"Action": [
"secretsmanager:CreateSecret", "secretsmanager:GetSecretValue",
"secretsmanager:PutSecretValue", "secretsmanager:ListSecretVersionIds",
"secretsmanager:DeleteSecret"
],
"Resource": "*"
},
{
"Sid": "S3Demo",
"Effect": "Allow",
"Action": [
"s3:CreateBucket", "s3:DeleteBucket", "s3:PutObject", "s3:GetObject",
"s3:DeleteObject", "s3:PutEncryptionConfiguration",
"s3:ListBucket"
],
"Resource": "*"
},
{
"Sid": "CloudTrailLookup",
"Effect": "Allow",
"Action": ["cloudtrail:LookupEvents"],
"Resource": "*"
},
{
"Sid": "STSIdentity",
"Effect": "Allow",
"Action": ["sts:GetCallerIdentity"],
"Resource": "*"
}
]
}Tip: If you have
AdministratorAccessorPowerUserAccess, you already have all required permissions.
- Key deletion: KMS keys have a mandatory 7-30 day waiting period before deletion. You will be charged $1/month per key until deletion completes.
- Cleanup: Always run the cleanup steps after each demo to avoid ongoing charges.
- Safety: These demos create no infrastructure (no EC2, no VPCs). The only billable resources are KMS keys ($1/month each) and a temporary S3 bucket (Demo 5).
Created as companion material for the AWS Security Engineering course.