Önceki yazıda ZITADEL'in Postgres parolasını ve masterkey'ini Git'e base64 olarak koydum ve bunu bilerek yaptığımı söyledim. Bu yazıda onları Vault'a taşıyoruz. Önce şunu söylemem gerekiyor: dosyayı Git'ten silmek bir şey çözmüyor. Değer Git geçmişinde kalıyor ve git show blog-05:zitadel/db/secrets.yaml her zaman onu geri getiriyor. Gerçekten işe yarayan tek şey değeri değiştirmek.
Bu yazının sonunda ZITADEL'in üç Secret'ı Git'te değil, Vault'ta. Kubernetes'e External Secrets Operator (ESO) yazıyor, Git'te sadece "değer nerede" bilgisi kalıyor. Postgres parolası değişti. Değiştirme sırasında ZITADEL'e yarım saniyede bir istek attım; 180 isteğin hepsi 200 döndü. Git geçmişindeki eski parola artık FATAL: password authentication failed alıyor. 05'te Git'e hiç girmemiş tek secret da, yani Argo CD'nin ZITADEL'deki client secret'ı, aynı yoldan geçiyor ve 0,7 saniyede yenileniyor.
Bir düzeltme de yapmam lazım: 05'in sonunda "taşırken hepsini değiştiriyoruz" yazdım. Masterkey için bu mümkün değil. ZITADEL verisini o anahtarla şifreliyor; yeni bir anahtar eski veriyi okuyamaz. Masterkey Vault'a aynı değerle taşınıyor ve bunu Adım 3'te açıkça anlatıyorum.
Lab vault-eso-secret-migration klasöründe. Platform tarafı platform-gitops reposunun blog-05b branch'inde. Branch'teki üç commit geçişin üç adımı; her birini aşağıda ayrı ayrı açıyorum. blog-05 olduğu gibi duruyor, önceki yazının lab'ı hâlâ ondan çalışıyor.
Neye ihtiyacınız var
- Önceki yazının lab'ı: Argo CD SSO Entegrasyonu: ZITADEL ile OIDC ve RBAC. Çalışan bir 05 cluster'ınız varsa
make from-05onublog-05b'ye geçiriyor. Yoksa bu lab'ın Makefile'ı her şeyi baştan kuruyor. - 4 CPU / 8 GB Docker (Colima, Apple Silicon). Vault ve ESO 05'in üstüne yaklaşık 180 MiB ekliyor. Ölçülen working set:
vault-070 MiB, ESO controller 33, webhook 26, cert-controller 51. kind,kubectl,helm3.15+,jq,curl,openssl.- Bu çalıştırmadaki sürümler: hashicorp/vault chart 0.34.1 (Vault 2.0.4), external-secrets chart 2.11.0 (ESO v2.11.0, API
external-secrets.io/v1), argo/argo-cd chart 10.9.2 (Argo CD v3.5.3), zitadel/zitadel chart 10.1.0 (ZITADEL v4.19).
Neden bu şekilde
Dosyayı silmek değil, değeri değiştirmek. Bir secret bir kez Git'e girdiyse, o repoyu okuyabilmiş herkes ona sahip olmuş kabul edilmeli. git filter-repo ile geçmişi temizlemek mümkün, ama klonları, fork'ları ve CI cache'lerini temizlemiyor. Bu yüzden geçişin asıl adımı taşımak değil, rotasyon.
ESO, Vault Agent injector değil. ESO, Vault'taki değerden normal bir Kubernetes Secret'ı üretiyor. Secret'ların adları değişmediği için ZITADEL'in chart'ına da Postgres StatefulSet'ine de dokunmadım; ikisi de hâlâ zitadel-db ve zitadel-postgres Secret'larını okuyor. Injector ise her pod'a bir sidecar ekleyip değeri dosyaya yazıyor; bu da ZITADEL'in nasıl başladığını değiştirmek demek. Bedeli şu: değer etcd'de yine bir Secret olarak duruyor. etcd şifrelemesi ve RBAC hâlâ gerekli; ESO bu ikisinin yerine geçmiyor.
SOPS ya da Sealed Secrets değil. İkisi de şifreli değeri Git'te tutuyor. Rotasyon yine bir commit gerektiriyor ve anahtar sızarsa geçmişteki her değer açılıyor. Vault'ta her değerin bir sürüm geçmişi var, geri alma tek bir komut ve kimin neyi okuduğu tek bir yerde tutuluyor.
Vault cluster'ın içinde, tek node. Lab için. Production'da Vault cluster'ın dışında, 3 VM'de çalışıyor; anahtar 5 parçaya bölünüyor ve açmak için 3'ü gerekiyor (Shamir). Init ve unseal adımları ikisinde de aynı, lab'da da 5/3 kullandım. Lisans notu: Vault 1.15'ten beri BSL 1.1 ile dağıtılıyor; Linux Foundation altındaki açık kaynak çatalı OpenBao. OpenBao'yu bu lab'da denemedim.
Production ile bir fark. Bizim production'da ESO 0.10.4 çalışıyor ve kaynaklar external-secrets.io/v1beta1. Burada 2.11 ve v1 var. Buradaki manifestleri v1beta1 ile denemedim; eski bir ESO'ya uygularken kubectl explain externalsecret.spec ile alanlara bakın.
Adım 1 — Vault ve ESO'yu Argo CD kuruyor
blog-05b'nin ilk commit'i (3011bfd) apps/ dizinine üç Application ekliyor:
root (Application)
├── platform, products (AppProject, wave -2)
├── argocd (wave -1)
├── applicationsets, external-secrets (wave 0) → ESO operatörü ve CRD'leri
├── zitadel-db, vault, external-secrets-store (wave 1)
└── zitadel (wave 2)
external-secrets-store ayrı bir Application, çünkü ClusterSecretStore'un CRD'si external-secrets ile geliyor. İkisi aynı Application'da olsaydı, CRD henüz yokken store'u uygulamaya çalışırdı. ESO Application'ında ServerSideApply=true var: CRD'ler, kubectl apply'ın 256 KiB'lık last-applied annotation sınırını aşıyor.
Vault'un values dosyası kısa:
# platform-gitops/vault/values.yaml
injector:
enabled: false # secrets reach pods through ESO, not the sidecar injector
server:
ha:
enabled: true
replicas: 1
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
tls_disable = 1 # lab: port-forward only; production terminates TLS on Vault itself
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "raft" {
path = "/vault/data"
}
service_registration "kubernetes" {}
ha.enabled: true ve replicas: 1 ilk bakışta çelişkili görünüyor. Chart'ta raft depolama HA modunun içinde geliyor; tek replica, tek node'luk bir raft cluster'ı demek. Production'a geçerken sadece replica sayısı değişiyor.
Argo CD Vault'u kuruyor, ama açmıyor. Vault ilk açılışta sealed: elinde şifreleme anahtarı yok, hiçbir isteğe cevap vermiyor ve pod'u 0/1 Ready. Init ve unseal bir insanın işi:
$ make vault-init
initialized, keys in .lab/vault-init.json
Sealed false
Version 2.0.4
Storage Type raft
HA Mode active
.lab/vault-init.json içinde 5 anahtar parçası ve root token var. Lab'da .lab/ dizininde, .gitignore'da ve make down ile siliniyor. Production'da bu 5 parça 5 ayrı kişiye gidiyor ve root token kurulumdan sonra iptal ediliyor.
Sonra ESO'nun Vault'a nasıl gireceği:
$ make vault-config
Success! Enabled the kv-v2 secrets engine at: secret/
Success! Enabled kubernetes auth method at: kubernetes/
Success! Data written to: auth/kubernetes/config
Success! Uploaded policy: eso-read
Success! Data written to: auth/kubernetes/role/eso
# scripts/vault-config.sh — ESO only reads
path "secret/data/platform/*" { capabilities = ["read"] }
path "secret/metadata/platform/*" { capabilities = ["read", "list"] }
ESO, Vault'a kendi ServiceAccount'ıyla giriyor: Kubernetes auth, ServiceAccount token'ını Kubernetes API'ye sorarak doğruluyor. Hiçbir yerde bir Vault token'ı saklanmıyor. Role'e bağlı policy sadece okuma izni veriyor. Vault'a yazmak bir insanın işi, cluster'ın değil.
# platform-gitops/external-secrets/store/cluster-secret-store.yaml
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault
spec:
provider:
vault:
server: http://vault-active.vault.svc:8200
path: secret
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: eso
serviceAccountRef:
name: external-secrets
namespace: external-secrets
audiences: [vault]
$ kubectl get clustersecretstore vault
NAME AGE STATUS CAPABILITIES READY
vault 8s Valid ReadWrite True
ReadWrite, store'un yetenek listesi; ESO'ya verilen yetkiyi değil, provider'ın neyi desteklediğini söylüyor. Gerçek yetki Vault'taki eso-read policy'si.
UI'da: make vault-ui → http://localhost:8200, token jq -r .root_token .lab/vault-init.json. secret/platform/ altında iki dizin var: ZITADEL'in değerleri ve Argo CD'nin OIDC client'ı.

Adım 2 — Yan yana: aynı değerler, farklı isimler
İlk turda değerleri değiştirmeden Vault'a koydum. Değerler blog-05 branch'inden okunuyor; zaten oradan sızıyorlar:
# scripts/seed-from-git.sh (özü)
RAW=https://raw.githubusercontent.com/miraccan00/platform-gitops/blog-05/zitadel/db/secrets.yaml
v kv put secret/platform/zitadel/db POSTGRES_USER=... POSTGRES_PASSWORD=... POSTGRES_DB=... ZITADEL_DATABASE_POSTGRES_DSN=...
v kv put secret/platform/zitadel/masterkey masterkey=...
Sonra her Secret için bir ExternalSecret yazdım, ama hedefi farklı bir isimle: zitadel-db yerine zitadel-db-eso. Bu kopyaları hiçbir şey okumuyor. Tek amaçları, Vault → ESO → Secret yolunun Git'teki değerin aynısını ürettiğini kanıtlamak. Değerleri ekrana basmadan karşılaştırmak için her anahtarın SHA-256'sının ilk 12 karakterine baktım:
$ bash scripts/compare.sh zitadel zitadel-postgres zitadel-postgres-eso
== zitadel-postgres
POSTGRES_DB 713824a0429e
POSTGRES_PASSWORD f05fca35a4c1
POSTGRES_USER 713824a0429e
== zitadel-postgres-eso
POSTGRES_DB 713824a0429e
POSTGRES_PASSWORD f05fca35a4c1
POSTGRES_USER 713824a0429e
zitadel-db'nin DSN'i (db9451511e8d) ve masterkey (c4579c585107) de birebir aynı çıktı. POSTGRES_DB ile POSTGRES_USER'ın hash'i de aynı, çünkü ikisinin değeri de zitadel.
Bu adımın asıl değeri şu: bir şey yanlışsa (yanlış path, eksik anahtar, ESO'nun Vault'a girememesi) bunu hiçbir şey kırılmadan, burada görüyorsunuz.
Adım 3 — Kesme: Secret'lar Git'ten çıkıyor, isimler kalıyor
İkinci commit (e872343) iki şey yapıyor: zitadel/db/secrets.yaml'ı siliyor ve ExternalSecret'ların hedefini gerçek isimlere çeviriyor. Değerler hâlâ aynı. Bu yüzden ZITADEL açısından hiçbir şey değişmemeli.
# platform-gitops/zitadel/db/externalsecrets.yaml (biri)
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: zitadel-db
namespace: zitadel
annotations:
argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
spec:
refreshInterval: 1h
secretStoreRef: {kind: ClusterSecretStore, name: vault}
target: {name: zitadel-db}
data:
- {secretKey: ZITADEL_DATABASE_POSTGRES_DSN, remoteRef: {key: platform/zitadel/db, property: ZITADEL_DATABASE_POSTGRES_DSN}}
SkipDryRunOnMissingResource, temiz bir cluster için var. zitadel-db Application'ı wave 1'de ve ESO'nun CRD'si o anda henüz yoksa, Argo CD'nin dry-run'ı "böyle bir kaynak türü yok" diye bütün sync'i durdururdu. Application'a bir de retry ekledim.
Masterkey de aynı şekilde bir ExternalSecret'a geçiyor, değeri değişmeden. Dosyadaki yorum durumu açıkça söylüyor: ZITADEL encrypts its data with this key, a new one can't read the old data. Sonuç olarak Git geçmişindeki masterkey geçerliliğini koruyor. Repoyu okuyabilmiş biri, veritabanının bir kopyasını da ele geçirirse ZITADEL'in şifreli alanlarını açabilir. Bunun önlemi Postgres'e erişimi kapatmak ve parolayı değiştirmek; ikisi de bu yazıda var.
Push'tan sonra Argo CD'nin sync sonucu:
Secret zitadel-masterkey Pruned pruned
Secret zitadel-postgres Pruned pruned
Secret zitadel-db Pruned pruned
ExternalSecret zitadel-postgres-eso Pruned pruned
ExternalSecret zitadel-masterkey-eso Pruned pruned
ExternalSecret zitadel-db-eso Pruned pruned
ExternalSecret zitadel-db Synced externalsecret.external-secrets.io/zitadel-db created
ExternalSecret zitadel-masterkey Synced externalsecret.external-secrets.io/zitadel-masterkey created
ExternalSecret zitadel-postgres Synced externalsecret.external-secrets.io/zitadel-postgres created
Argo CD Git'ten kalkan üç Secret'ı sildi. ESO aynı saniyede (17:58:57) aynı isimlerle yenilerini yarattı; UID'ler değişti, sahipleri artık ExternalSecret. ZITADEL pod'u yeniden başlamadı, çünkü env değişkenlerini başlarken okumuştu. Ama arada, Secret'ın hiç olmadığı kısa bir an vardı. O an pod yeniden başlasaydı CreateContainerConfigError alırdı. Ne ters gitti #1'de bunu nasıl önleyeceğimi anlatıyorum.
UI'da: zitadel-db Application'ının ağacında her ExternalSecret'ın altında, ondan doğan Secret görünüyor. Argo CD bu ilişkiyi Secret'taki ownerReference'tan okuyor:

Adım 4 — Rotasyon: Postgres parolası, kesintisiz
Değer artık Vault'ta. Sırada onu değiştirmek var. Sıralama önemli, çünkü parolayı iki taraf biliyor: Postgres ve ZITADEL'in DSN'i.
# scripts/rotate-db-password.sh (özü)
NEW=$(openssl rand -hex 24)
# 1. Postgres. Inside the pod psql connects over the local socket without a password.
kubectl -n zitadel exec zitadel-postgres-0 -- psql -U zitadel -d zitadel -qc "ALTER USER zitadel WITH PASSWORD '$NEW'"
# 2. Vault. patch, not put: put replaces the whole secret and drops every key you didn't repeat.
v kv patch secret/platform/zitadel/db POSTGRES_PASSWORD="$NEW" \
ZITADEL_DATABASE_POSTGRES_DSN="postgresql://zitadel:[email protected]:5432/zitadel?sslmode=disable"
# 3. ESO. Don't wait for refreshInterval (1h): any change to this annotation triggers a sync now.
kubectl -n zitadel annotate es zitadel-db zitadel-postgres force-sync="$(date +%s)" --overwrite
# 4. ZITADEL reads the DSN from env only at start.
kubectl -n zitadel rollout restart deploy/zitadel
Neden önce Postgres: Postgres parolayı sadece yeni bir bağlantı açılırken kontrol ediyor. ALTER USER'dan sonra ZITADEL'in açık bağlantıları çalışmaya devam ediyor; eski pod, yeni DSN'li pod Ready olana kadar istek karşılıyor. Sıra ters olsaydı, yani önce ZITADEL'i yeni DSN ile başlatsaydım, yeni pod Postgres'in henüz bilmediği bir parolayla girmeye çalışırdı.
refreshInterval: 1h bilinçli bir seçim. ESO her saat Vault'a gidip değeri kontrol ediyor. Production'da 68 ExternalSecret'ta 1 saat, sık değişen 2 tanesinde 1 dakika kullanıyoruz. Rotasyonda bir saat beklemek istemiyorsanız force-sync annotation'ı ESO'yu hemen çalıştırıyor.
Rotasyon sırasında ZITADEL'e iki prob bağladım. Biri /debug/ready, diğeri veritabanına giden /oauth/v2/keys; ikisi de yarım saniyede bir istek atıyordu:
$ make rotate-db
deployment.apps/zitadel restarted
Waiting for deployment "zitadel" rollout to finish: 1 old replicas are pending termination...
deployment "zitadel" successfully rolled out
rotated; Vault version: 2
bash scripts/rotate-db-password.sh 0.28s user 0.15s system 13% cpu 3.296 total
== probe: 0 non-200 / 110
== probe-keys: 0 non-200 / 70
Yeni pod 18:02:09'da yeni parolayla 10 bağlantı açtı (pg_stat_activity). 180 isteğin hiçbiri düşmedi.
Yayın günü lab'ı temiz bir cluster'da baştan sona tekrar kurdum ve rotasyonu dört kez daha çalıştırdım. 422 istekten biri, /debug/ready'ye giden, 200 dönmedi. Hangi kodla döndüğünü kaydedemedim, çünkü prob pod'unu çoktan silmiştim. Kesinti değil, ama "sıfır hata" her çalıştırmada garanti de değil. ZITADEL burada tek replica ve eski pod kapanırken gelen bir istek boşa düşebiliyor. İki replica ile tekrar ölçmedim. Sonra asıl soru: Git geçmişindeki parola hâlâ bir şey açıyor mu?
$ OLD=$(git show origin/blog-05:zitadel/db/secrets.yaml | awk '/POSTGRES_PASSWORD:/{print $2}' | base64 -d)
$ kubectl -n zitadel exec zitadel-postgres-0 -- env PGPASSWORD="$OLD" \
psql -h zitadel-postgres.zitadel.svc -U zitadel -d zitadel -c 'select 1'
psql: error: connection to server at "zitadel-postgres.zitadel.svc" (10.96.70.87), port 5432 failed: FATAL: password authentication failed for user "zitadel"
Açmıyor.
Bir yan not: ESO zitadel-postgres Secret'ını da güncelledi ve Postgres StatefulSet'i POSTGRES_PASSWORD'ü oradan okuyor. Ama postgres imajı bu değişkeni sadece veri dizini boşken, ilk initdb'de kullanıyor. Bu yüzden Postgres pod'unu yeniden başlatmak gerekmedi. Parolayı gerçekten değiştiren ALTER USER.
UI'da: Vault'ta değerler maskeli duruyor, ama her değişiklik yeni bir sürüm:


Version 1, Git'ten gelen değer. Gerekirse vault kv rollback -version=1 ile geri dönülebiliyor. Bunun işe yaradığını #2'de, kendi hatamı geri alırken gördüm.
Adım 5 — Git'e hiç girmemiş tek secret
05'te ZITADEL, Argo CD için bir OIDC uygulaması oluşturdu ve client secret'ı üretti. zitadel-setup.sh bu değeri doğrudan argocd/argocd-oidc-zitadel Secret'ına yazdı. Git'te değildi, ama hiçbir yerde kaydı da yoktu: cluster gidince secret da gidiyordu, rotasyonu da bir script'in aklındaydı.
Üçüncü commit (f2e4cd4) bunu da ESO'ya bağlıyor:
# platform-gitops/argocd/secrets/oidc-zitadel.yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: argocd-oidc-zitadel
namespace: argocd
spec:
refreshInterval: 1h
secretStoreRef: {kind: ClusterSecretStore, name: vault}
target:
name: argocd-oidc-zitadel
template:
metadata:
labels:
app.kubernetes.io/part-of: argocd
dataFrom:
- extract: {key: platform/argocd/oidc-zitadel}
template.metadata.labels şart. Argo CD, oidc.config'teki $argocd-oidc-zitadel:clientSecret referansını sadece app.kubernetes.io/part-of: argocd label'lı Secret'larda çözüyor. Bu, 04'te silindiğinde Argo CD'yi düşüren label'ın aynısı. Secret'a onu template.metadata koyuyor.
dataFrom.extract, Vault'taki path'in bütün anahtarlarını Secret'a alıyor (clientID, clientSecret, cliClientID). Pratik, ama bir bedeli var: path'ten bir anahtar düşerse Secret'tan da sessizce düşüyor. #2 tam olarak bu.
Burada ilginç bir fark gördüm. Bu Secret'ı Argo CD değil, bir script yaratmıştı. ESO onu silip yeniden yaratmadı, yerinde devraldı: UID aynı kaldı (676ed02b…), olay Updated: secret updated. Adım 3'teki silme ESO'dan değil, Argo CD'nin prune'undan geliyordu.
Rotasyonda sıra ters, çünkü yeni secret'ı ZITADEL üretiyor:
# scripts/rotate-oidc-secret.sh (özü)
secret=$(api POST "/management/v1/projects/$project/apps/$app/oidc_config/_generate_client_secret" '{}' | jq -r .clientSecret)
v kv patch secret/platform/argocd/oidc-zitadel clientSecret="$secret"
kubectl -n argocd annotate es argocd-oidc-zitadel force-sync="$(date +%s)" --overwrite
$ make rotate-oidc
rotated; Vault version: 2
Secret'taki clientSecret'ın SHA-256 öneki 934c2ab6e5fb iken 4c8dca6d00b0 oldu.
ZITADEL eski secret'ı yenisini ürettiği anda geçersiz kılıyor. Script'in toplam süresi 0,7 saniye; bu sürede başlatılan bir giriş başarısız olurdu. Açık oturumlar etkilenmiyor, çünkü Argo CD client secret'ı sadece giriş sırasında kullanıyor. argocd-server yeniden başlatılmadı; Argo CD Secret'taki değişikliği kendisi okudu ve alice hemen giriş yaptı:
{"userinfo":{"loggedIn":true,"username":"[email protected]","iss":"http://zitadel.127.0.0.1.nip.io:8081","groups":["platform-admins"]},"canSync":{"value":"yes"}, ...}
UI'da: argocd-secrets Application'ında ExternalSecret ve ondan doğan Secret; Vault'ta aynı path'in üç sürümü. Temiz kurulumda bunlar setup (1), cliClientID (2) ve rotasyon (3):


Ne ters gitti
1. Argo CD'nin prune'u ile ESO'nun yaratması arasında bir boşluk var. Adım 3'te Secret'lar, Argo CD sildiği için bir an yoktu. Ölçebildiğim kadarıyla aynı saniye içinde, ama sıfır değil. ZITADEL'in bir replica'sı vardı ve o sırada yeniden başlamadı; şanslıydım.
Adım 5 sebebi gösterdi: Argo CD'nin takip etmediği bir Secret'ı ESO yerinde devralıyor. Bunu bir dahaki sefere iki commit'le yapardım. Birinci commit Git'teki Secret'lara argocd.argoproj.io/sync-options: Prune=false ekler, ikinci commit onları dosyadan siler. Böylece Argo CD Secret'ı silmeden bırakır ve ESO onu yerinde devralır. Bu sırayı lab'da denemedim; çalışan bir 05 cluster'ını geçirecekseniz bir kez deneyip UID'nin değişmediğine bakın.
2. kv put bir anahtarı sildi ve Argo CD girişi bozuldu. Production'da bir kez vault kv put ile mevcut bir path'e anahtar eklemeye çalıştım. put, path'in bütün içeriğini değiştiriyor; aynı path'teki repo erişim token'ını sildi ve Argo CD repoya erişemez oldu. Lab'da bilerek tekrarladım:
$ vault kv put secret/platform/argocd/oidc-zitadel clientSecret="$CS"
version 3
$ kubectl -n argocd get secret argocd-oidc-zitadel -o json | jq -c '.data|keys'
["clientSecret"]
clientID ve cliClientID gitti. Argo CD hata vermedi, sadece bir uyarı yazdı ve referansı olduğu gibi client ID olarak gönderdi:
level=warning msg="secret key does not exist in secret"
level=info msg="Performing authorization_code flow login: http://zitadel.127.0.0.1.nip.io:8081/oauth/v2/authorize?client_id=%24argocd-oidc-zitadel%3AclientID&..."
ZITADEL de bu isimde bir uygulama bulamadı:
HTTP/1.1 400 Bad Request
{"error":"invalid_request","error_description":"Errors.App.NotFound"}
Düzeltmesi tek bir komut, çünkü Vault eski sürümü tutuyor:
$ vault kv rollback -version=2 secret/platform/argocd/oidc-zitadel
version 4
$ kubectl -n argocd annotate es argocd-oidc-zitadel force-sync="$(date +%s)" --overwrite
Rollback eski sürümü silmiyor, onu yeni bir sürüm (4) olarak yazıyor; geçmiş korunuyor. Kural: bir path'i ilk kez oluştururken put, sonrasında her zaman patch. Script'lerde de böyle.
3. Temiz kurulumda bir Application Degraded kalıyor ve bekleme script'i hiç bitmiyor. make vault 13 Application'ın yeşile dönmesini bekliyordu ve 10 dakika sonra hâlâ bekliyordu. argocd-secrets Degraded'dı, çünkü OIDC client'ı ZITADEL'de make setup ile oluşuyor ve Vault'taki path o zamana kadar yok. ESO'nun mesajı: SecretSyncedError. Production'da bunun daha kötüsünü yaşadım: Vault'ta eksik tek bir anahtar, erken bir sync wave'deki bir ExternalSecret'ı Degraded bıraktı ve sonraki wave'lerin hiçbiri uygulanmadı. Lab'da wait-apps.sh'a SKIP= ekledim ve make vault argocd-secrets'ı beklemiyor. Production için ders: kuruluma başlamadan önce Vault'ta olması gereken anahtarların listesini çıkarın.
4. vault status sealed iken hata kodu dönüyor. Init script'inin ilk hali ilk satırda düştü:
command terminated with exit code 2
vault status sealed bir Vault için exit code 2 dönüyor. Script set -e ile çalıştığı için bunu hata sayıp duruyordu. Oysa "sealed" burada sorulan sorunun cevabı. Script artık durumu JSON olarak okuyor ve exit code'u yok sayıyor.
5. Role'de audience yoktu. vault write auth/kubernetes/role/eso başarılı oldu, ama bir uyarı yazdı:
WARNING! The following warnings were returned from Vault:
* Role eso does not have an audience configured.
Audience olmadan Vault, aynı cluster'da herhangi bir amaçla üretilmiş bir ServiceAccount token'ını kabul ediyor. Role'e audience=vault, store'a audiences: [vault] ekledim; ESO artık sadece Vault için üretilmiş token'la giriyor.
6. Rotasyonlar arka arkaya çalışınca ikincisi düştü. make rotate-db hemen ardından make rotate-oidc:
make: *** [rotate-oidc] Error 52
curl exit code 52 boş cevap demek. kubectl port-forward bir Service'e değil, o Service'in arkasındaki tek bir pod'a bağlanıyor. rotate-db ZITADEL'i yeniden başlatınca port-forward'un bağlı olduğu pod gitti; make zitadel-ui döngüsünün yeniden bağlanması bir-iki saniye sürüyor. rotate-oidc-secret.sh artık önce /debug/ready'yi bekliyor. Bu, 05'teki port-forward hatasının (#5) başka bir hali.
Doğrulama
make status # 13 Application Synced/Healthy, Sealed false, 4 ExternalSecret SecretSynced
kubectl -n zitadel get secret zitadel-db -o jsonpath='{.metadata.ownerReferences[0].kind}' # ExternalSecret
git show origin/blog-05b:zitadel/db/ 2>/dev/null | grep secrets.yaml || echo "no secrets.yaml on blog-05b"
make rotate-db # "rotated; Vault version: N", ZITADEL ayakta
make rotate-oidc # sonra Argo CD'ye ZITADEL ile giriş
Eski parolanın çalışmadığını görmek için Adım 4'teki psql komutu: FATAL: password authentication failed.
UI'da:
- Argo CD: 13 Application, hepsi
blog-05büzerinde Synced/Healthy. zitadel-dbveargocd-secretsağaçlarında her ExternalSecret'ın altında bir Secret.- Vault: her rotasyonda
Version History'ye bir satır ekleniyor. - alice / dave / bob ile giriş: 05'teki yetkiler aynen duruyor (sync
product-helloapi-dev: 200 / 200 / 403).

Kaynaklar: hashicorp/vault chart 0.34.1 values (server.ha.raft, injector), Vault dokümanları Kubernetes auth method ve KV v2 (kv patch, kv rollback), External Secrets dokümanları HashiCorp Vault provider, ExternalSecret (refreshInterval, target.template, dataFrom.extract) ve force-sync annotation'ı, Argo CD dokümanları Sync Options (Prune=false, SkipDryRunOnMissingResource, ServerSideApply) ve User Management ($secret:key referansı ve part-of label'ı), postgres Docker imajının POSTGRES_PASSWORD notu.
Bu yazıdan sonra ne değişti
| Önce | Sonra |
|---|---|
| ZITADEL'in üç Secret'ı Git'te base64 | Git'te sadece ExternalSecret: hangi Vault path'inden, hangi anahtar |
| Argo CD'nin OIDC secret'ı bir script'in yazdığı Secret'ta | Vault'ta, sürüm geçmişiyle; ESO label'ıyla birlikte yazıyor |
| Git geçmişindeki Postgres parolası geçerli | FATAL: password authentication failed |
| Rotasyon = commit + PR | Rotasyon = make rotate-db (3,3 sn, 180 istekte 0 hata) / make rotate-oidc (0,7 sn) |
| Yanlış bir değer = Git'te revert | Yanlış bir değer = vault kv rollback |
| Masterkey Git'te | Vault'ta, aynı değerle. Git geçmişindeki hâli hâlâ geçerli |
| — | Vault her restart'ta sealed açılıyor; unseal bir insanın işi |
Bu seride
Önceki: Argo CD SSO Entegrasyonu: ZITADEL ile OIDC ve RBAC · Sonraki: Terraform ile Hetzner'da Talos: 17 Node, Sıfır SSH
Bu yazının kodu: https://github.com/miraccan00/blog-wiki/tree/main/vault-eso-secret-migration