Przed Tobą opowieść o tym, jak wdrożyliśmy 3-węzłowy klaster HashiCorp Vault z High Availability przy użyciu nowoczesnych praktyk Infrastructure as Code. Artykuł zawiera konkretne przykłady z naszych repozytoriów, architekturę systemu oraz praktyczne learningi z procesu.
Note
Jeśli przed Tobą pierwszy raz słyszysz o Vaulcie — jest to narzędzie do zarządzania sekretami i szyfrowaniem danych .
Zamiast przechowywać hasła, klucze API czy certyfikaty w plikach konfiguracyjnych (co jest bardzo niebezpieczne), wszystkie tajne dane przechowujesz w scentralizowanym, bezpiecznym sejfie.
Secrets Management : Przechowywanie i rotacja haseł, API keys, kluczy SSH
Encryption as a Service : Szyfrowanie i deszyfrowanie danych na żądanie
Certificate Management : Automatyczne generowanie i zarządzanie certyfikatami TLS
Access Control : Granularne uprawnienia dzięki ACL politykom
Audit Logging : Pełna historia dostępu do sekretów
W środowisku produkcyjnym nie możesz sobie pozwolić na to, by Vault był Single Point of Failure. Dlatego właśnie tworzymy klaster z High Availability :
Redundancja : Jeśli jeden węzeł padnie, pozostałe 2 pracują dalej
Load Balancing : Rozkład ruchu między węzły
Failover : Automatyczne przełączanie w przypadku awarii
Distributed Storage : Dane replikowane na wszystkich węzłach
⚙️ ansible
Automatyzacja provisioning’u klastra Vault
Playbooki Ansible do instalacji i konfiguracji:
Vault (3-węzłowy klaster HA)
Consul (storage backend)
Keepalived + HAProxy (HA proxy layer)
🔗 Odwiedź
🏗️ iac-vault
Infrastructure as Code dla Vault (OpenTofu)
Zarządzanie:
Secret Engines (KV v2)
Auth Methods (userpass, AppRole, JWT)
ACL Policies (26 polityk)
PKI Hierarchy (Root + Intermediate CA)
🔗 Odwiedź
Utworzenie 3 maszyn wirtualnych
Ustawienie im adresów DNS i IP (w tym VIP)
Utworzenie repozytoriów
Utworzenie palybooka Ansible instalującego cluster VAULT
Utworzenie IaC za pomocą OpenTofu do zarządzania Vaultem
Zanim przejdziemy do kodu, pokażmy jak wygląda architektura:
Komponenta Rola Port Ilość Keepalived Virtual IP (VRRP) 112 3 instancje HAProxy TLS termination + SNI 443 3 instancje Vault Secrets engine 8200 3 węzły (1 leader + 2 standby) Consul Storage backend (HA) 8501 3 węzły
Pierwsza faza naszego projektu infrastrukturalnego polegała na przygotowaniu procesu automatyzacji za pomocą Ansible . Z uwagi na to, że to jest pierwszy projekt Ansible w organizacji, musieliśmy od podstaw opracować zestaw roli Ansible, które systematycznie hartują wirtualne maszyny zgodnie z najlepszymi praktykami bezpieczeństwa.
Stworzyliśmy modularną kolekcję ról, z których każda odpowiada za konkretny aspekt konfiguracji systemu. Podejście to zapewnia czytelność, wielokrotnego użytku oraz łatwość utrzymania kodu infrastrukturalnej.
set-timezone – ustawienie strefy czasowej zgodnie ze standardami naszego środowiska
set-hostname – konfiguracja nazwy hosta maszyny
users-management – zarządzanie kontami użytkowników, tworzenie dedykowanych kont serwisowych
sudo – ustalenie polityki uprawnień i reguł eskalacji privilegiów
ssh-hardening – zastosowanie restrykcyjnych polityk SSH, w tym wyłączenie logowania z użytkownikiem root, zmiana portów domyślnych oraz ograniczenie metod autentykacji
install-packages – instalacja niezbędnych pakietów oraz narzędzi diagnostycznych, uwzględniająca tylko te zależności, które faktycznie wykorzystujemy
install-keepalived-vip – konfiguracja wirtualnego IP (VIP) z użyciem keepalived, umożliwiająca failover w klastrach
haproxy – wdrażanie load balancera HAProxy do dystrybucji ruchu między węzłami
certificates – zarządzanie certyfikatami SSL/TLS, krityczne dla bezpiecznej komunikacji
Każda rola została zwersjpnowana i hostowana w dedykowanym repozytorium GitLab, co umożliwia niezależne iteracje, testowanie oraz utrzymanie. Ten systematyczny podejścia pozwolił nam wybudować powtarzalny i wiarygodny proces przygotowania maszyn od zera.
Konfiguracja nie jest hardkodowana w playbooku — wszystko przechowujemy w YAML:
# inventory/group_vars/vault/vault.yml
inv_group_vault_address : "https://vault.rachuna-net.pl"
inv_vault_tls_disabled : false
inv_vault_disable_mlock : true # Wymagane w homelab/VM
inv_consul_config :
address : "localhost:8501"
token : "{{ token_from_vault }}"
scheme : https
# inventory/group_vars/vault/consul.yml
inv_group_consul_config :
datacenter : "vault"
bootstrap_expect : 3 # WAŻNE: oczekujemy 3 węzłów
ui : true
acl :
enabled : true
# inventory/group_vars/vault/keepalived.yml
inv_group_keepalived_config :
vrrp :
virtual_router_id : 53
interface : eth0
virtual_ip : 10.3.1.253/24
priority :
vault-1022 : 200 # Primary
vault-1023 : 120 # Secondary
vault-1024 : 110 # Backup
Playbook install.yml aplikuje role w konkretnej kolejności :
# playbooks/install.yml
---
- name : Provision Vault HA Cluster
hosts : vault
become : true
roles :
- set-timezone # 1. Lokalizacja
- users-management # 2. Użytkownicy + SSH keys
- set-hostname # 3. FQDN (np. vault-1022.rachuna-net.pl)
- install-packages # 4. Systemy pakiety (curl, openssl, etc)
- install-keepalived-vip # 5. VRRP Virtual IP
- certificates # 6. TLS z Vault PKI (bootstrap)
- haproxy # 7. Load balancer
- install-consul # 8. Consul storage backend
- install-vault # 9. Vault daemon
- vault-auto-unseal # 10. Automatyczne unsealing
Dlaczego ta konkretna kolejność?
Musimy najpierw ustawić hostname , bo Consul i Vault ich używają
TLS certificates musimy pobrać zanim skonfigurujemy HAProxy (który ich potrzebuje)
Consul musi być uruchomiony zanim Vault (bo Vault go używa jako storage)
vault-auto-unseal (opcjonalnie) jest ostatnią rolą (timer, który będzie automatycznie Vault’a odblokowywać)
# Przygotowanie
source .envrc # Załaduj unseal keys
ansible-galaxy install -r requirements.yml # Pobierz external role
# Dry run (bez zmian)
ansible-playbook -i inventory/hosts.yml playbooks/install.yml --check
# Pełna provisioning
ansible-playbook -i inventory/hosts.yml playbooks/install.yml -v
# Obserwuj logi w czasie rzeczywistym
ansible vault -m shell -a "journalctl -f -u vault"
Co się dzieje?
Ansible łączy się do każdego węzła (vault-1022, 1023, 1024) via SSH
Na każdym węźle uruchamia role w kolejności
Role instalują pakiety, konfigurują usługi, startują demony
Po ~5-10 minutach masz gotowy 3-węzłowy klaster
Po provisioning’u infrastruktury mamy gotowy klaster Vault, ale jest on pusty — brak polityk ACL, brak auth methods, brak sekretów.
iac-vault/
├── main.tf.json # Root module (orkestruje wszystko)
├── policies/ # ACL policies (26 plików)
├── auth/ # Auth methods (userpass, approle, jwt)
├── users/ # User accounts
├── kv/ # Secrets KV v2
├── pki/ # PKI CA + intermediate certificates
├── approles/ # AppRole definitions
└── tools/
├── create-user-account.sh # Automated user creation
└── tofu-plan.sh
W naszym projekcie używamy .tf.json zamiast .tf (HCL). Dlaczego?
HCL jest czytelniejszy dla ludzi
JSON jest łatwiejszy do generowania automatycznie
Mniej konfiguracji “magii”, bardziej explicitnie
// main.tf.json
{
"module" : {
"kv" : {
"source" : "./kv/"
},
"users" : {
"source" : "./users/"
},
"auth" : {
"source" : "./auth/"
},
"pki" : {
"source" : "./pki/"
}
}
}
Serio — to wszystko! Każdy moduł zawiera swoją logikę.
Vault jest oparty na principle of least privilege — każdy użytkownik/aplikacja ma dostęp tylko do tego, co potrzebuje.
# policies/admin.tf.json
{
"resource" : {
"vault_policy" : {
"admin" : {
"name" : "admin" ,
"policy" : "path \" * \" { \n capabilities = [ \" create \" , \" read \" , \" update \" , \" delete \" , \" list \" , \" sudo \" ] \n }"
}
}
}
}
# policies/kv_user_read.tf.json
{
"resource" : {
"vault_policy" : {
"kv_user_read" : {
"name" : "kv_user_read" ,
"policy" : "path \" users/data/myuser/* \" { \n capabilities = [ \" read \" , \" list \" ] \n }"
}
}
}
}
Vault wspiera wiele sposobów na uwierzytelnianie:
Userpass (najprostszy):
// auth/userpass.tf.json
{
"resource" : {
"vault_auth_backend" : {
"userpass" : {
"type" : "userpass"
}
}
}
}
AppRole (dla aplikacji/robotów):
// auth/approle.tf.json.txt (disabled for now)
{
"resource" : {
"vault_auth_backend" : {
"approle" : {
"type" : "approle"
}
}
}
}
Aby włączyć AppRole, wystarczy zmienić .tf.json.txt na .tf.json.
Użytkownicy są tworzeni za pomocą modułu vault-userpass-account:
// users/mrachuna.tf.json
{
"module" : {
"mrachuna" : {
"source" : "git::https://gitlab.com/pl.rachuna-net/vault-userpass-account?ref=v1.1.0" ,
"default_password_kv_path" : "users/defaults_passwords" ,
"users" : {
"mrachuna" : {
"policies" : [ "admin" ],
"ttl" : "720h"
}
}
}
}
}
Moduł:
Generuje losowe hasło (24 znaki + znaki specjalne)
Tworzy użytkownika w Vault’a userpass auth method
Przechowuje hasło tymczasowo w KV (do odebrania przez admina)
Tworzy policy do czytania KV użytkownika
Po tofu apply:
# Pobierz wygenerowane hasło
tofu output -raw mrachuna_password
# Daj użytkownikowi — poproś żeby zmienił hasło
vault login -method=userpass username=mrachuna
KV v2 to przechowawka klucz-wartość dla sekretów:
# Struktura (koncepuje z Vaulta)
users/
├── data/mrachuna/ # Sekrety użytkownika mrachuna
├── metadata/mrachuna/ # Metadane (wersje, bity)
└── ...
Konfiguracja w IaC:
// kv/main.tf.json
{
"resource" : {
"vault_mount" : {
"kv" : {
"path" : "users" ,
"type" : "kv" ,
"options" : {
"version" : "2"
}
}
}
}
}
Najfajniejsza część — Vault może być Certificate Authority :
// pki/root.tf.json
{
"resource" : {
"vault_pki_secret_backend" : {
"root" : {
"path" : "pki_root" ,
"max_lease_ttl_seconds" : 315360000
}
},
"vault_pki_secret_backend_root_cert" : {
"root" : {
"backend" : "pki_root" ,
"type" : "internal" ,
"common_name" : "rachuna-net.pl CA" ,
"ttl" : "87600h"
}
}
}
}
// pki/intermediate.tf.json
{
"resource" : {
"vault_pki_secret_backend" : {
"intermediate" : {
"path" : "pki_int_prod" ,
"max_lease_ttl_seconds" : 31536000
}
}
}
}
Rezultat : Ansible role certificates może automatycznie pobierać certyfikaty TLS dla vault.rachuna-net.pl od Vault’a:
# Ansible pobiera:
curl -s -H "X-Vault-Token: $VAULT_TOKEN " \
https://vault.rachuna-net.pl:8200/v1/pki_int_prod/issue/vault-domain \
-d @request.json | jq '.data.certificate' > /opt/vault/tls/vault.crt
# 1. Automated tool
export GITLAB_TOKEN = "glpat-..."
./tools/create-user-account.sh john_doe
# Tool:
# - Tworzy branch: feat/user-john_doe
# - Tworzy plik: users/john_doe.tf.json
# - Commits + pushuje
# - Tworzy MR
# 2. Review MR w GitLab
# 3. Merge
# 4. CI/CD aplikuje w Vaulcie
# (GitLab CI automatycznie uruchamia: tofu apply)
# 5. Admin pobiera hasło
tofu output -raw john_doe_password
# 6. Daj hasło użytkownikowi
# 7. Użytkownik się loguje i zmienia hasło
vault login -method=userpass username=john_doe
vault auth list
# 8. Usuń temporary password
vault kv delete users/defaults_passwords/john_doe
OpenTofu musi gdzieś przechowywać state (mapę między plikami a zasobami w Vaulcie).
Używamy GitLab HTTP backend :
# tofu init
tofu init -backend-config= "address=https://gitlab.com/api/v4/projects/78249750/terraform/state/production"
State jest przechowywany w GitLab’a, a nie jest przechowywany lokalnie.
Jeśli chcesz go pogłębiać — polecam:
Maciej Rachuna | rachuna-net.pl
Artykuł bazuje na realnym wdrożeniu w środowisku homelab. Kod, learnings, i best practices pochodzą z praktyki, a nie z teorii. Dokumentacja znajduje się tutaj