diff --git a/README.md b/README.md index 7c0ee85..21a136e 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,96 @@ -# ADIM 1 — Placeholder secrets oluştur (manager node) +# Vault HA Cluster Bootstrap (Shamir & Docker Secret) + +Bu dizin, Docker Swarm üzerinde Shamir mühürleme (seal) ve Docker secret'ları kullanarak yüksek kullanılabilirliğe (HA) sahip bir Vault kümesinin kurulumunu ve yönetimini içerir. Mevcut yapı, otomatik unseal işlemi için ayrı bir transit vault ihtiyacını ortadan kaldırarak Shamir anahtarını güvenli bir Docker secret'ı olarak kullanır. + +--- + +## 🏗️ Mimari Yapı + +- **Düğümler:** 3 replika (`vault-1`, `vault-2`, `vault-3`) +- **Depolama:** Raft entegre depolama +- **Unseal:** `vault_unseal_key` isimli Docker secret'ı +- **Otomasyon:** `vault-bootstrap.sh` betiği üzerinden uçtan uca kurulum + +--- + +## 🚀 Hızlı Başlangıç (Bootstrap) + +Tüm küme kurulumunu, initialize işlemini ve unseal yapılandırmasını tek bir komutla gerçekleştirmek için: ```bash -# opsiyonel history reset -history -w && > ~/.bash_history && history -c - -echo "bootstrap" | docker secret create vault_transit_unseal_key - -echo "bootstrap" | docker secret create transit_master_token - +chmod +x vault-bootstrap.sh +./vault-bootstrap.sh ``` +Bu betik sırasıyla şu adımları izler: -# ADIM 2 — Stack deploy et +### ADIM 0 — Ön Koşullar +- Swarm manager node kontrolü. +- `docker-stack-vault.yml` dosyasının varlığı. +### ADIM 1 — Yer Tutucu Secret +- `vault_unseal_key` için geçici bir "bootstrap" secret'ı oluşturur. Bu, servislerin çökmeden başlamasını sağlar. + +### ADIM 2 — Stack Deploy +- `iklimco` stack'ini ilgili konfigürasyonla yayına alır. + +### ADIM 3 — Servis Bekleme +- Vault servisinin 3/3 replika olarak ayağa kalkmasını bekler. + +### ADIM 4 — Durum Kontrolü +- Eğer küme zaten initialize edilmiş ve unsealed durumdaysa işlemi güvenli bir şekilde sonlandırır. + +### ADIM 5 — Initialize & Key Hazırlığı +- Vault henüz başlatılmadıysa `vault operator init` komutunu çalıştırır. +- Çıktıları `/tmp/vault-bootstrap/main-vault-init.txt` dosyasına kaydeder. + +### ADIM 6 — Secret Güncelleme +- Geçici secret'ı siler ve gerçek Shamir unseal anahtarı ile Docker secret'ını günceller. +- Servislerin bu yeni anahtarla rolling restart yapmasını sağlar. + +### ADIM 7 — Doğrulama +- Tüm düğümlerin (leader ve follower) başarıyla unseal edildiğini ve Raft kümesine katıldığını doğrular. + +--- + +## 📂 Dizin İçeriği + +- `vault-bootstrap.sh`: Küme kurulumunu otomatize eden ana betik. +- `docker-stack-vault.yml`: Vault HA kümesinin Docker Swarm stack tanımı. +- `vault-template-v1.json`: TLS doğrulamasının daha esnek olduğu (`tls_skip_verify: true`) konfigürasyon şablonu. +- `vault-template-v2.json`: Karşılıklı TLS (mTLS) doğrulamasının zorunlu olduğu, daha sıkı güvenlikli konfigürasyon şablonu. +- `failover_scenarios.md`: Hata durumları ve kurtarma senaryoları dökümantasyonu. + +**Neden iki versiyon var?** +* **v1 (İlk Kurulum):** SWAG reverse proxy henüz wildcard sertifikaları üretmeden önce, self-signed (kendinden imzalı) sertifikalarla ilk kurulumu gerçekleştirebilmek için kullanılır. Bu sürümde TLS doğrulaması esnetilmiştir. +* **v2 (Kalıcı Yapı):** Geçerli wildcard sertifikalar üretildikten sonra, düğümler arası iletişimin tam sertifika doğrulaması (mTLS) ile yapıldığı güvenli ve kalıcı sürümdür. Mevcut bootstrap betiği varsayılan olarak `v2` şablonunu kullanır. + +--- + +## ⚠️ Önemli Güvenlik Notları + +1. **Init Dosyası:** `/tmp/vault-bootstrap/main-vault-init.txt` dosyası root token ve unseal anahtarlarını içerir. Kurulum bittikten sonra bu dosyayı güvenli bir yere (Vault'un kendisi dışındaki bir şifre yöneticisine) yedekleyin ve **Host Makine** üzerinden silin! +2. **Secret Yönetimi:** `vault_unseal_key` secret'ına sadece vault servisinin erişimi vardır. Bu anahtarı Swarm dışında paylaşmayın. +3. **SSL Sertifikaları:** Host makinelerde `/opt/iklimco/ssl/` dizininde gerekli wildcard sertifikaların mevcut olduğundan emin olun. + +--- + +## 🛠️ Manuel Müdahaleler + +Küme normal şartlarda kendi kendini iyileştirme (self-healing) yeteneğine sahiptir. Ancak yeni bir host makine eklendiğinde etiketleme yapmanız gerekir. + +**Düğüm kimliğini (Node ID) bulmak için:** ```bash -docker node update --label-add vault_transit=true iklim-app-03 -docker stack deploy --with-registry-auth -c docker-stack-vault.yml iklimco +docker node ls ``` -Ana vault node'ları transit henüz hazır olmadığı için crash loop'a girer — beklenen durum. - -# ADIM 3 — Transit vault'u initialize et - +**Etiketleme örneği (ID veya Hostname ile):** ```bash -# Transit'in hangi node'da çalıştığını bul: -docker service ps iklimco_vault-transit +# Node ID kullanarak +docker node update --label-add type=service 0z2qov6x078hghu88v0p7z6e7 -# O node'a SSH'la, sonra: -docker exec -it $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault operator init -key-shares=1 -key-threshold=1' - -# Unseal Key 1 ve Initial Root Token'ı kaydet. - -# Unseal Key 1: ........ -# -# Initial Root Token: hvs.xxxxxxxxxx -# -# Vault initialized with 1 key shares and a key threshold of 1. Please securely -# distribute the key shares printed above. When the Vault is re-sealed, -# restarted, or stopped, you must supply at least 1 of these keys to unseal it -# before it can start servicing requests. -# -# Vault does not store the generated root key. Without at least 1 keys to -# reconstruct the root key, Vault will remain permanently sealed! -# -# It is possible to generate new unseal keys, provided you have a quorum of -# existing unseal keys shares. See "vault operator rekey" for more information. +# Veya Hostname kullanarak +docker node update --label-add type=service iklim-app-02 ``` -Unseal Key 1: cS0HPNVl8/9r42SXxeq9Y4uokJP886UAeRQ/sBsBFnQ= -Initial Root Token: hvs.AReLHEa44pztSLBUqW2djdEv -# ADIM 4 — Transit'i manuel unseal et (sadece bu seferlik) - -```bash -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal UNSEAL_KEY_1' -``` - -| Key | Value | -| ------------ | ------------------------------------ | -| Seal Type | shamir | -| Initialized | true | -| Sealed | false | -| Total Shares | 1 | -| Threshold | 1 | -| Version | 2.0.1 | -| Build Date | 2026-05-19T17:20:48Z | -| Storage Type | file | -| Cluster Name | vault-cluster-5bd8a332 | -| Cluster ID | b03a2f93-53b0-d32b-9762-c36a9d45df90 | -| HA Enabled | false | -# ADIM 5 — Transit engine kur - -```bash -# Policy dosyasını host'ta oluştur, container'a kopyala: -cat > /tmp/autounseal-policy.hcl << 'EOF' -path "transit/encrypt/autounseal" { capabilities = ["update"] } -path "transit/decrypt/autounseal" { capabilities = ["update"] } -EOF - -docker cp /tmp/autounseal-policy.hcl \ - $(docker ps -q -f name=iklimco_vault-transit):/tmp/ -# Successfully copied 128B (transferred 2.05kB) to 61a136a1c04e:/tmp/ - -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault login ROOT_TOKEN' -``` -Success! You are now authenticated. The token information displayed below -is already stored in the token helper. You do NOT need to run "vault login" -again. Future Vault requests will automatically use this token. - -(token -> root token) - -| Key | Value | -| ----------------- | ---------------------------- | -| token | hvs.AReLHEa44pztSLBUqW2djdEv | -| token_accessor | 6w5ZKxbSSP3S5kz4D6luAmjv | -| token_duration | ∞ | -| token_renewable | false | -| token_policies | ["root"] | -| identity_policies | [] | -| policies | ["root"] | - - -```bash -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault secrets enable transit' -# Success! Enabled the transit secrets engine at: transit/ - -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault write -f transit/keys/autounseal' -``` - -| Key | Value | -| ---------------------- | ----------------- | -| allow_plaintext_backup | false | -| auto_rotate_period | 0s | -| deletion_allowed | false | -| derived | false | -| exportable | false | -| imported_key | false | -| keys | map[1:1779831017] | -| latest_version | 1 | -| min_available_version | 0 | -| min_decryption_version | 1 | -| min_encryption_version | 0 | -| name | autounseal | -| supports_decryption | true | -| supports_derivation | true | -| supports_encryption | true | -| supports_signing | false | -| type | aes256-gcm96 | - -```bash -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault policy write autounseal /tmp/autounseal-policy.hcl' -# Success! Uploaded policy: autounseal - -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault token create -policy=autounseal -period=768h -orphan' -``` - -(token -> auto unseal token) - -| Key | Value | -| ----------------- | ----------------------------------------------------------------------------------------------- | -| token | hvs.CAESIFqiceeloWSqHszPL8OY9PCFKpNQsh6NXoBxw_Us0w7gGh4KHGh2cy5XWTBXekE1VUNQcGhmNlE4U1F1RVhWOFo | -| token_accessor | mRgwI0az8UZguETf5iqJWXhb | -| token_duration | 768h | -| token_renewable | true | -| token_policies | ["autounseal" "default"] | -| identity_policies | [] | -| policies | ["autounseal" "default"] | -# ADIM 6 — Secrets'ı gerçek değerlerle güncelle (manager node'a dön) - -```bash -# 6a. Transit unseal key — sırayla: servis'ten çıkar, sil, gerçek değerle oluştur, ekle -docker service update --secret-rm vault_transit_unseal_key iklimco_vault-transit -# iklimco_vault-transit -# overall progress: 1 out of 1 tasks -# 1/1: running [==================================================>] -# verify: Service iklimco_vault-transit converged - -docker secret rm vault_transit_unseal_key -# vault_transit_unseal_key - -echo "UNSEAL_KEY_1" | docker secret create vault_transit_unseal_key - -docker service update --secret-add vault_transit_unseal_key iklimco_vault-transit -# iklimco_vault-transit -# overall progress: 1 out of 1 tasks -# 1/1: running [==================================================>] -# verify: Service iklimco_vault-transit converged - -# 6b. Transit'in unsealed olduğunu doğrula (iklim-app-03'te) -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault status' -# Sealed: false olmalı. Eğer hâlâ sealed ise manuel unseal et: -docker exec $(docker ps -q -f name=iklimco_vault-transit) \ - sh -c 'VAULT_ADDR=http://127.0.0.1:8200 vault operator unseal UNSEAL_KEY_1' - -# 6c. Autounseal token — ATOMIC SWAP (vault hiç token'sız restart olmaz) -# DIKKAT: --secret-rm ve --secret-add AYNI komutta verilmeli -echo "hvs.AUTOUNSEAL_TOKEN" | docker secret create transit_master_token_v2 - -docker service update \ - --secret-rm transit_master_token \ - --secret-add source=transit_master_token_v2,target=transit_master_token \ - iklimco_vault -``` - -# ADIM 7 — Ana vault cluster'ı initialize et - -```bash -# Transit açıldıktan ve vault node'ları stable olduktan sonra (~1-2 dk): - -docker service ps iklimco_vault # vault.1'in hangi node'da olduğunu bul -# O node'a SSH'la, sonra: -docker exec $(docker ps -q -f name=iklimco_vault.1) vault operator init - -# Recovery Keys ve Root Token'ı kaydet. Bitti. -``` +Detaylı hata kurtarma senaryoları için `failover_scenarios.md` dökümanını inceleyin. diff --git a/failover_scenarios.md b/failover_scenarios.md new file mode 100644 index 0000000..438134a --- /dev/null +++ b/failover_scenarios.md @@ -0,0 +1,167 @@ +# Vault HA Failover Senaryoları + +Bu belge, 3 düğümlü Vault HA Raft kümesinin Docker Swarm üzerindeki çeşitli host makine hatası ve kurtarma koşulları altında nasıl davrandığını ve her durumda manuel müdahale gerekip gerekmediğini kapsamaktadır. + +--- + +## Mimari Özeti + +| Özellik | Değer | +|---|---| +| Vault düğümleri | 3 replika (`vault-1`, `vault-2`, `vault-3`) | +| Raft çoğunluğu (quorum) | 3 düğümden 2'si gereklidir | +| Düğüm kimliği | `hostname: "vault-{{.Task.Slot}}.iklim.co"` — yeniden başlatmalarda sabittir | +| Unseal mekanizması | Shamir anahtarı, Docker Swarm secret'ı `vault_unseal_key` olarak saklanır | +| Entrypoint unseal döngüsü | Başlangıçtan sonra 3 dakikaya kadar her 2 saniyede bir `vault operator unseal` komutunu dener | +| Yerleşim (Placement) | `max_replicas_per_node: 1`, `node.labels.type == service` | + +`hostname` ayarı kritik parçadır: Swarm, rastgele bir konteyner kimliği yerine **sabit bir slot numarası** (1, 2, 3) atadığı için, Raft `node_id` her yeniden başlatmada aynı kalır. Raft, geri dönen bir düğümü `node_id` üzerinden hemen tanır ve onu yepyeni bir eş (peer) olarak değerlendirmek yerine replikasyona kaldığı yerden devam eder. + +--- + +## Raft Çoğunluk (Quorum) Kuralları + +| Hayattaki düğümler | Çoğunluk | Küme durumu | +|---|---|---| +| 3 / 3 | Evet | Tamamen operasyonel | +| 2 / 3 | Evet | Operasyonel — okuma ve yazma işlemleri devam eder | +| 1 / 3 | **Hayır** | Donmuş — çoğunluk sağlanana kadar okuma ve yazma işlemleri engellenir | +| 0 / 3 | **Hayır** | Kapalı | + +Küme, herhangi bir hizmet kesintisi olmadan **aynı anda bir düğüm hatasını** tolere edebilir. + +--- + +## Senaryo 1 — Takipçi (Follower) Host Makine Çöküyor, Volume Sağlam + +**Tetikleyici**: `vault-N` (bir takipçi) çalıştıran host makine çöker veya kapatılır. Vault volume'u host makinenin diskinde hayatta kalır. + +**Otomatik davranış**: +1. Docker Swarm görevi (task) başarısız olarak işaretler. `max_replicas_per_node: 1` ayarı uygun başka bir host makine bırakmadığı için, Swarm replikayı başka bir yerde yeniden zamanlamaz. Hizmet 2/3 görevle çalışmaya devam eder. +2. Kalan 2 düğüm (1 lider + 1 takipçi) çoğunluğu korur. Küme, isteklere normal şekilde hizmet vermeye devam eder. +3. Host makine geri gelip Swarm'a tekrar katıldığında, Swarm görevi hemen üzerinde zamanlar. +4. Konteyner başlar, Raft verilerini sağlam bulur, lider ile `retry_join` üzerinden bağlantı kurar ve saklanan log indeksinden devam eder. +5. Entrypoint unseal döngüsü `/run/secrets/vault_unseal_key` dosyasını okur ve düğümü otomatik olarak unseal eder. +6. Düğüm saniyeler içinde `standby` (takipçi) durumuna geçer. + +**Manuel müdahale**: Gerekli değildir. + +--- + +## Senaryo 2 — Takipçi Host Makine Çöküyor, Volume Kayıp (Host Makine Yeniden Kuruldu) + +**Tetikleyici**: `vault-N` çalıştıran host makine tamamen değiştirilir veya Docker volume'ları silinir (`rm -rf /var/lib/docker/volumes/iklimco_vault-data-vl/_data/*`). + +> Bu senaryo testlerde doğrulandı: o sırada Raft lideri olan vault-3'ün volume'u silindi. Tam kurtarma yaklaşık 5 saniye sürdü. + +**Otomatik davranış**: +1. Swarm, görevi (şimdi temiz olan) host makine üzerinde zamanlar. +2. Konteyner boş bir veri dizini ile başlar (`security barrier not initialized`). +3. `retry_join`, `vault.iklim.co:8200` (paylaşılan overlay takma adı) ile iletişime geçer. Başlangıçta sealed bir düğüme denk gelirse otomatik olarak tekrar dener. +4. Başarılı bir katılımda lider, tam Raft günlüğünü yeni düğüme aktarır (`previous-index= → last-index=1`). +5. `Initialized: true` olduğunda, entrypoint unseal döngüsü düğümü başarıyla unseal eder. +6. Düğüm, Raft seçim sonucuna bağlı olarak `standby` (takipçi) veya `active` (lider) durumuna geçer. + +**Manuel müdahale**: Gerekli değildir. + +**Yeniden kurulan host makinede ön koşul**: +- Konteyner başlamadan önce wildcard sertifika dosyaları `/opt/iklimco/ssl/` dizininde mevcut olmalıdır. Bunlar konteynere salt okunur (read-only) olarak bağlanır. Eğer host makine sıfırdan kurulduysa, Swarm'ı tekrar ayağa kaldırmadan önce sertifikaları geri yükleyin. + +--- + +## Senaryo 3 — Lider (Leader) Host Makine Çöküyor, Volume Sağlam + +**Tetikleyici**: Mevcut Raft liderini çalıştıran host makine çöker veya kapatılır. Volume hayatta kalır. + +**Otomatik davranış**: +1. Hayatta kalan iki takipçi, Raft kalp atışı zaman aşımı (~5–10 sn) üzerinden lider kaybını tespit eder. Bunlardan biri Raft seçimini kazanır ve yeni lider olur. +2. Küme, yeni lider ile isteklere hizmet vermeye devam eder. Herhangi bir konfigürasyon değişikliği gerekmez — `vault.iklim.co` overlay takma adı, çalışan tüm konteynerlere round-robin (sırayla) dağıtım yapar. +3. Orijinal lider host makine geri geldiğinde, konteyneri yeniden başlar, Raft verilerini bulur, bir takipçi olarak tekrar katılır ve otomatik olarak unseal edilir. + +**Manuel müdahale**: Gerekli değildir. + +--- + +## Senaryo 4 — Lider Host Makine Çöküyor, Volume Kayıp (Host Makine Yeniden Kuruldu) + +Senaryo 2 ile aynıdır ancak lider düğüm üzerinde tetiklenir. Davranış aynıdır: kalan 2 düğüm hemen yeni bir lider seçer ve yeniden kurulan host makine, tam log replikasyonu ile yeni bir takipçi olarak katılır. + +**Manuel müdahale**: Gerekli değildir (Senaryo 2 ile aynı wildcard sertifika ön koşulu geçerlidir). + +--- + +## Senaryo 5 — İki Düğüm Aynı Anda Çöküyor + +**Tetikleyici**: Üç host makineden ikisi aynı anda arızalanır (örneğin iki kabini etkileyen bir güç kesintisi). + +**Otomatik davranış**: +1. Çoğunluk kaybolur (3 düğümden sadece 1'i hayatta). +2. Hayatta kalan düğüm bir lider seçemez ve salt okunur / donmuş duruma geçer. Tüm Vault API çağrıları hata döndürür. +3. Çöken iki host makineden **herhangi biri** geri gelip Vault konteyneri başladığında, çoğunluk geri yüklenir (3 düğümden 2'si). +4. Raft tekrar bir lider seçer. Küme normal operasyona döner. +5. Üçüncü host makine geri döndüğünde, takipçi olarak tekrar katılır. + +**Manuel müdahale**: Gerekli değildir — arızalanan iki düğümden en az birinin en sonunda volume'u sağlam olarak geri dönmesi (veya `retry_join` üzerinden taze bir düğüm olarak tekrar katılması) şartıyla. + +> Eğer her iki arızalı düğümün de volume'u silinmişse ve hiçbiri tekrar katılamıyorsa, hayatta kalan tek düğüm Raft günlüğünün tek kopyasını elinde tutar. Bu uç durumda küme kendi kendini iyileştirecektir: iki taze düğüm katıldığında hayatta kalan liderden tam bir Raft anlık görüntüsü (snapshot) alacaktır. + +--- + +## Senaryo 6 — Üç Düğümün Hepsi Çöküyor + +**Tetikleyici**: Tam veri merkezi kesintisi, üç Swarm worker düğümünün tamamı aynı anda kapanır. + +**Otomatik davranış**: +1. Host makineler geri geldiğinde, Swarm üç Vault görevini de zamanlar. +2. Setiap konteyner başlar ve Raft verilerini sağlam bulur (volume'lar yeniden başlatmalarda korunur). +3. Raft, üç düğüm arasında bir lider seçer (en yüksek Raft `CommitIndex` değerine sahip olan düğüm kazanır). +4. Üç düğümün tamamı entrypoint döngüsü üzerinden otomatik olarak unseal edilir. +5. Küme, herhangi bir insan müdahalesi olmadan tamamen operasyonel hale gelir. + +**Manuel müdahale**: Gerekli değildir (volume'ların host makine yeniden başlatmalarından sağ çıktığı varsayılmaktadır; bu, işletim sistemi yeniden başlatmaları / güç döngüleri için normaldir). + +--- + +## Senaryo 7 — Host Makine Swarm'dan Tamamen Çıkarıldı ve Tekrar Eklendi + +**Tetikleyici**: Bir worker düğümü manuel olarak drain edilir ve Swarm'dan çıkarılır (`docker node rm`), ardından yeni veya yeniden imajlanmış bir host makine `docker swarm join` ile tekrar katılır. + +**Otomatik davranış**: +- Yeni Swarm düğümü, gerekli düğüm etiketine (label) sahip olmadığı için otomatik olarak bir Vault görevi almaz. + +**Manuel müdahale gereklidir**: +```bash +# Yeni düğüm kimliğini (node ID) bulun +docker node ls + +# Swarm'ın vault'u üzerinde zamanlayabilmesi için yerleşim etiketini (placement label) tekrar uygulayın +docker node update --label-add type=service +``` + +Etiket uygulandıktan sonra Swarm, bir sonraki mutabakat döngüsü içinde (genellikle 10 sn içinde) Vault replikasını düğüm üzerinde zamanlar. Bu noktadan itibaren Senaryo 1–4 geçerlidir (otomatik unseal). + +--- + +## Özet Tablosu + +| Senaryo | Çoğunluk kayboldu mu? | Otomatik kurtarma | Manuel adımlar | +|---|---|---|---| +| 1 — Takipçi çöktü, volume sağlam | Hayır | Evet | Yok | +| 2 — Takipçi çöktü, volume silindi | Hayır | Evet | Host makine yeniden kurulduysa `/opt/iklimco/ssl` sertifikalarını geri yükleyin | +| 3 — Lider çöktü, volume sağlam | Hayır (yeni seçim) | Evet | Yok | +| 4 — Lider çöktü, volume silindi | Hayır (yeni seçim) | Evet | Host makine yeniden kurulduysa `/opt/iklimco/ssl` sertifikalarını geri yükleyin | +| 5 — İki düğüm aynı anda çöktü | **Evet** (biri dönene kadar) | Evet | Yok — bir düğümün dönmesini bekleyin | +| 6 — Üç düğümün hepsi çöktü | **Evet** (ikisi dönene kadar) | Evet | Yok | +| 7 — Host makine Swarm'dan çıkarıldı ve tekrar eklendi | Hayır | Kısmi | `docker node update --label-add type=service ` | + +--- + +## Otomatik Kurtarmayı Sağlayan Temel Tasarım Özellikleri + +1. **Sabit `node_id`**: `hostname: "vault-{{.Task.Slot}}.iklim.co"` ayarı, Vault'un Raft `node_id` değerinin konteyner yeniden başlatmalarında asla değişmemesini sağlar. Bu olmasaydı, her yeniden başlatma yeni bir eş kimliği oluştururdu ve küme asla tekrar kurulamazdı. + +2. **Entrypoint unseal döngüsü**: Konteyner, 3 dakikaya kadar her 2 saniyede bir `vault operator unseal` komutunu dener. Bu, taze bir düğümün Raft'a katılana kadar unseal edilemediği "tavuk mu yumurta mı" durumunu kapsar — döngü, Raft katılımı başarılı olana kadar denemeye devam eder. + +3. **`vault_unseal_key` Docker secret'ı**: Unseal anahtarı, konteyner içinde her zaman `/run/secrets/vault_unseal_key` konumunda mevcuttur. Herhangi bir yeniden başlatmadan sonra hiç kimsenin anahtarı yazması gerekmez. + +4. **Paylaşılan takma ad ile `retry_join`**: Tüm düğümler `vault.iklim.co:8200` (hizmet VIP'si / overlay takma adı) adresini işaret eder. Swarm bunu çalışan herhangi bir sağlıklı konteynere yük dengeler (load-balance), böylece yeni bir düğüm, hangi eşin mevcut lider olduğunu bilmesine gerek kalmadan kümeyi her zaman bulabilir. diff --git a/vault-bootstrap.sh b/vault-bootstrap.sh index be0d762..027ac53 100755 --- a/vault-bootstrap.sh +++ b/vault-bootstrap.sh @@ -18,27 +18,27 @@ MAIN_INIT_FILE="$OUT_DIR/main-vault-init.txt" step() { echo; echo "════════════════════════════════════════════════"; echo " [$(date '+%H:%M:%S')] $*"; echo "════════════════════════════════════════════════"; } ok() { echo " [OK] $*"; } info() { echo " --> $*"; } -fail() { echo; echo " [HATA] $*" >&2; exit 1; } -trap 'echo; echo " [HATA] Script satir $LINENO'"'"'de beklenmedik sekilde sonlandi" >&2' ERR +fail() { echo; echo " [ERROR] $*" >&2; exit 1; } +trap 'echo; echo " [ERROR] Script terminated unexpectedly at line $LINENO" >&2' ERR # ───────────────────────────────────────────────────────────────────── # ─── Helpers ───────────────────────────────────────────────────────── wait_service_running() { local svc="$1" expected="$2" timeout="${3:-180}" elapsed=0 - info "Bekleniyor: $svc ($expected running task)..." + info "Waiting for: $svc ($expected running task)..." while [ "$elapsed" -lt "$timeout" ]; do running=$(docker service ps "$svc" \ --filter "desired-state=running" \ --format '{{.CurrentState}}' 2>/dev/null \ | grep -c "^Running" || true) if [ "$running" -ge "$expected" ]; then - ok "$svc hazir: $running/$expected" + ok "$svc ready: $running/$expected" return 0 fi sleep 5; elapsed=$((elapsed+5)) echo " ${elapsed}s/${timeout}s — running: $running/$expected" done - fail "$svc $timeout saniye icinde hazir olmadi" + fail "$svc did not become ready within $timeout seconds" } # Run a vault CLI command — uses docker exec if a vault replica is on this node, @@ -83,94 +83,94 @@ check_cluster_unsealed() { } # ───────────────────────────────────────────────────────────────────── -# ━━━ ADIM 0 — On kosullar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -step "ADIM 0 — On kosullar kontrol ediliyor" -docker node ls &>/dev/null || fail "Swarm manager node gerekli" -[ -f "$STACK_FILE" ] || fail "Stack dosyasi bulunamadi: $STACK_FILE" -ok "On kosullar tamam" +# ━━━ STEP 0 — Prerequisites ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +step "STEP 0 — Checking prerequisites" +docker node ls &>/dev/null || fail "Swarm manager node is required" +[ -f "$STACK_FILE" ] || fail "Stack file not found: $STACK_FILE" +ok "Prerequisites completed" -# ━━━ ADIM 1 — Placeholder secret ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -step "ADIM 1 — vault_unseal_key kontrol ediliyor" +# ━━━ STEP 1 — Placeholder secret ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +step "STEP 1 — Checking vault_unseal_key" if docker secret ls --format '{{.Name}}' | grep -q '^vault_unseal_key'; then - info "vault_unseal_key mevcut, atlaniyor" + info "vault_unseal_key exists, skipping" else echo "bootstrap" | docker secret create vault_unseal_key - >/dev/null - ok "vault_unseal_key (placeholder) olusturuldu" + ok "vault_unseal_key (placeholder) created" fi -# ━━━ ADIM 2 — Stack deploy ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -step "ADIM 2 — Stack deploy" +# ━━━ STEP 2 — Stack deploy ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +step "STEP 2 — Stack deploy" if [ "$SKIP_DEPLOY" = "true" ]; then - info "SKIP_DEPLOY=true — atlaniyor" + info "SKIP_DEPLOY=true — skipping" else docker stack deploy --with-registry-auth -c "$STACK_FILE" "$STACK_NAME" - ok "Stack deploy edildi" + ok "Stack deployed" fi -# ━━━ ADIM 3 — Vault cluster bekleniyor ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -step "ADIM 3 — Vault cluster bekleniyor" +# ━━━ STEP 3 — Waiting for Vault cluster ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +step "STEP 3 — Waiting for Vault cluster" wait_service_running "${STACK_NAME}_vault" 3 300 sleep 10 -# ━━━ ADIM 4 — Vault durum kontrolu (erken cikis) ━━━━━━━━━━━━━━━━━━━ +# ━━━ STEP 4 — Vault status check (early exit) ━━━━━━━━━━━━━━━━━━━ # Early-exit requires the ENTIRE cluster to be unsealed. We fire N requests to # the shared alias (load-balanced) and all must return Sealed: false. A single # healthy node is not sufficient evidence that all 3 nodes are unsealed. -step "ADIM 4 — Vault durum kontrolu" +step "STEP 4 — Vault status check" VAULT_STATUS_OUT=$(run_vault "vault status 2>/dev/null" || true) VAULT_INITIALIZED=$(echo "$VAULT_STATUS_OUT" | awk '/^Initialized/{print $2}') VAULT_SEALED=$(echo "$VAULT_STATUS_OUT" | awk '/^Sealed/{print $2}') info "Initialized: ${VAULT_INITIALIZED:-unknown}, Sealed: ${VAULT_SEALED:-unknown}" if [ "$VAULT_INITIALIZED" = "true" ] && [ "$VAULT_SEALED" = "false" ]; then - info "En az 1 node saglikli — cluster geneli kontrol ediliyor (9 istek)..." + info "At least 1 node healthy — checking cluster-wide (9 requests)..." if check_cluster_unsealed 9; then - ok "Vault cluster tamamen unsealed ve saglikli" + ok "Vault cluster fully unsealed and healthy" echo echo "════════════════════════════════════════════════" - echo " BOOTSTRAP TAMAMLANDI (Vault saglıklı)" + echo " BOOTSTRAP COMPLETED (Vault healthy)" echo "════════════════════════════════════════════════" exit 0 else - info "Bazi node'lar hala sealed — bootstrap devam ediyor..." + info "Some nodes are still sealed — bootstrap continuing..." fi fi -# ━━━ ADIM 5 — Vault initialize (gerekirse) ━━━━━━━━━━━━━━━━━━━━━━━━━ -step "ADIM 5 — Vault initialize / unseal key hazirlaniyor" +# ━━━ STEP 5 — Vault initialize (if needed) ━━━━━━━━━━━━━━━━━━━━━━━━━ +step "STEP 5 — Initializing Vault / preparing unseal key" if [ "$VAULT_INITIALIZED" = "true" ]; then # Vault is sealed but initialized. This happens when the vault_unseal_key Docker secret # contains the wrong value (e.g., placeholder was never replaced). Provide the init file # so the real key can be extracted and pushed to the secret. - info "Vault sealed ama initialize edilmis — mevcut init dosyasi kullanilacak" + info "Vault is sealed but initialized — using existing init file" [ -f "$MAIN_INIT_FILE" ] && grep -q "Unseal Key 1" "$MAIN_INIT_FILE" \ - || fail "Init dosyasi eksik: $MAIN_INIT_FILE\nUnseal Key'i manuel olarak su formatta dosyaya ekleyin:\n Unseal Key 1: " - ok "Init dosyasi mevcut" + || fail "Init file missing: $MAIN_INIT_FILE\nManually add the Unseal Key to the file in this format:\n Unseal Key 1: " + ok "Init file exists" else - info "Vault initialize ediliyor..." + info "Initializing Vault..." run_vault "vault operator init -key-shares=1 -key-threshold=1" | tee "$MAIN_INIT_FILE" - ok "Vault init tamamlandi: $MAIN_INIT_FILE" + ok "Vault init completed: $MAIN_INIT_FILE" fi -# ━━━ ADIM 6 — vault_unseal_key Docker secret guncelle ━━━━━━━━━━━━━━ +# ━━━ STEP 6 — Update vault_unseal_key Docker secret ━━━━━━━━━━━━━━ # Two-step update (delete + recreate with the same name) keeps the secret name # consistent with the stack file so future 'docker stack deploy' runs do not # trigger a service restart or revert to the placeholder. -step "ADIM 6 — vault_unseal_key Docker secret guncelleniyor" +step "STEP 6 — Updating vault_unseal_key Docker secret" UNSEAL_KEY=$(awk '/Unseal Key 1:/{print $NF}' "$MAIN_INIT_FILE") -[ -n "$UNSEAL_KEY" ] || fail "Unseal key '$MAIN_INIT_FILE' dosyasinda bulunamadi" +[ -n "$UNSEAL_KEY" ] || fail "Unseal key not found in '$MAIN_INIT_FILE' file" -info "Eski secret servis uzerinden kaldiriliyor (rolling restart 1/2)..." +info "Removing old secret from service (rolling restart 1/2)..." docker service update --secret-rm vault_unseal_key "${STACK_NAME}_vault" >/dev/null sleep 5 docker secret rm vault_unseal_key || true -info "Gercek unseal key ile secret yeniden olusturuluyor (rolling restart 2/2)..." +info "Recreating secret with real unseal key (rolling restart 2/2)..." echo "$UNSEAL_KEY" | docker secret create vault_unseal_key - >/dev/null docker service update --secret-add vault_unseal_key "${STACK_NAME}_vault" >/dev/null -ok "vault_unseal_key gercek degerle guncellendi" +ok "vault_unseal_key updated with real value" -# ━━━ ADIM 6b — Leader unseal ve peer node'lar ━━━━━━━━━━━━━━━━━━━━━━ +# ━━━ STEP 6b — Leader unseal and peer nodes ━━━━━━━━━━━━━━━━━━━━━━ # After rolling restart: # - The node that ran 'vault operator init' has Raft data; its entrypoint retry # loop will unseal it and it becomes the Raft leader. @@ -182,8 +182,8 @@ ok "vault_unseal_key gercek degerle guncellendi" # hostname). This requires the node_id to be resolvable on the overlay network. # If it is not, the explicit attempt is silently skipped and the entrypoint # retry loop handles it instead (worst case: ~60s extra wait). -step "ADIM 6b — Raft leader bekleniyor ve peer node'lar unsealing" -info "Rolling restart sonrasi Raft leader unseal bekleniyor (max 3 dakika)..." +step "STEP 6b — Waiting for Raft leader and unsealing peer nodes" +info "Waiting for Raft leader unseal after rolling restart (max 3 minutes)..." LEADER_UP=0 for i in $(seq 1 36); do @@ -193,62 +193,62 @@ for i in $(seq 1 36); do LEADER_UP=1 break fi - echo " ${i}/36 — Sealed: ${STATUS}, 5s bekleniyor..." + echo " ${i}/36 — Sealed: ${STATUS}, waiting 5s..." sleep 5 done -[ "$LEADER_UP" -eq 1 ] || fail "Raft leader 3 dakika icinde unseal olmadi" +[ "$LEADER_UP" -eq 1 ] || fail "Raft leader did not unseal within 3 minutes" ROOT_TOKEN=$(awk '/^Initial Root Token:/{print $NF}' "$MAIN_INIT_FILE") -[ -n "$ROOT_TOKEN" ] || fail "Root token '$MAIN_INIT_FILE' dosyasinda bulunamadi" +[ -n "$ROOT_TOKEN" ] || fail "Root token not found in '$MAIN_INIT_FILE' file" VAULT_TOKEN="$ROOT_TOKEN" # Wait for all peers to join the Raft cluster (retry_join retries every ~30s). -info "Raft cluster olusmasi bekleniyor (3 peer, max 3 dakika)..." +info "Waiting for Raft cluster formation (3 peers, max 3 minutes)..." ALL_JOINED=0 for i in $(seq 1 36); do PEER_COUNT=$(run_vault "vault operator raft list-peers 2>/dev/null" \ | awk 'NR>2 && /[a-zA-Z0-9]/{c++} END{print c+0}' || true) if [ "${PEER_COUNT:-0}" -ge 3 ]; then - ok "Raft cluster tam: ${PEER_COUNT}/3 peer" + ok "Raft cluster complete: ${PEER_COUNT}/3 peers" ALL_JOINED=1 break fi - echo " ${i}/36 — Raft peers: ${PEER_COUNT:-0}/3, 5s bekleniyor..." + echo " ${i}/36 — Raft peers: ${PEER_COUNT:-0}/3, waiting 5s..." sleep 5 done -[ "$ALL_JOINED" -eq 1 ] || fail "Raft cluster 3 dakika icinde tam olusmaadi" +[ "$ALL_JOINED" -eq 1 ] || fail "Raft cluster did not form within 3 minutes" # Explicitly unseal each non-leader peer via its node_id on the overlay network. # node_id equals STABLE_ID (the api_addr hostname configured in vault-template-v2.json). # Best-effort: if the hostname is not resolvable, the entrypoint retry loop handles it. -info "Peer node'lar individually unsealing (best-effort)..." +info "Unsealing peer nodes individually (best-effort)..." PEER_HOSTS=$(run_vault "vault operator raft list-peers 2>/dev/null" \ | awk 'NR>2 && /[a-zA-Z0-9]/ && !/leader/{print $1}' || true) for peer_host in $PEER_HOSTS; do info " Unsealing peer: $peer_host" if run_vault_on "$peer_host" "vault operator unseal $UNSEAL_KEY" > /dev/null 2>&1; then - ok " $peer_host: unseal komutu gonderildi" + ok " $peer_host: unseal command sent" else - info " $peer_host: direct unseal basarisiz (overlay DNS resolve edilemedi — entrypoint loop devam ediyor)" + info " $peer_host: direct unseal failed (overlay DNS could not be resolved — entrypoint loop continuing)" fi done -# ━━━ ADIM 7 — Tum node'lar unsealed mi? ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +# ━━━ STEP 7 — Are all nodes unsealed? ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ # Fire 9 requests to the shared alias with 1s sleep between each. With 3 nodes # and any reasonable load-balancing the probability of hitting all 3 is very high. # All 9 must return Sealed: false. We retry for up to 4 minutes to give the # entrypoint retry loop time to finish for nodes that joined Raft late. -step "ADIM 7 — Vault cluster tam unseal dogrulaniyor" -info "Entrypoint retry loop tamamlanmasi bekleniyor (max 4 dakika)..." +step "STEP 7 — Verifying full Vault cluster unseal" +info "Waiting for entrypoint retry loop completion (max 4 minutes)..." UNSEALED=0 for i in $(seq 1 24); do if check_cluster_unsealed 9; then - ok "Vault cluster tamamen unsealed (9/9 kontrol basarili)" + ok "Vault cluster fully unsealed (9/9 checks successful)" UNSEALED=1 break fi - echo " ${i}/24 — Cluster henuz tam saglikli degil, 10s bekleniyor..." + echo " ${i}/24 — Cluster not fully healthy yet, waiting 10s..." # Re-attempt explicit peer unseal on every iteration in case hostname became # resolvable after Raft catch-up (containers may still be starting up). PEER_HOSTS=$(run_vault "vault operator raft list-peers 2>/dev/null" \ @@ -259,12 +259,12 @@ for i in $(seq 1 24); do sleep 10 done -[ "$UNSEALED" -eq 1 ] || fail "Vault cluster unseal olmadi — 'docker service logs ${STACK_NAME}_vault' ile loglari kontrol edin" +[ "$UNSEALED" -eq 1 ] || fail "Vault cluster did not unseal — check logs with 'docker service logs ${STACK_NAME}_vault'" echo echo "════════════════════════════════════════════════" -echo " BOOTSTRAP TAMAMLANDI" -echo " Init cikti: $MAIN_INIT_FILE" -echo " ONEMLI: Bu dosyayi guvenli yere yedekle ve" -echo " produksiyon ortamindan sil!" +echo " BOOTSTRAP COMPLETED" +echo " Init output: $MAIN_INIT_FILE" +echo " IMPORTANT: Back up this file to a safe place and" +echo " delete it from the production environment!" echo "════════════════════════════════════════════════"