İçeriğe atla

2025-12-23

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:

Evet

Hayir

Evet

Hayir

RDS/Redshift/DocumentDB

Custom Uygulama

Basit

Kompleks

Secret Tipi?

Sensitive Data?

Rotation Gerekli?

Parameter Store Standard

Ucretsiz

Built-in Destek?

Parameter Store SecureString

Ucretsiz

Secrets Manager

$0.40/ay

Rotation Karmasikligi?

Secrets Manager

Lambda ile

Vault veya

Custom Cozum Dusun

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ış

1. AssumeRole

2. GetSecretValue

3. KMS Decrypt

4. Return Secret

Account A - Owner

111111111111

Secret

Customer KMS Key

Account B - Consumer

222222222222

IAM Role

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.

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:

# 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 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.

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

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 ($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:

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

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

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

# 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 Configuration

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 Configuration

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 Implementation

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

Rotation Flow

DatabaseLambda FunctionSecrets ManagerDatabaseLambda FunctionSecrets ManagerAWSPENDING label ile saklaPassword'u guncelleAWSCURRENT'i yeni versiona tasiAWSPENDING label'i kaldir1. createSecretYeni password olustur2. setSecretALTER USER password3. testSecretConnection'i test etConnection OK4. finishSecret

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
    )

CDK Setup for Rotation

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:

  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

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

Break-Glass Role Architecture

// 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 Access Monitoring

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

  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:

# 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-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 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

Audit Logging with CloudTrail

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

CloudTrail Configuration

// 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.

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

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

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

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:

// 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ç: %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 yazılar