Skip to content

AWS Secrets Manager & Parameter Store: Güvenlik Best Practices

AWS Secrets Manager ve Systems Manager Parameter Store'u karşılaştıran kapsamlı teknik rehber - hangi servisi ne zaman kullanmalı ve gerçek dünya 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. Bu rehber teknik karar kriterleri, complete implementation pattern'leri ve gerçek dünyadan öğrenilmiş dersleri içeriyor. Kısa kural: statik config → Parameter Store; RDS şifreleri ve rotation → Secrets Manager.

Servisleri Anlamak

Implementation'a geçmeden önce, bu servisler arasındaki teknik farkları ortaya koyalım. Cross-account secret paylaşımı gerekiyorsa Secrets Manager resource policy'leri daha esnek; Parameter Store için RAM (Resource Access Manager) kullanırsınız.

Servis Karşılaştırması

FeatureParameter Store StandardParameter Store AdvancedSecrets Manager
MaliyetÜcretsiz$0.05/secret/ay$0.40/secret/ay
Max Boyut4 KB8 KB64 KB
RotationSadece manuelSadece manuelLambda ile otomatik
VersioningTek aktif versionTek aktif versionMultiple concurrent version'lar
Cross-AccountRAM ile (2024'ten beri)RAM ile (2024'ten beri)Native resource policy'ler
Multi-RegionManuel replicationManuel replicationOtomatik replication
EncryptionOpsiyonel (SecureString)Opsiyonel (SecureString)Her zaman encrypted (zorunlu)
Native Integration'larBasitBasitRDS, Redshift, DocumentDB

Önemli Teknik İçgörü: Parameter Store SecureString ile KMS encryption kullanır ve ücretsiz temel secrets management sağlar. Secrets Manager buna otomatik rotation, native RDS entegrasyonu ve staging label'ları ile built-in versioning ekler.

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/ayvs.hers\ceySecretsManagerdaolsaydı2.00/ay vs. her şey Secrets Manager'da olsaydı 10.00/ay

Cross-Account Secret Sharing

En yaygın requirement'lardan biri AWS account'ları arasında secret paylaşımı. İşte complete implementation pattern:

Architecture Genel Bakış

Implementation - Account A (Secret Owner)

typescript
// CDK code for Account Aimport * 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 verkmsKey.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 eklesecret.addToResourcePolicy(new iam.PolicyStatement({  sid: 'AllowAccountBAccess',  principals: [new iam.AccountPrincipal('222222222222')],  actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],  resources: ['*'],}));

Implementation - Account B (Secret Consumer)

typescript
// CDK code for Account Bconst 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.

Troubleshooting: 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:

bash
# Reference parameter oluşturaws 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:

typescript
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 Secrets Injection

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.

typescript
// CDK - ECS Task Definition with secretsconst 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ı verdbSecret.grantRead(taskDefinition.taskRole);apiKeySecret.grantRead(taskDefinition.taskRole);configParam.grantRead(taskDefinition.taskRole);

Raw Task Definition JSON:

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: Runtime Retrieval with Caching

Restart olmadan rotation handle etmesi gereken uygulamalar için:

typescript
// Application code - runtime'da secret'ları alimport { 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ğiconst 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 ($1.44/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:

typescript
// Extension kullanan Lambda functionexport 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ı:

  • Built-in caching (API call'ları ~%90 azaltır)
  • Caching için application logic'te değişiklik gerektirmez
  • Hem Secrets Manager hem Parameter Store'u destekler

Deployment:

typescript
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: Lambda extension API call'ları %99 azaltır. Yüksek trafikli function'lar için maliyet 5/aydan5/ay'dan 0.05/ay'a düşer.

EKS Secrets with CSI Driver

EKS üzerinde Kubernetes workload'ları için native entegrasyon sağlayan Secrets Store CSI Driver'ı kullan.

Architecture Setup

bash
# 1. Secrets Store CSI Driver'ı yüklehelm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/chartshelm install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \  --namespace kube-system
# 2. AWS Provider'ı yüklekubectl apply -f https://raw.githubusercontent.com/aws/secrets-store-csi-driver-provider-aws/main/deployment/aws-provider-installer.yaml

SecretProviderClass Configuration

yaml
apiVersion: secrets-store.csi.x-k8s.io/v1kind: SecretProviderClassmetadata:  name: aws-secrets  namespace: productionspec:  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 Configuration

yaml
apiVersion: v1kind: ServiceAccountmetadata:  name: app-service-account  namespace: production  annotations:    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/app-secrets-access
---apiVersion: apps/v1kind: Deploymentmetadata:  name: myapp  namespace: productionspec:  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

typescript
// CDK - EKS Pod Identity (IRSA) için IAM role oluşturconst 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 verdbSecret.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 Implementation

Otomatik rotation Secrets Manager'ın önemli faydalarından biri. İşte doğru implementation:

Rotation Flow

Lambda Rotation Function - RDS MySQL

python
# Lambda rotation functionimport boto3import jsonimport pymysqlimport 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    )

CDK Setup for Rotation

Built-in destekli RDS database'leri için:

typescript
// RDS databaseconst 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 ekledbInstance.secret!.addRotationSchedule('RotationSchedule', {  automaticallyAfter: cdk.Duration.days(30),  hostedRotation: secretsmanager.HostedRotation.mysqlSingleUser(),});

Custom uygulamalar için:

typescript
// Rotation için Lambda functionconst 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ı verdbSecret.grantRead(rotationLambda);dbSecret.grantWrite(rotationLambda);
// Rotation'ı ekledbSecret.addRotationSchedule('CustomRotation', {  rotationLambda,  automaticallyAfter: cdk.Duration.days(30),});

Warning: Yaygın hatalar:

  1. Network Access: Lambda'nın database'e erişmek için VPC access'e ihtiyacı var. VPC subnet'leri doğru configure et.
  2. Timeout: Default 3 saniye çok kısa. Rotation için 5 dakikaya ayarla.
  3. Permission'lar: Lambda'nın secret'a hem read hem write ve KMS decrypt/encrypt permission'ları gerekir.
  4. Idempotency: Yeni oluşturmadan önce AWSPENDING version'ının var olup olmadığını her zaman kontrol et.
  5. 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_a ve app_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

typescript
// Replication ile primary region secretconst 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

typescript
// Failover logic ile application codeclass 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):

typescript
// 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. İşte güvenli implementation:

Break-Glass Role Architecture

typescript
// CDK - Tüm account'larda break-glass IAM roleexport 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 Access Monitoring

typescript
// Break-glass access'i detect etmek için EventBridge ruleconst 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 topicconst 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:

  1. Security team break-glass password'ü fiziksel kasadan alır
  2. İkinci kişi YubiKey'i ayrı güvenli konumdan alır
  3. İkisi de mevcut olmalı (two-person rule)

Access:

bash
# Break-glass user ile AWS CLI'yi configure etaws configure --profile break-glass
# Target account'ta role assume etaws sts assume-role \  --role-arn arn:aws:iam::222222222222:role/BREAK-GLASS-EMERGENCY-ACCESS \  --role-session-name "incident-2024-11-30-database-outage" \  --serial-number arn:aws:iam::111111111111:mfa/EMERGENCY-BREAK-GLASS \  --token-code 123456 \  --profile break-glass
# Temporary credential'ları export etexport 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

Audit Logging with CloudTrail

Kapsamlı audit logging güvenlik ve uyumluluk için esansiyel.

CloudTrail Configuration

typescript
// CloudTrail log'ları için S3 bucketconst 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 trailconst trail = new cloudtrail.Trail(this, 'SecurityAuditTrail', {  bucket: trailBucket,  includeGlobalServiceEvents: true,  isMultiRegionTrail: true,  enableFileValidation: true,  sendToCloudWatchLogs: true,});
// Secrets Manager için data event'leritrail.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.

Maliyet Etkisi: ~0.10/ayper100,000event.1,000kez/ayeris\cilen10secretic\cin:0.10/ay per 100,000 event. 1,000 kez/ay erişilen 10 secret için: 0.01/ay.

Analiz için Athena Query'leri

sql
-- Tüm secret access event'lerini bulSELECT  eventTime,  userIdentity.principalId,  userIdentity.arn,  eventName,  json_extract_scalar(requestParameters, '$.secretId') as secretId,  sourceIPAddress,  errorCodeFROM cloudtrail_logsWHERE 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 accessSELECT  eventTime,  userIdentity.accountId as callerAccount,  json_extract_scalar(requestParameters, '$.secretId') as secretArn,  eventName,  errorCodeFROM cloudtrail_logsWHERE 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+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: API call'lar %99 azaldı = $0.05/ay
  • Tasarruf: $4.95/ay (%99 azalma)

Maliyet Optimizasyon Stratejileri

Strateji 1: Hybrid Yaklaşım

Statik configuration için Parameter Store, rotating credential'lar için Secrets Manager kullan:

typescript
// 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:

typescript
// 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:

typescript
// Sadece kritik production database secret'larını replicate etif (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

Karşılaştığım teknik sorunlar ve nasıl çözüldükleri:

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:

typescript
// Yanlış: Default key kullanıyorconst secret = new secretsmanager.Secret(this, 'Secret', {  secretName: 'shared-secret',});
// Doğru: Customer-managed keyconst 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:

typescript
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 verrotationSecurityGroup.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:

typescript
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ç: %99 maliyet azalması.

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)

Önemli Çıkarımlar

AWS secrets management ile çalışmak bana şu önemli dersleri öğretti:

  1. Servis Seçimi Use Case Hakkında: Secrets Manager'ı rotating credential'lar için ayır. Geri kalan her şey için Parameter Store kullan. Bu basit kural maliyetlerde %80 tasarruf sağlayabilir.

  2. Cross-Account Access Customer-Managed Key Gerektirir: Default aws/secretsmanager key çalışmaz. Migration acısından kaçınmak için ilk günden customer-managed KMS key'ler oluştur.

  3. Container Injection Tek Seferlik: Startup'ta inject edilen secret'lar rotation'da güncellenmez. Uygulamaları connection refresh handle edecek şekilde tasarla veya alternating-users strategy kullan.

  4. Lambda Extension Maliyeti %99 Azaltır: Yüksek trafikli Lambda function'lar için extension'ın built-in caching'i esansiyel. Tek satırlık bir ekleme önemli para tasarrufu sağlar.

  5. CloudTrail Data Event'leri Kritik: İlk günden enable et. Maliyet ihmal edilebilir (~$0.10 per 100,000 event) ama audit değeri ölçülemez.

  6. Multi-Region Replication İş Kararı: Her şeyi replicate etme. RTO/RPO requirement'larını analiz et ve sadece kritik secret'ları replicate et. Cross-region API call'lar genellikle kabul edilebilir.

  7. Break-Glass Prosedürleri Test Edilmeli: Test edilmemiş emergency access incident'lar sırasında işe yaramaz. Hem teknik hem organizasyonel hazırlığı validate etmek için quarterly test yap.

  8. Otomasyon Süreci Yener: Manuel rotation mühendis zamanında ayda 250amalolur.Otomatikrotationayda250'a mal olur. Otomatik rotation ayda 4'a mal olur. ROI anında gerçekleşir.

Anahtar güvenlik, maliyet ve operasyonel kompleksiteyi dengelemek. Statik config için Parameter Store ile basit başla, sensitive credential'ları Secrets Manager'a migrate et, database'ler için rotation implement et ve sadece gerektiğinde cross-region replication ekle.


İlgili Konular:

İlgili Yazılar