mirror of
https://github.com/BagelHole/DevOps-Security-Agent-Skills.git
synced 2026-08-22 12:49:53 +02:00
463 lines
14 KiB
Markdown
463 lines
14 KiB
Markdown
---
|
|
name: aws-secrets-manager
|
|
description: Store and rotate secrets in AWS Secrets Manager. Configure automatic rotation, access policies, and application integration. Use when managing secrets in AWS environments or requiring automatic credential rotation.
|
|
license: MIT
|
|
metadata:
|
|
author: devops-skills
|
|
version: "1.0"
|
|
---
|
|
|
|
# AWS Secrets Manager
|
|
|
|
Securely store, manage, and rotate secrets in AWS.
|
|
|
|
## When to Use This Skill
|
|
|
|
Use this skill when:
|
|
- Storing database credentials, API keys, or tokens in AWS
|
|
- Implementing automatic credential rotation for RDS or other services
|
|
- Replacing hardcoded secrets in application code or config files
|
|
- Integrating secrets into ECS, EKS, or Lambda workloads
|
|
- Meeting compliance requirements for secret management and rotation
|
|
|
|
## Prerequisites
|
|
|
|
- AWS account with appropriate IAM permissions
|
|
- AWS CLI v2 installed and configured
|
|
- IAM policy allowing `secretsmanager:*` actions (or scoped permissions)
|
|
- For rotation: Lambda execution role and VPC access to target services
|
|
- Python 3.9+ with `boto3` for SDK examples
|
|
|
|
## Secret Creation and Management
|
|
|
|
```bash
|
|
# Create a secret with JSON structure
|
|
aws secretsmanager create-secret \
|
|
--name myapp/production/database \
|
|
--description "Production database credentials" \
|
|
--secret-string '{"username":"dbadmin","password":"S3cur3P@ssw0rd!","engine":"postgres","host":"db.internal.example.com","port":5432,"dbname":"myapp"}' \
|
|
--tags '[{"Key":"Environment","Value":"production"},{"Key":"Team","Value":"platform"}]'
|
|
|
|
# Create a secret with KMS encryption (custom key)
|
|
aws secretsmanager create-secret \
|
|
--name myapp/production/api-key \
|
|
--description "Third-party API key" \
|
|
--secret-string "ak_live_xxxxxxxxxxxx" \
|
|
--kms-key-id alias/secrets-key
|
|
|
|
# Create a binary secret (certificates, keys)
|
|
aws secretsmanager create-secret \
|
|
--name myapp/production/tls-cert \
|
|
--secret-binary fileb://server.pfx
|
|
|
|
# Get secret value
|
|
aws secretsmanager get-secret-value \
|
|
--secret-id myapp/production/database \
|
|
--query 'SecretString' --output text | jq .
|
|
|
|
# Get a specific version
|
|
aws secretsmanager get-secret-value \
|
|
--secret-id myapp/production/database \
|
|
--version-stage AWSPREVIOUS
|
|
|
|
# Update secret value
|
|
aws secretsmanager put-secret-value \
|
|
--secret-id myapp/production/database \
|
|
--secret-string '{"username":"dbadmin","password":"N3wS3cur3P@ss!","engine":"postgres","host":"db.internal.example.com","port":5432,"dbname":"myapp"}'
|
|
|
|
# List all secrets
|
|
aws secretsmanager list-secrets \
|
|
--filters Key=name,Values=myapp/production
|
|
|
|
# Delete secret (with recovery window)
|
|
aws secretsmanager delete-secret \
|
|
--secret-id myapp/production/old-key \
|
|
--recovery-window-in-days 7
|
|
|
|
# Restore a deleted secret
|
|
aws secretsmanager restore-secret \
|
|
--secret-id myapp/production/old-key
|
|
|
|
# Tag a secret
|
|
aws secretsmanager tag-resource \
|
|
--secret-id myapp/production/database \
|
|
--tags '[{"Key":"RotationEnabled","Value":"true"}]'
|
|
```
|
|
|
|
## Automatic Rotation
|
|
|
|
### Enable Rotation
|
|
|
|
```bash
|
|
# Enable rotation with an existing Lambda function
|
|
aws secretsmanager rotate-secret \
|
|
--secret-id myapp/production/database \
|
|
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:SecretsManagerRDSPostgreSQLRotation \
|
|
--rotation-rules '{"AutomaticallyAfterDays":30,"ScheduleExpression":"rate(30 days)"}'
|
|
|
|
# Trigger immediate rotation
|
|
aws secretsmanager rotate-secret \
|
|
--secret-id myapp/production/database
|
|
|
|
# Check rotation status
|
|
aws secretsmanager describe-secret \
|
|
--secret-id myapp/production/database \
|
|
--query '{RotationEnabled:RotationEnabled,RotationLambdaARN:RotationLambdaARN,RotationRules:RotationRules,LastRotatedDate:LastRotatedDate}'
|
|
```
|
|
|
|
### Lambda Rotation Function
|
|
|
|
```python
|
|
"""rotation_function.py - Custom rotation Lambda for database credentials."""
|
|
|
|
import boto3
|
|
import json
|
|
import logging
|
|
import psycopg2
|
|
|
|
logger = logging.getLogger()
|
|
logger.setLevel(logging.INFO)
|
|
|
|
def lambda_handler(event, context):
|
|
"""Secrets Manager rotation handler.
|
|
|
|
The rotation process has four steps:
|
|
1. createSecret - Generate new secret value
|
|
2. setSecret - Apply the new secret to the target service
|
|
3. testSecret - Verify the new secret works
|
|
4. finishSecret - Mark rotation complete
|
|
"""
|
|
secret_arn = event['SecretId']
|
|
token = event['ClientRequestToken']
|
|
step = event['Step']
|
|
|
|
client = boto3.client('secretsmanager')
|
|
|
|
metadata = client.describe_secret(SecretId=secret_arn)
|
|
if not metadata.get('RotationEnabled'):
|
|
raise ValueError(f"Secret {secret_arn} does not have rotation enabled")
|
|
|
|
versions = metadata.get('VersionIdsToStages', {})
|
|
if token not in versions:
|
|
raise ValueError(f"Secret version {token} has no stage for rotation")
|
|
|
|
if step == "createSecret":
|
|
create_secret(client, secret_arn, token)
|
|
elif step == "setSecret":
|
|
set_secret(client, secret_arn, token)
|
|
elif step == "testSecret":
|
|
test_secret(client, secret_arn, token)
|
|
elif step == "finishSecret":
|
|
finish_secret(client, secret_arn, token)
|
|
else:
|
|
raise ValueError(f"Invalid step: {step}")
|
|
|
|
|
|
def create_secret(client, secret_arn, token):
|
|
"""Generate a new secret value."""
|
|
current = client.get_secret_value(
|
|
SecretId=secret_arn, VersionStage="AWSCURRENT"
|
|
)
|
|
current_dict = json.loads(current['SecretString'])
|
|
|
|
new_password = client.get_random_password(
|
|
PasswordLength=32,
|
|
ExcludeCharacters='/@"\\',
|
|
RequireEachIncludedType=True,
|
|
)['RandomPassword']
|
|
|
|
current_dict['password'] = new_password
|
|
client.put_secret_value(
|
|
SecretId=secret_arn,
|
|
ClientRequestToken=token,
|
|
SecretString=json.dumps(current_dict),
|
|
VersionStages=['AWSPENDING'],
|
|
)
|
|
logger.info(f"createSecret: New secret version created for {secret_arn}")
|
|
|
|
|
|
def set_secret(client, secret_arn, token):
|
|
"""Apply the new secret to the target database."""
|
|
pending = client.get_secret_value(
|
|
SecretId=secret_arn, VersionId=token, VersionStage="AWSPENDING"
|
|
)
|
|
pending_dict = json.loads(pending['SecretString'])
|
|
|
|
current = client.get_secret_value(
|
|
SecretId=secret_arn, VersionStage="AWSCURRENT"
|
|
)
|
|
current_dict = json.loads(current['SecretString'])
|
|
|
|
conn = psycopg2.connect(
|
|
host=current_dict['host'],
|
|
port=current_dict.get('port', 5432),
|
|
user=current_dict['username'],
|
|
password=current_dict['password'],
|
|
dbname=current_dict.get('dbname', 'postgres'),
|
|
)
|
|
conn.autocommit = True
|
|
with conn.cursor() as cur:
|
|
cur.execute(
|
|
"ALTER USER %s WITH PASSWORD %s",
|
|
(pending_dict['username'], pending_dict['password']),
|
|
)
|
|
conn.close()
|
|
logger.info(f"setSecret: Password updated in database for {secret_arn}")
|
|
|
|
|
|
def test_secret(client, secret_arn, token):
|
|
"""Verify the new secret works."""
|
|
pending = client.get_secret_value(
|
|
SecretId=secret_arn, VersionId=token, VersionStage="AWSPENDING"
|
|
)
|
|
pending_dict = json.loads(pending['SecretString'])
|
|
|
|
conn = psycopg2.connect(
|
|
host=pending_dict['host'],
|
|
port=pending_dict.get('port', 5432),
|
|
user=pending_dict['username'],
|
|
password=pending_dict['password'],
|
|
dbname=pending_dict.get('dbname', 'postgres'),
|
|
)
|
|
conn.close()
|
|
logger.info(f"testSecret: New credentials verified for {secret_arn}")
|
|
|
|
|
|
def finish_secret(client, secret_arn, token):
|
|
"""Finalize the rotation by updating version stages."""
|
|
metadata = client.describe_secret(SecretId=secret_arn)
|
|
versions = metadata.get('VersionIdsToStages', {})
|
|
|
|
current_version = None
|
|
for version_id, stages in versions.items():
|
|
if "AWSCURRENT" in stages:
|
|
if version_id == token:
|
|
logger.info("finishSecret: Version already marked AWSCURRENT")
|
|
return
|
|
current_version = version_id
|
|
break
|
|
|
|
client.update_secret_version_stage(
|
|
SecretId=secret_arn,
|
|
VersionStage="AWSCURRENT",
|
|
MoveToVersionId=token,
|
|
RemoveFromVersionId=current_version,
|
|
)
|
|
logger.info(f"finishSecret: Rotation complete for {secret_arn}")
|
|
```
|
|
|
|
### Rotation Lambda Terraform
|
|
|
|
```hcl
|
|
resource "aws_lambda_function" "rotation" {
|
|
filename = "rotation_function.zip"
|
|
function_name = "secrets-rotation-postgresql"
|
|
role = aws_iam_role.rotation.arn
|
|
handler = "rotation_function.lambda_handler"
|
|
runtime = "python3.11"
|
|
timeout = 60
|
|
|
|
vpc_config {
|
|
subnet_ids = var.private_subnet_ids
|
|
security_group_ids = [aws_security_group.rotation.id]
|
|
}
|
|
|
|
environment {
|
|
variables = {
|
|
SECRETS_MANAGER_ENDPOINT = "https://secretsmanager.${var.region}.amazonaws.com"
|
|
}
|
|
}
|
|
}
|
|
|
|
resource "aws_lambda_permission" "secrets_manager" {
|
|
action = "lambda:InvokeFunction"
|
|
function_name = aws_lambda_function.rotation.function_name
|
|
principal = "secretsmanager.amazonaws.com"
|
|
statement_id = "AllowSecretsManager"
|
|
}
|
|
|
|
resource "aws_secretsmanager_secret_rotation" "db" {
|
|
secret_id = aws_secretsmanager_secret.db.id
|
|
rotation_lambda_arn = aws_lambda_function.rotation.arn
|
|
rotation_rules {
|
|
automatically_after_days = 30
|
|
}
|
|
}
|
|
```
|
|
|
|
## Application Integration
|
|
|
|
### Python SDK
|
|
|
|
```python
|
|
import boto3
|
|
import json
|
|
from functools import lru_cache
|
|
|
|
def get_secret(secret_name: str, region: str = "us-east-1") -> dict:
|
|
"""Retrieve and parse a secret from AWS Secrets Manager."""
|
|
client = boto3.client("secretsmanager", region_name=region)
|
|
response = client.get_secret_value(SecretId=secret_name)
|
|
if "SecretString" in response:
|
|
return json.loads(response["SecretString"])
|
|
else:
|
|
import base64
|
|
return base64.b64decode(response["SecretBinary"])
|
|
|
|
@lru_cache(maxsize=32)
|
|
def get_cached_secret(secret_name: str) -> dict:
|
|
"""Cached secret retrieval. Clear cache on rotation events."""
|
|
return get_secret(secret_name)
|
|
|
|
# Usage
|
|
creds = get_secret("myapp/production/database")
|
|
connection_string = (
|
|
f"postgresql://{creds['username']}:{creds['password']}"
|
|
f"@{creds['host']}:{creds['port']}/{creds['dbname']}"
|
|
)
|
|
```
|
|
|
|
### ECS Task Definition
|
|
|
|
```json
|
|
{
|
|
"containerDefinitions": [
|
|
{
|
|
"name": "myapp",
|
|
"image": "ghcr.io/acme/myapp:v1.0.0",
|
|
"secrets": [
|
|
{
|
|
"name": "DB_USERNAME",
|
|
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/production/database:username::"
|
|
},
|
|
{
|
|
"name": "DB_PASSWORD",
|
|
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/production/database:password::"
|
|
},
|
|
{
|
|
"name": "API_KEY",
|
|
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/production/api-key"
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"executionRoleArn": "arn:aws:iam::123456789:role/ecsTaskExecutionRole"
|
|
}
|
|
```
|
|
|
|
### EKS with External Secrets Operator
|
|
|
|
```yaml
|
|
apiVersion: external-secrets.io/v1beta1
|
|
kind: SecretStore
|
|
metadata:
|
|
name: aws-secrets-manager
|
|
namespace: production
|
|
spec:
|
|
provider:
|
|
aws:
|
|
service: SecretsManager
|
|
region: us-east-1
|
|
auth:
|
|
jwt:
|
|
serviceAccountRef:
|
|
name: external-secrets-sa
|
|
---
|
|
apiVersion: external-secrets.io/v1beta1
|
|
kind: ExternalSecret
|
|
metadata:
|
|
name: db-credentials
|
|
namespace: production
|
|
spec:
|
|
refreshInterval: 1h
|
|
secretStoreRef:
|
|
name: aws-secrets-manager
|
|
kind: SecretStore
|
|
target:
|
|
name: db-credentials
|
|
creationPolicy: Owner
|
|
data:
|
|
- secretKey: username
|
|
remoteRef:
|
|
key: myapp/production/database
|
|
property: username
|
|
- secretKey: password
|
|
remoteRef:
|
|
key: myapp/production/database
|
|
property: password
|
|
```
|
|
|
|
## Resource-Based Policy
|
|
|
|
```bash
|
|
# Restrict secret access to specific roles
|
|
aws secretsmanager put-resource-policy \
|
|
--secret-id myapp/production/database \
|
|
--resource-policy '{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Effect": "Allow",
|
|
"Principal": {
|
|
"AWS": [
|
|
"arn:aws:iam::123456789:role/myapp-ecs-task-role",
|
|
"arn:aws:iam::123456789:role/myapp-lambda-role"
|
|
]
|
|
},
|
|
"Action": [
|
|
"secretsmanager:GetSecretValue"
|
|
],
|
|
"Resource": "*",
|
|
"Condition": {
|
|
"StringEquals": {
|
|
"aws:RequestedRegion": "us-east-1"
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"Effect": "Deny",
|
|
"Principal": "*",
|
|
"Action": "secretsmanager:GetSecretValue",
|
|
"Resource": "*",
|
|
"Condition": {
|
|
"StringNotEquals": {
|
|
"aws:PrincipalAccount": "123456789012"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}'
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
| Problem | Cause | Solution |
|
|
|---------|-------|----------|
|
|
| `AccessDeniedException` on GetSecretValue | IAM policy missing permission | Add `secretsmanager:GetSecretValue` to the role; check resource-based policy |
|
|
| Rotation fails with Lambda timeout | Lambda cannot reach database | Ensure Lambda is in same VPC with route to DB; check security groups |
|
|
| Secret value is empty after rotation | createSecret step failed | Check Lambda CloudWatch logs; verify random password generation works |
|
|
| ECS container fails to start | Secret ARN format incorrect | Use full ARN with `::` for JSON key extraction; verify secret exists |
|
|
| Application uses old credentials after rotation | Client caching stale values | Implement cache invalidation on rotation; reduce cache TTL |
|
|
| Rotation Lambda permission error | Missing `lambda:InvokeFunction` permission | Add `aws_lambda_permission` for secretsmanager.amazonaws.com principal |
|
|
| KMS decrypt fails | Secret KMS key policy missing role | Add the accessing role to the KMS key policy's `kms:Decrypt` principals |
|
|
|
|
## Best Practices
|
|
|
|
- Enable automatic rotation with 30-day intervals minimum
|
|
- Use resource-based policies in addition to IAM policies (defense in depth)
|
|
- Encrypt secrets with customer-managed KMS keys (not default)
|
|
- Implement least-privilege access (only the roles that need each secret)
|
|
- Use secret versioning for safe rollback during rotation issues
|
|
- Monitor secret access with CloudTrail and alert on unusual patterns
|
|
- Structure secret names hierarchically: `{app}/{env}/{secret-type}`
|
|
- Never log secret values; log only secret ARNs and access metadata
|
|
- Test rotation in staging before enabling in production
|
|
- Set up CloudWatch alarms for rotation failures
|
|
|
|
## Related Skills
|
|
|
|
- [hashicorp-vault](../hashicorp-vault/) - Multi-cloud secrets
|
|
- [aws-iam](../../../infrastructure/cloud-aws/aws-iam/) - IAM policies
|
|
- [azure-keyvault](../azure-keyvault/) - Azure secret management
|
|
- [gcp-secret-manager](../gcp-secret-manager/) - GCP secret management
|