Mert Özen Her Satırda Daha İleri
DevOps Configuration ve Secret Management — #07

Secret Rotation ve Sızıntı Müdahalesi: Kesintisiz Rotation, Sızan Secret'ta İlk 60 Dakika ve Secret'sız Mimari

Mert Özen 14 Kas 2026 10 dk 8 görüntülenme
Secret Rotation ve Sızıntı Müdahalesi: Kesintisiz Rotation, Sızan Secret'ta İlk 60 Dakika ve Secret'sız Mimari

Kesintisiz rotation için bir süre iki secret birlikte geçerli sayılır: yeni secret önce kabul eden tarafa eklenir, her yere yayılınca kullanılmaya başlanır, eski secret ölçümle kaldırılır. Bookstore'un ödeme webhook'unda primary ve secondary secret ile restart'sız rotation'ı, Vault audit log'uyla sızıntı kapsamını çıkarmayı, Key Vault'un SecretNearExpiry olayıyla otomasyonu ve Bookstore'un Azure'a ve veritabanına erişen secret sayısını sıfıra indiren secret'sız mimariyi görüyoruz.

Sürüm notu: Bu yazıdaki örnekler .NET 10 (SDK 10.0.402), xUnit 2.9, HashiCorp Vault 2.2.2 ve openssl ile denenmiştir. Azure Key Vault, Event Grid, Azure SQL ve GitHub Actions adımları Microsoft Learn ve GitHub dokümanlarına göre yazıldı, çalıştırılmadı.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) ödeme sağlayıcısı, her ödeme olayını paylaşılan bir webhook secret'ıyla imzalıyor ve Bookstore imzayı doğruluyor. Secret'ı değiştirmen gerekiyor. Sağlayıcının panelinde yeni secret'ı kaydettiğin an ile Bookstore'un üç instance'ına yeni değerin ulaştığı an arasında gelen ödeme bildirimlerine ne olur? İkinci soru: bir secret'ın sızdığını öğrendin. İlk yapacağın şey Git geçmişini temizlemek mi, yoksa başka bir şey mi?

Rotation Neden Zor?

Bir secret'ı değiştirmenin iki tarafı var: secret'ı üreten ya da doğrulayan sistem (ödeme sağlayıcısı, veritabanı, JWT (JSON Web Token; imzalı kimlik token'ı) doğrulayan API) ve onu kullanan uygulama. İki taraf hiçbir zaman aynı anda değişmez. Tek bir secret ile çalışıyorsan, bir tarafın yeni değere geçtiği an ile diğerinin geçtiği an arasındaki her istek reddedilir. Bu pencere, önceki yazılarda ölçtüğümüz gecikmelerin toplamı: Key Vault reload aralığı, ESO (External Secrets Operator; dış store'dan Kubernetes Secret'ı üreten controller) refresh aralığı, kubelet'in volume güncellemesi, App Service reference'larının 24 saatlik cache'i ya da environment variable kullanılıyorsa bir restart.

Bu yüzden kesintisiz rotation'ın temel fikri her yerde aynı: bir süre için iki secret'ı birden geçerli saymak. Yeni secret önce "kabul edilen" tarafa eklenir, her yere yayıldığından emin olunca "kullanan" taraf yeni secret'a geçer, en sonunda eski secret kaldırılır. Bu üç adımlı akışın adı pattern literatüründe genellikle "dual credential" ya da "two-key rotation".

İki Aktif Secret: Bookstore'un Webhook Secret'ı

07-two-secret-rotation-tr.png

Bookstore tarafında options class'ına ikinci bir alan eklemek yeterli:

public sealed class PaymentWebhookOptions
{
    public const string SectionName = "Payments:Webhook";

    // Ödeme sağlayıcısının şu an imzaladığı secret
    [Required, MinLength(32)]
    public string Secret { get; set; } = "";

    // Rotation sırasında geçici olarak kabul edilen ikinci secret
    public string? SecondarySecret { get; set; }
}

Doğrulama iki secret'ı da dener ve hangisinin eşleştiğini döndürür. İmzaları karşılaştırırken CryptographicOperations.FixedTimeEquals kullanmak şart; sıradan string karşılaştırması ilk farklı karakterde durur ve süre farkı imzanın tahmin edilmesine yardım eder:

public static string Sign(string secret, ReadOnlySpan<byte> body) =>
    Prefix + Convert.ToHexStringLower(HMACSHA256.HashData(Encoding.UTF8.GetBytes(secret), body));

// İki secret da denenir; karşılaştırma sabit zamanlı
public static WebhookSecretMatch Verify(string? header, ReadOnlySpan<byte> body, string primary, string? secondary)
{
    if (string.IsNullOrEmpty(header) || !header.StartsWith(Prefix, StringComparison.Ordinal))
    {
        return WebhookSecretMatch.None;
    }

    if (Matches(header, body, primary))
    {
        return WebhookSecretMatch.Primary;
    }

    return !string.IsNullOrEmpty(secondary) && Matches(header, body, secondary)
        ? WebhookSecretMatch.Secondary
        : WebhookSecretMatch.None;
}
// IOptionsMonitor: rotation'da yeni secret restart olmadan görünür
var current = options.CurrentValue;
var match = WebhookSignature.Verify(
    request.Headers["X-Bookstore-Signature"], buffer.ToArray(), current.Secret, current.SecondarySecret);

if (match == WebhookSecretMatch.None)
{
    return Results.Unauthorized();
}

// Hangi secret'ın kullanıldığı, eskisini ne zaman kaldırabileceğini söyler
loggerFactory.CreateLogger("Bookstore.Payments.Webhook").LogInformation(
    "Payment webhook verified with {SecretSlot} secret", match);
return Results.Ok(new { Accepted = true });

Rotation'ı gerçekten yaptım. Secret'lar altıncı yazıdaki key-per-file düzeniyle bir klasörden okunuyor, böylece dosyayı değiştirmek Kubernetes'te ESO'nun Secret'ı güncellemesine karşılık geliyor. Ödeme olayını openssl ile imzalayan küçük bir script, sağlayıcı rolünü oynuyor:

BODY='{"orderId":"A-1001","status":"paid","amount":389.00}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$(tr -d '\n' < $1)" -hex | awk '{print $NF}')
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:5097/api/payments/webhook \
  -H "Content-Type: application/json" -H "X-Bookstore-Signature: sha256=$SIG" --data "$BODY"
# 1) Yalnız eski secret tanımlı
old=200 new=401
# 2) Yeni secret "Secret", eskisi "SecondarySecret": ikisi de geçerli
old=200 new=200
# 3) SecondarySecret dosyası silindi; polling watcher değişikliği gördükten sonra
+0s old=200
+2s old=200
+4s old=200
+6s old=401
info: Bookstore.Payments.Webhook[0]
      Payment webhook verified with Primary secret
info: Bookstore.Payments.Webhook[0]
      Payment webhook verified with Secondary secret

Uygulama hiç yeniden başlamadı; IOptionsMonitor yeni değerleri gördü. Baştaki sorunun cevabı da burada: iki secret'ı birlikte kabul eden bir pencere yoksa, sağlayıcının yeni secret'la imzaladığı ama Bookstore'a henüz ulaşmamış her bildirim 401 alır; sağlayıcı da yeniden dener ya da vazgeçer. Log'daki Secondary satırları eski secret'ın hâlâ kullanıldığını gösteriyor. Bu sayı sıfıra inmeden eskisini kaldırmamak, overlap penceresinin süresini tahmin yerine ölçümle belirlemek demek.

Aynı kodda okuma IOptions<PaymentWebhookOptions> ile yapılsaydı, ikinci yazıda gördüğümüz gibi uygulama ilk okuduğu secret'ta kalırdı. Rotation sonrası en sık görülen sorun bu: vault'ta, Secret'ta ve dosyada yeni değer duruyor, ama bir singleton eski değeri bellekte tutuyor.

JWT Signing Key ve SQL Parolası için Aynı Fikir

JWT signing key'inde iki aktif key, "imzalama için tek key, doğrulama için birden çok key" demek. Yeni key önce doğrulama listesine eklenir, sonra token'lar onunla imzalanmaya başlar, eski key ise en uzun ömürlü token'ın süresi dolana kadar listede kalır. Token'daki kid başlığı hangi key'in kullanıldığını söyler ve doğrulayıcılar key'leri bir JWKS (JSON Web Key Set; public key'lerin yayımlandığı JSON listesi) endpoint'inden alır; mekanizma Authentication ve Authorization serisinin JWT Derinlemesine yazısında ayrıntısıyla anlatıldı. Bookstore'un /api/info'daki jwtKeyFingerprint alanı bu yüzden işe yarıyor: bir rotation sırasında hangi instance'ın hangi key'le çalıştığını değeri göstermeden gösteriyor.

SQL Server parolasında iki yol var. Birincisi iki login: bookstore_app_a ve bookstore_app_b. Uygulama A ile çalışırken B'nin parolası değiştirilir, connection string B'ye çevrilir ve yayılması beklenir, sonra A'nın parolası değiştirilir; sıradaki rotation'da roller yer değiştirir. Bu Key Vault'un Microsoft Learn'deki "iki credential seti" rotation öğreticisindeki fikirle aynı: secret'ın her yeni sürümü iki credential'dan diğerini taşır. İkinci yol beşinci yazıdaki dynamic credential: her instance'ın kendi kısa ömürlü kullanıcısı olduğu için rotation diye ayrı bir işlem kalmaz, Vault Agent bunu sürekli yapar.

Rotation'ı Otomatikleştirmek

Elle yapılan rotation unutulur. Key Vault'ta iki mekanizma var. Key'ler (kriptografik anahtarlar) için bir rotation policy tanımlanabilir; Key Vault yeni sürümü kendisi üretir. Secret'lar için ise Key Vault değeri kendisi üretemez, çünkü değerin geçerli olduğu yer (ödeme sağlayıcısı, veritabanı) Key Vault'un dışında. Bunun yerine Microsoft Learn'deki öğreticide anlatılan olay tabanlı akış kullanılır: secret'a bir son kullanma tarihi verilir, Key Vault süre dolmadan 30 gün önce Event Grid'e SecretNearExpiry olayını yayınlar, bu olaya abone bir Function yeni değeri üretip hem hedef sisteme hem Key Vault'a yazar. Bookstore için kurulum şöyle olurdu (çalıştırılmadı):

# Secret'a son kullanma tarihi ver: olay bu tarihten 30 gün önce gelir
az keyvault secret set --vault-name bookstore-kv-prod \
  --name BookstoreApi-Payments--Webhook--SecondarySecret --value "$NEW_SECRET" \
  --expires 2027-02-14T00:00:00Z --tags ValidityPeriodDays=90

# Olayı rotation Function'ına bağla
az eventgrid event-subscription create --name payments-webhook-near-expiry \
  --source-resource-id $(az keyvault show --name bookstore-kv-prod --query id -o tsv) \
  --endpoint-type azurefunction --endpoint "$ROTATION_FUNCTION_ID" \
  --included-event-types Microsoft.KeyVault.SecretNearExpiry

Function'ın kendisi yukarıdaki üç adımı uygular: yeni secret'ı sağlayıcıda oluşturur ve Key Vault'a secondary olarak yazar, yayılma süresi kadar bekler, sağlayıcıyı yeni secret'a geçirir ve sonraki çalışmada eskisini kaldırır. Bu sürenin ne olacağını artık biliyoruz: üçüncü yazıdaki ReloadInterval, altıncı yazıda ölçtüğümüz yaklaşık bir dakikalık kubelet gecikmesi ve ESO'nun refresh aralığının toplamından uzun olmalı.

Sızıntıda İlk 60 Dakika

Bir secret'ın sızdığını öğrendiğinde sıra önemli. Tespit tarafı (secret scanning, push protection) ve Git geçmişini temizleme adımları DevSecOps serisinin Secret Scanning yazısında anlatılıyor; burada secret'ın kendisini yöneten taraf açısından sırayı veriyoruz.

  • 0–15 dakika: secret'ı geçersiz kıl. Rotation prosedürü hazırsa onu çalıştır; değilse önce eski değeri iptal et, kısa bir kesinti, sızan bir secret'la çalışmaya devam etmekten iyidir. Vault'ta lease'ler vault lease revoke -prefix ile toplu iptal edilir.
  • 15–30 dakika: kapsamı belirle. Secret ne zamandan beri açıktaydı, kimler okudu, hangi işlemler yapıldı? Key Vault diagnostic log'ları, Vault audit log'u ve hedef sistemin log'ları (ödeme sağlayıcısının paneli, veritabanı login kayıtları) burada kullanılır.
  • 30–45 dakika: kaynağı kapat. Secret commit'lendiyse Git geçmişini temizle; ama bu adım tek başına yetmez, çünkü fork'lar, clone'lar ve CI cache'i değeri zaten aldı. Log'a ya da dump'a düştüyse oradaki kopyaları sil.
  • 45–60 dakika: bildir ve kaydet. Etkilenen ekiplere ve gerekiyorsa müşterilere haber ver, olayı bir postmortem için zaman çizelgesiyle yaz, aynı secret türü için secret'sız bir alternatif var mı diye bak.

Kapsam analizinin nasıl yapıldığını Vault'un audit log'uyla denedim. Audit device'ı açıp ödeme secret'ını AppRole kimliğiyle okudum:

$ vault audit enable file file_path=/work/vault-audit.log
# Uygulamanın bookstore-payments kimliğiyle alınmış token ile
$ vault kv get -field=webhook_secret secret/bookstore/payments
{
  "time": "2026-11-14T10:01:15.247280166Z",
  "type": "response",
  "auth": {
    "display_name": "approle",
    "policies": [
      "bookstore-payments",
      "default"
    ],
    "metadata": {
      "role_name": "bookstore-payments"
    },
    "accessor": "hmac-sha256:239ba5efef357595ad7b842c7b5d90044289d5932c67e686b8f8176c196940c7"
  },
  "request": {
    "operation": "read",
    "path": "secret/data/bookstore/payments",
    "remote_address": "127.0.0.1"
  },
  "response": {
    "data": {
      "data": {
        "webhook_secret": "hmac-sha256:e3ebe3a8b72d7ff5dc0296efc0226384de2b6886ea3d5fa1a30f29d86a1358dd"
      }
    }
  }
}

Log'da secret'ın değeri yok, yalnızca HMAC'ı (Hash-based Message Authentication Code; bir key ile hesaplanan özet) var. Kimin, hangi rolle, ne zaman ve nereden okuduğu ise açık. Sızan değerin hangi kayıtlarda geçtiğini bulmak için aynı HMAC'ı Vault'a hesaplatıp log'da aramak yeterli:

$ vault write -field=hash sys/audit-hash/file input="$LEAKED_VALUE"
hmac-sha256:e3ebe3a8b72d7ff5dc0296efc0226384de2b6886ea3d5fa1a30f29d86a1358dd
$ grep -c "hmac-sha256:e3ebe3a8b72d7ff5dc0296efc0226384de2b6886ea3d5fa1a30f29d86a1358dd" vault-audit.log
7

Yedi kayıt; yazma ve okuma istekleriyle cevaplarının hepsi. Gerçek bir olayda bu listeyi beklenen kimliklerle karşılaştırırsın: bookstore-payments dışında bir rolden gelen okuma, sızıntının Vault'un içinden de olabileceğini gösterir.

Secret'sız Mimariye Geçiş

En iyi rotation, rotation gerektirmeyen tasarımdır. İlk yazıdaki envanteri serinin sonunda tekrar açalım:

Bookstore secret'ıSerinin başındaSerinin sonunda
SQL connection stringParolalı, environment variableAzure'da managed identity ile parolasız; diğer ortamlarda Vault'tan kısa ömürlü kullanıcı
Key Vault ve App Configuration erişimi-Managed identity ya da workload identity, secret yok
Storage (kitap kapakları)Account keyManaged identity ve rol ataması, account key kapalı
CI/CD'nin Azure erişimiService principal secret'ıGitHub Actions OIDC (OpenID Connect; token tabanlı kimlik protokolü), secret yok
JWT signing keyEnvironment variableKey Vault, iki aktif key ile rotation
Ödeme webhook secret'ıEnvironment variableKey Vault, primary ve secondary ile rotation
E-posta servisi API key'iEnvironment variableKey Vault, yalnız Notifications module'ü okur

Uygulamanın kendisinin sakladığı secret sayısı dörtten üçe, Azure'a ve veritabanına erişen secret sayısı sıfıra indi. Geriye kalan üçü dış bir tarafın (ödeme sağlayıcısı, e-posta servisi) bize verdiği secret'lar ya da kendi imzaladığımız token'ların key'i; bunlar secret'sız hâle getirilemez, ama artık merkezde, denetlenebilir ve iki secret'lı rotation ile değiştirilebilir durumda.

Azure SQL'e managed identity ile bağlanmak connection string'de tek bir ayardan ibaret; Microsoft.Data.SqlClient token'ı uygulamanın identity'si ile kendisi alır. Veritabanında da Entra ID yöneticisi olarak bağlanıp identity için bir kullanıcı oluşturursun (ikisi de çalıştırılmadı):

Server=tcp:bookstore-sql.database.windows.net,1433;Database=BookstoreDb;Authentication=Active Directory Managed Identity;Encrypt=True
CREATE USER [bookstore-api-prod] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [bookstore-api-prod];
ALTER ROLE db_datawriter ADD MEMBER [bookstore-api-prod];

Storage tarafında aynı fikir "Storage Blob Data Reader" gibi bir rolle ve account key ile erişimi kapatarak uygulanır. Pipeline tarafında GitHub Actions'ın OIDC token'ıyla Azure'a secret'sız giriş, GitHub Actions serisinin OIDC ile Azure App Service'e deploy yazısında kuruldu.

Üretimde Dikkat

Rotation'ı yalnızca production'da denemek, onu ilk kez bir olay sırasında denemek demek. Rotation prosedürünü staging'de düzenli aralıklarla, tercihen otomatik olarak çalıştır ve süresini ölç; "sızıntıda kaç dakikada değiştirebilirim?" sorusunun cevabı bu ölçüm. Her secret'ın bir son kullanma tarihi olsun; Key Vault'un SecretNearExpiry olayı ya da Vault'taki TTL (time to live; yaşam süresi) değerleri, unutulan secret'ları görünür kılar. Overlap penceresini ölçümle belirle: Secondary ile doğrulanan isteklerin sayısını bir metric olarak izle ve sıfıra inmeden eski secret'ı kaldırma. Rotation bir değişiklik olduğu için kim, ne zaman, neden yaptı sorusunun cevabı audit log'da ve değişiklik kaydında bulunmalı.

Sık Düşülen Tuzaklar

Secret'ı IOptions<T> ya da bir singleton'da saklayıp rotation'dan sonra eski değerle çalışmaya devam etmek. Tek secret ile rotation yapıp yayılma süresi boyunca istekleri reddetmek. Eski secret'ı "nasılsa kimse kullanmıyor" diye ölçmeden kaldırmak. Sızıntıda önce Git geçmişini temizleyip secret'ı geçersiz kılmayı geciktirmek. İmzaları == ile karşılaştırmak. Rotation'ı yalnızca production'da ve yalnızca bir olay sırasında yapmak. Managed identity mümkünken parolalı connection string'de ısrar etmek; ne kadar iyi rotation yaparsan yap, olmayan bir parola her zaman daha güvenlidir.

Küçük Bir Deneme

★☆☆ Kendi uygulamanda bir secret seç ve rotation adımlarını yaz: yeni değer nerede üretilir, kabul eden taraf hangisi, yayılma süresi ne kadar, eski değer ne zaman kaldırılır?

★★☆ Webhook doğrulamasına SecondarySecret ekle, IOptionsMonitor ile oku ve rotation'ı bu yazıdaki gibi dosya değiştirerek dene. Aynı denemeyi IOptions ile tekrarla ve eski secret'ın ne kadar süre geçerli kaldığını gözle.

★★★ Vault'ta audit device'ı aç, birkaç farklı rolle bir secret'ı oku ve sys/audit-hash ile değerin geçtiği kayıtları bul. Ardından bir "sızıntı tatbikatı" yap: secret'ı iptal et, kapsamı log'lardan çıkar ve 60 dakikalık runbook'un her adımının ne kadar sürdüğünü not et.

Serinin sonunda Bookstore'un configuration'ı katmanlı, secret'ları merkezde, kimlikleri parolasız ve kalan secret'ları kesintisiz değiştirilebilir durumda. Buradan devam etmek isteyenler için iki yön var: secret'ların repo'ya ve image'a hiç girmemesini sağlayan tespit araçları DevSecOps serisinde, şifreli secret'ları Git'te tutup cluster'a GitOps ile taşımak ise GitOps ve Argo CD serisinde.