AWS Secrets Manager & Parameter Store: Güvenlik Best Practices
Secrets Manager ve Parameter Store'u karşılaştıran teknik rehber: hangi servisi ne zaman seçmeli ve production implementation pattern'leri.
AWS’de çalışan mühendisler sık sık aynı dilemmayla karşılaşır: secrets management için Secrets Manager mi yoksa Parameter Store mu kullanmalı? Her iki servis de sensitive data saklıyor ama farklı amaçlara hizmet ediyorlar ve farklı maliyet yapıları var. Kısa kural: statik config → Parameter Store; RDS şifreleri ve rotate etmesi gereken her şey → Secrets Manager. Bundan sonrası maliyete, cross-account erişime ve workload’ların secret’ı runtime’da nasıl okuduğuna bağlı.
İki Servisin Farkları#
İki servisi üç eksen ayırıyor: maliyet, rotation desteği ve native entegrasyonlar. Dördüncüsü cross-account paylaşım. Orada Secrets Manager resource policy’leri daha esnek, Parameter Store için ise RAM (Resource Access Manager) kullanman gerekiyor.
Servis Karşılaştırması#
| Feature | Parameter Store Standard | Parameter Store Advanced | Secrets Manager |
|---|---|---|---|
| Maliyet | Ücretsiz | $0.05/secret/ay | $0.40/secret/ay |
| Max Boyut | 4 KB | 8 KB | 64 KB |
| Rotation | Sadece manuel | Sadece manuel | Lambda ile otomatik |
| Versioning | Tek aktif version | Tek aktif version | Multiple concurrent version’lar |
| Cross-Account | RAM ile (2024’ten beri) | RAM ile (2024’ten beri) | Native resource policy’ler |
| Multi-Region | Manuel replication | Manuel replication | Otomatik replication |
| Encryption | Opsiyonel (SecureString) | Opsiyonel (SecureString) | Her zaman encrypted (zorunlu) |
| Native Integration’lar | Basit | Basit | RDS, Redshift, DocumentDB |
Parameter Store SecureString ile zaten KMS encryption kullanıyor, yani ücretsiz katman temel secret saklamayı karşılıyor. Secrets Manager’ın üstüne koyduğu şey otomatik rotation, native RDS entegrasyonu ve staging label’lı versioning.
Karar Framework’ü#
Doğru servisi seçmek için bu teknik decision tree’yi kullan:
Maliyet Analizi Örneği:
- 10 statik API key → Parameter Store Standard: $0/ay
- 5 RDS password rotation ile → Secrets Manager: $2.00/ay
- 20 configuration value → Parameter Store Standard: $0/ay
- Toplam: $2.00/ay vs. her şey Secrets Manager’da olsaydı $10.00/ay
Account’lar Arası Secret Paylaşımı#
En yaygın requirement’lardan biri AWS account’ları arasında secret paylaşımı. Aşağıdaki pattern uçtan uca çalışıyor.
Mimari#
Implementation - Account A (Secret Owner)#
// CDK code for Account A
import * as cdk from 'aws-cdk-lib';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import * as iam from 'aws-cdk-lib/aws-iam';
const kmsKey = new kms.Key(this, 'SecretKey', {
enableKeyRotation: true,
description: 'KMS key for cross-account secret sharing',
});
// Account B'ye key kullanma izni ver
kmsKey.addToResourcePolicy(new iam.PolicyStatement({
sid: 'AllowAccountBDecrypt',
principals: [new iam.AccountPrincipal('222222222222')], // Account B
actions: ['kms:Decrypt', 'kms:DescribeKey'],
resources: ['*'],
conditions: {
StringEquals: {
'kms:ViaService': 'secretsmanager.us-east-1.amazonaws.com',
},
},
}));
const secret = new secretsmanager.Secret(this, 'SharedSecret', {
secretName: 'cross-account/database-credentials',
encryptionKey: kmsKey,
});
// Secret'a resource policy ekle
secret.addToResourcePolicy(new iam.PolicyStatement({
sid: 'AllowAccountBAccess',
principals: [new iam.AccountPrincipal('222222222222')],
actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
resources: ['*'],
}));
Implementation - Account B (Secret Consumer)#
// CDK code for Account B
const applicationRole = new iam.Role(this, 'ApplicationRole', {
assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
inlinePolicies: {
'SecretAccess': new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['secretsmanager:GetSecretValue'],
resources: [
'arn:aws:secretsmanager:us-east-1:111111111111:secret:cross-account/database-credentials-*'
],
}),
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['kms:Decrypt'],
resources: [
'arn:aws:kms:us-east-1:111111111111:key/12345678-1234-1234-1234-123456789012'
],
conditions: {
StringEquals: {
'kms:ViaService': 'secretsmanager.us-east-1.amazonaws.com',
},
},
}),
],
}),
},
});
Warning
Account B’de KMS decrypt permission’ını unutmak yaygın bir hatadır. Secrets Manager policy doğru olsa bile secret retrieval “AccessDeniedException” ile fail olur.
Tip
CloudTrail’de error code’lu KMS API call’larını kontrol et. “AccessDenied” ile fail olan “Decrypt” operation’larına bak.
Parameter Store Reference Pattern#
Parameter Store API’yi standardize ederken gerçek secret’ları Secrets Manager’da saklayabilirsin:
# Reference parameter oluştur
aws ssm put-parameter \
--name "/app/database/password" \
--value "{{resolve:secretsmanager:prod/database:SecretString:password}}" \
--type "String" \
--description "Reference to Secrets Manager secret"
Application code’unda sadece Parameter Store SDK gerekir:
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
const client = new SSMClient({ region: "us-east-1" });
const command = new GetParameterCommand({
Name: "/app/database/password",
WithDecryption: true,
});
const response = await client.send(command);
console.log(response.Parameter.Value); // Secrets Manager'dan gerçek secret
Fayda: Basitleştirilmiş application code, tek API surface area, servisler arası daha kolay migration path.
Container’lara Secret Enjeksiyonu#
Container’lara secret inject etmek için birden fazla pattern var, her birinin farklı trade-off’ları var.
Pattern A: Environment Variable Injection (ECS)#
Bu native ECS yaklaşımı - secret’lar container startup’ında inject ediliyor.
// CDK - ECS Task Definition with secrets
const taskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', {
cpu: 256,
memoryLimitMiB: 512,
});
taskDefinition.addContainer('app', {
image: ecs.ContainerImage.fromRegistry('myapp:latest'),
secrets: {
// Secrets Manager'dan
DB_PASSWORD: ecs.Secret.fromSecretsManager(dbSecret, 'password'),
API_KEY: ecs.Secret.fromSecretsManager(apiKeySecret),
// Parameter Store'dan
CONFIG_VALUE: ecs.Secret.fromSsmParameter(configParam),
},
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'app' }),
});
// Read permission'ları ver
dbSecret.grantRead(taskDefinition.taskRole);
apiKeySecret.grantRead(taskDefinition.taskRole);
configParam.grantRead(taskDefinition.taskRole);
Raw Task Definition JSON:
{
"family": "myapp",
"taskRoleArn": "arn:aws:iam::123456789012:role/myapp-task-role",
"containerDefinitions": [
{
"name": "app",
"image": "myapp:latest",
"secrets": [
{
"name": "DB_PASSWORD",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/database-AbCdEf:password::"
},
{
"name": "API_KEY",
"valueFrom": "arn:aws:secretsmanager:us-east-1:123456789012:secret:api-key-XyZaBc"
},
{
"name": "CONFIG_VALUE",
"valueFrom": "arn:aws:ssm:us-east-1:123456789012:parameter/app/config"
}
]
}
]
}
Warning
Secret’lar SADECE container startup’ında inject edilir. Rotate edilen secret’lar container restart (yeni task launch) gerektirir.
Pattern B: Caching ile Runtime Retrieval#
Restart olmadan rotation handle etmesi gereken uygulamalar için:
// Application code - runtime'da secret'ları al
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";
class SecretCache {
private cache: Map<string, { value: string; expiry: number }> = new Map();
private client: SecretsManagerClient;
private ttlMs: number;
constructor(ttlMs: number = 300000) { // 5 dakika default
this.client = new SecretsManagerClient({});
this.ttlMs = ttlMs;
}
async getSecret(secretId: string): Promise<string> {
const now = Date.now();
const cached = this.cache.get(secretId);
if (cached && cached.expiry > now) {
return cached.value;
}
const command = new GetSecretValueCommand({ SecretId: secretId });
const response = await this.client.send(command);
const value = response.SecretString!;
this.cache.set(secretId, {
value,
expiry: now + this.ttlMs,
});
return value;
}
}
// Lambda handler örneği
const secretCache = new SecretCache(300000); // 5 dakika cache
export const handler = async (event: any) => {
const dbPassword = await secretCache.getSecret(process.env.DB_SECRET_ARN!);
// Password'ü database connection için kullan
};
Maliyet Analizi:
- Startup injection: Container başına 1 API call (~$0.05/10,000 call)
- 5 dakikalık cache ile runtime retrieval: Container başına
288 API call/gün, ayda yaklaşık 8,600 ($0.04/ay) - Her request’te runtime retrieval: Binlerce API call olası (pahalı, tavsiye edilmez)
Pattern C: AWS Parameters and Secrets Lambda Extension#
Lambda function’lar için built-in caching sağlayan extension’ı kullan:
// Extension kullanan Lambda function
export const handler = async (event: any) => {
// Extension sidecar olarak çalışır, local HTTP endpoint sağlar
const response = await fetch(
`http://localhost:2773/secretsmanager/get?secretId=${process.env.SECRET_ARN}`,
{
headers: {
'X-Aws-Parameters-Secrets-Token': process.env.AWS_SESSION_TOKEN!,
},
}
);
const secret = await response.json();
return { dbPassword: JSON.parse(secret.SecretString).password };
};
Faydaları:
- Warm execution environment’ta invocation’lar arası caching; tekrar eden okumalar sandbox’ın dışına çıkmıyor
- Caching için application logic’te değişiklik gerektirmez
- Hem Secrets Manager hem Parameter Store’u destekler
Deployment:
const lambdaFunction = new lambda.Function(this, 'Function', {
// ... diğer config
layers: [
lambda.LayerVersion.fromLayerVersionArn(
this,
'ParametersAndSecretsLayer',
`arn:aws:lambda:${this.region}:177933569100:layer:AWS-Parameters-and-Secrets-Lambda-Extension:11`
),
],
environment: {
PARAMETERS_SECRETS_EXTENSION_CACHE_ENABLED: 'true',
PARAMETERS_SECRETS_EXTENSION_CACHE_SIZE: '1000',
PARAMETERS_SECRETS_EXTENSION_HTTP_PORT: '2773',
},
});
Tip
Extension’ın tasarrufu invocation sayısını değil cache hit oranını takip eder. Faturanın düşeceğini varsayıp bütçe yapmadan önce hit/miss oranını ölç.
CSI Driver ile EKS Secret’ları#
EKS üzerinde Kubernetes workload’ları için native entegrasyon sağlayan Secrets Store CSI Driver’ı kullan.
Kurulum#
# 1. Secrets Store CSI Driver'ı yükle
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \
--namespace kube-system
# 2. AWS Provider'ı yükle
kubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml
SecretProviderClass Yapılandırması#
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: aws-secrets
namespace: production
spec:
provider: aws
parameters:
objects: |
- objectName: "prod/database"
objectType: "secretsmanager"
objectAlias: "db-creds"
jmesPath:
- path: username
objectAlias: db-username
- path: password
objectAlias: db-password
- objectName: "/app/config/api-endpoint"
objectType: "ssmparameter"
objectAlias: "api-endpoint"
# Opsiyonel: Kubernetes Secret'a sync et
secretObjects:
- secretName: database-secret
type: Opaque
data:
- objectName: db-username
key: username
- objectName: db-password
key: password
IRSA ile Pod Yapılandırması#
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-secrets-access
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: app-service-account
containers:
- name: app
image: myapp:latest
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets"
readOnly: true
env:
- name: DB_USERNAME
valueFrom:
secretKeyRef:
name: database-secret
key: username
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: database-secret
key: password
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "aws-secrets"
Pod Identity için IAM Role#
// CDK - EKS Pod Identity (IRSA) için IAM role oluştur
const podRole = new iam.Role(this, 'PodSecretsRole', {
assumedBy: new iam.FederatedPrincipal(
cluster.openIdConnectProvider.openIdConnectProviderArn,
{
StringEquals: {
[`${cluster.openIdConnectProvider.openIdConnectProviderIssuer}:sub`]:
'system:serviceaccount:production:app-service-account',
[`${cluster.openIdConnectProvider.openIdConnectProviderIssuer}:aud`]:
'sts.amazonaws.com',
},
},
'sts:AssumeRoleWithWebIdentity'
),
});
// Secret'lara access izni ver
dbSecret.grantRead(podRole);
configParam.grantRead(podRole);
// Custom key kullanıyorsan KMS permission'ları
kmsKey.grant(podRole, 'kms:Decrypt');
Önemli Fark - IRSA vs Pod Identity:
- IRSA (eski metod): OIDC provider setup gerektirir, EKS 1.17+ ile çalışır
- Pod Identity (yeni metod, 2024+): Basitleştirilmiş setup, daha iyi performance, EKS 1.24+ gerektirir
Secret Rotation Kurulumu#
Otomatik rotation, Secrets Manager’ın en somut faydası. Doğru kurulum şöyle görünüyor.
Rotation Flow#
Lambda Rotation Function - RDS MySQL#
# Lambda rotation function
import boto3
import json
import pymysql
import os
secrets_client = boto3.client('secretsmanager')
def lambda_handler(event, context):
secret_arn = event['SecretId']
token = event['ClientRequestToken']
step = event['Step']
# Uygun step'e dispatch et
if step == "createSecret":
create_secret(secrets_client, secret_arn, token)
elif step == "setSecret":
set_secret(secrets_client, secret_arn, token)
elif step == "testSecret":
test_secret(secrets_client, secret_arn, token)
elif step == "finishSecret":
finish_secret(secrets_client, secret_arn, token)
else:
raise ValueError(f"Invalid step: {step}")
def create_secret(client, arn, token):
# AWSPENDING label'li version zaten var mı kontrol et
try:
client.get_secret_value(
SecretId=arn,
VersionStage="AWSPENDING",
VersionId=token
)
print("Secret version zaten mevcut")
return
except client.exceptions.ResourceNotFoundException:
pass
# Mevcut secret'ı al
current_secret = client.get_secret_value(
SecretId=arn,
VersionStage="AWSCURRENT"
)
secret_dict = json.loads(current_secret['SecretString'])
# Yeni password oluştur
new_password = client.get_random_password(
ExcludeCharacters='/@"\'\\',
PasswordLength=32,
ExcludePunctuation=False,
RequireEachIncludedType=True
)
secret_dict['password'] = new_password['RandomPassword']
# AWSPENDING label ile yeni secret'ı sakla
client.put_secret_value(
SecretId=arn,
ClientRequestToken=token,
SecretString=json.dumps(secret_dict),
VersionStages=['AWSPENDING']
)
def set_secret(client, arn, token):
# Pending secret'ı al
pending_secret = client.get_secret_value(
SecretId=arn,
VersionStage="AWSPENDING",
VersionId=token
)
pending_dict = json.loads(pending_secret['SecretString'])
# Connection için current secret'ı al
current_secret = client.get_secret_value(
SecretId=arn,
VersionStage="AWSCURRENT"
)
current_dict = json.loads(current_secret['SecretString'])
# Current credential'lar ile database'e bağlan
connection = pymysql.connect(
host=current_dict['host'],
user=current_dict['username'],
password=current_dict['password'],
database=current_dict['dbname'],
connect_timeout=5
)
try:
with connection.cursor() as cursor:
# Database'de password'u güncelle
sql = f"ALTER USER '{pending_dict['username']}'@'%' IDENTIFIED BY %s"
cursor.execute(sql, (pending_dict['password'],))
connection.commit()
finally:
connection.close()
def test_secret(client, arn, token):
# Pending secret'ı al
pending_secret = client.get_secret_value(
SecretId=arn,
VersionStage="AWSPENDING",
VersionId=token
)
pending_dict = json.loads(pending_secret['SecretString'])
# Yeni credential'lar ile connection'ı test et
connection = pymysql.connect(
host=pending_dict['host'],
user=pending_dict['username'],
password=pending_dict['password'],
database=pending_dict['dbname'],
connect_timeout=5
)
try:
with connection.cursor() as cursor:
# Access'i verify etmek için basit query çalıştır
cursor.execute("SELECT 1")
result = cursor.fetchone()
if result[0] != 1:
raise ValueError("Test query başarısız")
finally:
connection.close()
def finish_secret(client, arn, token):
# AWSCURRENT label'i yeni versiona taşı
metadata = client.describe_secret(SecretId=arn)
current_version = None
for version, stages in metadata['VersionIdsToStages'].items():
if "AWSCURRENT" in stages:
if version == token:
# Zaten current, yapılacak bir şey yok
return
current_version = version
break
# Version stage'lerini güncelle
client.update_secret_version_stage(
SecretId=arn,
VersionStage="AWSCURRENT",
MoveToVersionId=token,
RemoveFromVersionId=current_version
)
Rotation için CDK Kurulumu#
Built-in destekli RDS database’leri için:
// RDS database
const dbInstance = new rds.DatabaseInstance(this, 'Database', {
engine: rds.DatabaseInstanceEngine.mysql({
version: rds.MysqlEngineVersion.VER_8_0
}),
vpc,
credentials: rds.Credentials.fromGeneratedSecret('admin'),
});
// Secret'a rotation ekle
dbInstance.secret!.addRotationSchedule('RotationSchedule', {
automaticallyAfter: cdk.Duration.days(30),
hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(),
});
Custom uygulamalar için:
// Rotation için Lambda function
const rotationLambda = new lambda.Function(this, 'RotationFunction', {
runtime: lambda.Runtime.PYTHON_3_12,
handler: 'rotation.lambda_handler',
code: lambda.Code.fromAsset('lambda/rotation'),
timeout: cdk.Duration.minutes(5),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
// Permission'ları ver
dbSecret.grantRead(rotationLambda);
dbSecret.grantWrite(rotationLambda);
// Rotation'ı ekle
dbSecret.addRotationSchedule('CustomRotation', {
rotationLambda,
automaticallyAfter: cdk.Duration.days(30),
});
Warning
Yaygın hatalar:
- Network Access: Lambda’nın database’e erişmek için VPC access’e ihtiyacı var. VPC subnet’leri doğru configure et.
- Timeout: Default 3 saniye çok kısa. Rotation için 5 dakikaya ayarla.
- Permission’lar: Lambda’nın secret’a hem read hem write ve KMS decrypt/encrypt permission’ları gerekir.
- Idempotency: Yeni oluşturmadan önce AWSPENDING version’ının var olup olmadığını her zaman kontrol et.
- Connection Pooling: Eski password kullanan açık connection’lar otomatik olarak yeni password’u almaz. Uygulamalar connection refresh’i handle etmeli.
Alternating Users Strategy#
High-availability uygulamalarda zero-downtime rotation için:
Architecture:
- İki database user:
app_user_aveapp_user_b - Her iki user’ın da identik permission’ları var
- Rotation hangi user’ın password’ünün güncelleneceğini alterne eder
- Uygulama rotation sırasında her zaman bir geçerli credential’a sahip
Faydaları:
- Downtime penceresi yok
- Aktif connection’lar rotation sırasında çalışmaya devam eder
- Connection refresh handle edemeyecek uygulamalar için uygun
Trade-off: User’ları clone etmek için ayrı bir secret’ta superuser credential’ları gerektirir.
Multi-Region Secrets Replication#
Disaster recovery senaryoları için Secrets Manager otomatik replication destekler.
Replication ile Primary Secret#
// Replication ile primary region secret
const primarySecret = new secretsmanager.Secret(this, 'PrimarySecret', {
secretName: 'prod/database-credentials',
description: 'Production database credentials',
replicaRegions: [
{
region: 'us-west-2',
encryptionKey: replicaKmsKey, // Opsiyonel: farklı KMS key kullan
},
{
region: 'eu-west-1',
},
],
});
ARN Yapısı:
- Primary:
arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/database-credentials-AbCdEf - Replica:
arn:aws:secretsmanager:us-west-2:123456789012:secret:prod/database-credentials-AbCdEf
Önemli Noktalar:
- Secret suffix (
-AbCdEf) region’lar arasında identik - Replication otomatik ve near real-time
- Primary region’daki rotation replica’lara propagate olur
- Her replica ayrı secret olarak faturalanır ($0.40/ay her biri)
- Replica’lar read-only, update’ler primary region’da olmalı
Failover ile Disaster Recovery#
// Failover logic ile application code
class SecretService {
private primaryRegion = 'us-east-1';
private replicaRegion = 'us-west-2';
private secretName = 'prod/database-credentials';
async getSecretWithFailover(): Promise<string> {
try {
// Önce primary region'ı dene
return await this.getSecret(this.primaryRegion);
} catch (error) {
console.error('Primary region başarısız, replica deneniyor', error);
// Replica region'a fallback
return await this.getSecret(this.replicaRegion);
}
}
private async getSecret(region: string): Promise<string> {
const client = new SecretsManagerClient({ region });
const command = new GetSecretValueCommand({
SecretId: this.secretName,
});
const response = await client.send(command);
return response.SecretString!;
}
}
Maliyet Optimizasyonu Alternatifi#
Replication yerine cross-region secret access kullan (daha yüksek latency, daha düşük maliyet):
// us-west-2 application'dan us-east-1'deki secret'a eriş
const client = new SecretsManagerClient({ region: 'us-east-1' });
const command = new GetSecretValueCommand({
SecretId: 'arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/database-credentials-AbCdEf',
});
const response = await client.send(command);
Trade-off: Replica başına $0.40/ay tasarruf eder ama cross-region API latency ekler (~50-150ms).
Break-Glass Emergency Access#
Emergency access prosedürleri incident response için kritik. Güvenli kurulum şu üç parçadan oluşuyor: geniş yetkili ama kısa ömürlü bir rol, rolün kullanımını yakalayan bir alarm ve iki kişi gerektiren bir aktivasyon prosedürü.
Break-Glass Role Mimarisi#
// CDK - Tüm account'larda break-glass IAM role
export class BreakGlassStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Geniş permission'lara sahip break-glass role
const breakGlassRole = new iam.Role(this, 'BreakGlassRole', {
roleName: 'BREAK-GLASS-EMERGENCY-ACCESS',
description: 'Incident response için emergency access role',
maxSessionDuration: cdk.Duration.hours(2), // Kısa session
assumedBy: new iam.AccountPrincipal('111111111111'), // Management account
});
// Tüm secret'lara access ver
breakGlassRole.addToPolicy(new iam.PolicyStatement({
sid: 'SecretsManagerEmergencyAccess',
effect: iam.Effect.ALLOW,
actions: [
'secretsmanager:GetSecretValue',
'secretsmanager:DescribeSecret',
'secretsmanager:ListSecrets',
],
resources: ['*'],
}));
// Tüm key'ler için KMS decrypt ver
breakGlassRole.addToPolicy(new iam.PolicyStatement({
sid: 'KMSDecryptEmergencyAccess',
effect: iam.Effect.ALLOW,
actions: ['kms:Decrypt', 'kms:DescribeKey'],
resources: ['*'],
conditions: {
StringEquals: {
'kms:ViaService': [
`secretsmanager.${this.region}.amazonaws.com`,
`ssm.${this.region}.amazonaws.com`,
],
},
},
}));
// Parameter Store access ver
breakGlassRole.addToPolicy(new iam.PolicyStatement({
sid: 'ParameterStoreEmergencyAccess',
effect: iam.Effect.ALLOW,
actions: [
'ssm:GetParameter',
'ssm:GetParameters',
'ssm:GetParametersByPath',
],
resources: ['*'],
}));
}
}
Break-Glass Erişimini İzleme#
// Break-glass access'i detect etmek için EventBridge rule
const breakGlassAlertRule = new events.Rule(this, 'BreakGlassAlert', {
eventPattern: {
source: ['aws.sts'],
detailType: ['AWS API Call via CloudTrail'],
detail: {
eventName: ['AssumeRole'],
requestParameters: {
roleArn: [{
prefix: 'arn:aws:iam::*:role/BREAK-GLASS-EMERGENCY-ACCESS'
}],
},
},
},
});
// Security team için SNS topic
const securityTopic = new sns.Topic(this, 'SecurityAlertTopic', {
displayName: 'Critical Security Alerts',
});
breakGlassAlertRule.addTarget(new targets.SnsTopic(securityTopic, {
message: events.RuleTargetInput.fromEventPath(
'$.detail.userIdentity.principalId break-glass role\'u assume etti'
),
}));
Emergency Access Prosedürü#
Aktivasyon:
- Security team break-glass password’ü fiziksel kasadan alır
- İkinci kişi YubiKey’i ayrı güvenli konumdan alır
- İkisi de mevcut olmalı (two-person rule)
Access:
# Break-glass user ile AWS CLI'yi configure et
aws configure --profile break-glass
# Target account'ta role assume et
aws sts assume-role \
--role-arn arn:aws:iam::222222222222:role/BREAK-GLASS-EMERGENCY-ACCESS \
--role-session-name "incident-database-outage" \
--serial-number arn:aws:iam::111111111111:mfa/EMERGENCY-BREAK-GLASS \
--token-code 123456 \
--profile break-glass
# Temporary credential'ları export et
export AWS_ACCESS_KEY_ID="..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
# Secret'lara eriş
aws secretsmanager get-secret-value \
--secret-id prod/database-credentials \
--query SecretString \
--output text
Post-Incident:
- Temporary credential’ları hemen revoke et
- Erişilen tüm secret’ları 4 saat içinde rotate et
- Alınan tüm aksiyonları incident report’a dokümante et
- Complete audit trail için CloudTrail log’larını review et
- Neden break-glass’a ihtiyaç duyulduğu konusunda post-mortem yap
CloudTrail ile Audit Logging#
Kapsamlı audit logging, güvenlik ve uyumluluk için şart.
CloudTrail Yapılandırması#
// CloudTrail log'ları için S3 bucket
const trailBucket = new s3.Bucket(this, 'CloudTrailBucket', {
bucketName: `cloudtrail-logs-${this.account}`,
encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
versioned: true,
lifecycleRules: [
{
transitions: [
{
storageClass: s3.StorageClass.INTELLIGENT_TIERING,
transitionAfter: cdk.Duration.days(30),
},
{
storageClass: s3.StorageClass.GLACIER,
transitionAfter: cdk.Duration.days(90),
},
],
expiration: cdk.Duration.days(2555), // Uyumluluk için 7 yıl
},
],
});
// CloudTrail trail
const trail = new cloudtrail.Trail(this, 'SecurityAuditTrail', {
bucket: trailBucket,
includeGlobalServiceEvents: true,
isMultiRegionTrail: true,
enableFileValidation: true,
sendToCloudWatchLogs: true,
});
// Secrets Manager için data event'leri
trail.addEventSelector({
readWriteType: cloudtrail.ReadWriteType.ALL,
includeManagementEvents: true,
dataResources: [
{
type: 'AWS::SecretsManager::Secret',
values: ['arn:aws:secretsmanager:*:*:secret:*'],
},
],
});
Warning
CloudTrail default olarak sadece management event’leri log’lar. Data event’leri (GetSecretValue dahil) explicit olarak enable edilmeli.
Note
100,000 event başına ~$0.10. Ayda 1,000 kez erişilen 10 secret için: $0.01/ay.
Analiz için Athena Query’leri#
-- Tüm secret access event'lerini bul
SELECT
eventTime,
userIdentity.principalId,
userIdentity.arn,
eventName,
json_extract_scalar(requestParameters, '$.secretId') as secretId,
sourceIPAddress,
errorCode
FROM cloudtrail_logs
WHERE eventSource = 'secretsmanager.amazonaws.com'
AND eventName IN ('GetSecretValue', 'PutSecretValue', 'DeleteSecret')
AND year = '2025' AND month = '12' AND day = '01'
ORDER BY eventTime DESC;
-- Cross-account secret access
SELECT
eventTime,
userIdentity.accountId as callerAccount,
json_extract_scalar(requestParameters, '$.secretId') as secretArn,
eventName,
errorCode
FROM cloudtrail_logs
WHERE eventSource = 'secretsmanager.amazonaws.com'
AND userIdentity.accountId != regexp_extract(
json_extract_scalar(requestParameters, '$.secretId'),
'arn:aws:secretsmanager:[^:]+:([^:]+):',
1
)
AND year = '2025' AND month = '12'
ORDER BY eventTime DESC;
Maliyet Analizi & Optimizasyon#
Maliyet yapısını anlamak, güvenlikten ödün vermeden harcamayı optimize etmene yardımcı olur.
Detaylı Maliyet Senaryoları#
Senaryo 1: 10 Statik API Key (Rotation Yok)
- Parameter Store Standard: $0/ay (ücretsiz tier)
- Secrets Manager: $4.00/ay
- Tavsiye: Parameter Store Standard
- Tasarruf: $4.00/ay
Senaryo 2: 5 RDS Password (Aylık Rotation)
- Parameter Store: $0.25/ay + manuel rotation iş gücü + downtime riski
- Secrets Manager: $2.00/ay + $0 rotation = $2.00/ay
- Tavsiye: Secrets Manager
- ROI: Otomasyon maliyete değer
Senaryo 3: Yüksek Trafikli Lambda
- Extension olmadan: 1M invocation/ay × 1 API call = $5.00/ay
- Extension ile, %99 cache hit oranı varsayımıyla: 10,000 call = $0.05/ay
- Tasarruf: Bu oranda $4.95/ay. Oran concurrency ve cache TTL’ine bağlı olduğu için bütçelemeden önce kendi oranını ölç.
Maliyet Optimizasyon Stratejileri#
Strateji 1: Hybrid Yaklaşım
Statik configuration için Parameter Store, rotating credential’lar için Secrets Manager kullan:
// Statik değerler Parameter Store'da (ücretsiz)
const apiEndpoint = ssm.StringParameter.fromStringParameterAttributes(
this, 'ApiEndpoint', {
parameterName: '/app/api/endpoint',
}
);
// Rotating credential'lar Secrets Manager'da (ücretli)
const dbSecret = secretsmanager.Secret.fromSecretNameV2(
this, 'DbSecret',
'prod/database'
);
Strateji 2: Secret’ları Consolidate Et
Her credential component için ayrı secret yerine:
// Yanlış: Birden fazla secret ($1.20/ay)
const dbUsername = new secretsmanager.Secret(this, 'DbUser');
const dbPassword = new secretsmanager.Secret(this, 'DbPass');
const dbHost = new secretsmanager.Secret(this, 'DbHost');
// Doğru: Tek secret ($0.40/ay)
const dbCredentials = new secretsmanager.Secret(this, 'DbCreds', {
secretObjectValue: {
username: cdk.SecretValue.unsafePlainText('admin'),
password: cdk.SecretValue.unsafePlainText('generated'),
host: cdk.SecretValue.unsafePlainText('db.example.com'),
port: cdk.SecretValue.unsafePlainText('3306'),
},
});
// Tasarruf: Database başına $0.80/ay
Strateji 3: Seçici Replication
Sadece kritik production secret’ları replicate et:
// Sadece kritik production database secret'larını replicate et
if (secretName.includes('/prod/database') || secretName.includes('/prod/auth')) {
secret.addReplicaRegion('us-west-2', replicaKmsKey);
}
// Kritik olmayan secret'lar: cross-region API call'ları kullan
// Kabul edilebilir: API key'ler, statik token'lar, config değerleri
Maliyet Analizi:
- 20 secret, 2 replica: $24/ay
- 5 kritik replicated + 15 sadece primary: $10/ay
- Tasarruf: $14/ay (%58 azalma)
Yaygın Hatalar & Çözümler#
Secrets management vakalarının çoğu altı hata biçiminden çıkıyor ve her birinin mekanik bir çözümü var.
Hata 1: Cross-Account Access için Default KMS Key#
Problem: Default aws/secretsmanager key kullanırken cross-account sharing “AccessDeniedException” ile fail oluyor.
Root Cause: AWS-managed key’lerin policy’leri cross-account access için modifiye edilemiyor.
Çözüm: Her zaman customer-managed KMS key’ler oluştur:
// Yanlış: Default key kullanıyor
const secret = new secretsmanager.Secret(this, 'Secret', {
secretName: 'shared-secret',
});
// Doğru: Customer-managed key
const kmsKey = new kms.Key(this, 'SharedSecretKey', {
enableKeyRotation: true,
});
kmsKey.addToResourcePolicy(/* cross-account policy */);
const secret = new secretsmanager.Secret(this, 'Secret', {
secretName: 'shared-secret',
encryptionKey: kmsKey,
});
Hata 2: Rotation için Lambda VPC Configuration#
Problem: Rotation Lambda VPC’deki RDS’e bağlanırken timeout oluyor.
Root Cause: Lambda VPC access ile configure edilmemiş.
Çözüm:
const rotationLambda = new lambda.Function(this, 'RotationFunction', {
// ...
vpc: database.vpc,
vpcSubnets: {
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
securityGroups: [rotationSecurityGroup],
timeout: cdk.Duration.minutes(5), // Default 3 saniye değil
});
// Lambda'nın RDS'e erişmesine izin ver
rotationSecurityGroup.addEgressRule(
database.connections.securityGroups[0],
ec2.Port.tcp(3306),
'Rotation Lambda\'nin database\'e erişimine izin ver'
);
Hata 3: ECS Secret’ları Sadece Startup’ta Inject Ediliyor#
Problem: Rotation sonrası container’lar authentication error ile fail oluyor.
Root Cause: ECS secret’ları sadece startup’ta inject ediyor.
Çözüm: Graceful connection handling implement et:
class DatabaseConnection {
private pool: any;
private secretId: string;
private secretCache: { value: string; expiry: number } | null = null;
async getConnection() {
try {
return await this.pool.getConnection();
} catch (error) {
if (this.isAuthError(error)) {
console.log('Auth error detect edildi, secret refresh ediliyor');
await this.refreshSecret();
this.pool = this.createPool(this.secretCache!.value);
return await this.pool.getConnection();
}
throw error;
}
}
private async refreshSecret() {
const command = new GetSecretValueCommand({ SecretId: this.secretId });
const response = await secretsClient.send(command);
this.secretCache = {
value: response.SecretString!,
expiry: Date.now() + 300000, // 5 dakika
};
}
}
Hata 4: Aşırı Lambda API Çağrıları#
Problem: Secrets Manager maliyetleri ayda $50+ seviyesine çıkıyor.
Root Cause: Her invocation’da secret fetch ediliyor, caching yok.
Çözüm: Lambda extension’ı kullan (Pattern C’de gösterildi).
Sonuç: Fatura, invocation sayısı yerine cache miss sayısıyla ölçekleniyor.
Hata 5: Eksik CloudTrail Data Event’leri#
Problem: GetSecretValue operasyonları için audit trail yok.
Root Cause: Data event’ler varsayılan olarak enable değil.
Çözüm: Data event logging’i enable et (Audit Logging bölümünde gösterildi).
Hata 6: Gizli Olmayan Config’i Secrets Manager’da Saklamak#
Problem: Gizli olmayan değerler için ayda $0.40 ödeniyor.
Çözüm: Karar framework’ü kullan:
Hassas mı? (şifre, API key, token)
├─ EVET → Rotate edilebilir mi?
│ ├─ EVET → Secrets Manager ($0.40/ay)
│ └─ HAYIR → Parameter Store SecureString (ücretsiz)
└─ HAYIR → Parameter Store Standard (ücretsiz)
Varsayılan ve Sınırları#
Statik config için Parameter Store, rotate eden credential’lar için Secrets Manager: bu ayrım AWS workload’larının çoğunda geçerli ve faturayı gerçekten rotation gerektiren değer sayısıyla orantılı tutuyor. Üç durumda ayrımın dışına çıkmak mantıklı. Secret account sınırını geçecekse, değer hiç rotate etmese bile customer-managed KMS key ile başla; AWS-managed key’den sonradan çıkmak secret’ı yeniden oluşturmak demek. Rotation sıklığını risk analizi yerine bir uyumluluk şartı belirliyorsa, hosted rotation yazıp sonra unutacağın manuel prosedürden ucuza geliyor. Secret lease’i, dinamik üretilen credential veya AWS dışı backend gerekiyorsa iki servis de yetmiyor; HashiCorp Vault gibi ayrı bir aracın operasyonel maliyetini hak ettiği yer burası.
Ayrım hangi yöne düşerse düşsün, ilk production secret’ı yazmadan önce CloudTrail data event’lerini aç. Management event’ler GetSecretValue’yu kapsamıyor ve bu boşluk ancak bir inceleme sırasında görünür hale geliyor; o noktada geriye dönük doldurma şansın yok.
Kaynaklar#
- AWS Secrets Manager Nedir? (yeni sekmede açılır) - Kimlik bilgilerini saklama, döndürme ve alma için Secrets Manager’a resmi genel bakış
- AWS Secrets Manager ile Sır Yönetimi (yeni sekmede açılır) - IAM erişim denetimiyle sır oluşturma, güncelleme ve silme
- AWS Secrets Manager Agent Kullanımı (yeni sekmede açılır) - Sırların yerel önbelleklenmesi için Lambda extension ve sidecar agent
- AWS Systems Manager Parameter Store (yeni sekmede açılır) - Döndürülmeyen yapılandırmalar ve SecureString değerleri için ekonomik alternatif
- AWS KMS Anahtar Yönetim Servisi (yeni sekmede açılır) - Çapraz hesap Secrets Manager erişimi için müşteri tarafından yönetilen anahtarlar
- AWS Secrets Manager için Kimlik Doğrulama ve Erişim Denetimi (yeni sekmede açılır) - Sırlar için IAM politikaları ve kaynak tabanlı politikalar
- Parameter Store’dan Secrets Manager Sırlarına Başvurma (yeni sekmede açılır) - Secrets Manager ve Parameter Store arasındaki entegrasyon kalıpları
İlgili yazılar
Git'te secret'ları şifrelemek için pratik Mozilla SOPS rehberi: age encryption, AWS CDK ve Lambda pattern'leri, production-ready serverless güvenlik.
secrets-management · ci-cd · terraform +7
Ölçekte karşılaşacağınız IAM boyut, ekleme ve kota limitleri; ve hepsinden uzak tutan dar kapsamlı politika, izin sınırı ve SCP yapısı.
aws · iam · security +2
Global uygulamalar için AWS edge computing çözümlerini seçme ve uygulama üzerine pratik örnekler ve maliyet optimizasyonu stratejileri içeren kapsamlı teknik rehber.
aws · cloudfront · lambda +6
Gelişmiş Amazon Cognito teknik rehberi: özel auth akışları, federation, multi-tenancy, migration stratejileri ve CDK ile production-grade güvenlik.
aws · authentication · serverless +6
TypeScript ile internal sistemleriniz için custom MCP server geliştirin: authentication, monitoring ve Kubernetes deployment örnekleriyle uçtan uca rehber.
typescript · mcp · nodejs +5