From f7471fb10518691e0448563ea9cae1c5620d9fb6 Mon Sep 17 00:00:00 2001 From: hermes-agent Date: Thu, 17 Sep 2026 16:53:30 +0000 Subject: [PATCH 1/3] =?UTF-8?q?feat(security):=20Vault=20standalone+raft?= =?UTF-8?q?=20=D0=BD=D0=B0=20proxmox-csi,=20=D1=87=D0=B0=D1=80=D1=82=20?= =?UTF-8?q?=D0=B8=D0=B7=20git?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kubernetes/apps/security/kustomization.yaml | 11 +++ kubernetes/apps/security/namespace.yaml | 7 ++ .../security/vault/app/gitrepository.yaml | 15 ++++ .../apps/security/vault/app/helmrelease.yaml | 70 +++++++++++++++++++ .../apps/security/vault/app/httproute.yaml | 18 +++++ .../security/vault/app/kustomization.yaml | 7 ++ kubernetes/apps/security/vault/ks.yaml | 24 +++++++ 7 files changed, 152 insertions(+) create mode 100644 kubernetes/apps/security/kustomization.yaml create mode 100644 kubernetes/apps/security/namespace.yaml create mode 100644 kubernetes/apps/security/vault/app/gitrepository.yaml create mode 100644 kubernetes/apps/security/vault/app/helmrelease.yaml create mode 100644 kubernetes/apps/security/vault/app/httproute.yaml create mode 100644 kubernetes/apps/security/vault/app/kustomization.yaml create mode 100644 kubernetes/apps/security/vault/ks.yaml diff --git a/kubernetes/apps/security/kustomization.yaml b/kubernetes/apps/security/kustomization.yaml new file mode 100644 index 0000000..0942720 --- /dev/null +++ b/kubernetes/apps/security/kustomization.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +namespace: security + +components: + - ../../components/sops + +resources: + - ./namespace.yaml + - ./vault/ks.yaml diff --git a/kubernetes/apps/security/namespace.yaml b/kubernetes/apps/security/namespace.yaml new file mode 100644 index 0000000..1d48e62 --- /dev/null +++ b/kubernetes/apps/security/namespace.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: security + annotations: + kustomize.toolkit.fluxcd.io/prune: disabled diff --git a/kubernetes/apps/security/vault/app/gitrepository.yaml b/kubernetes/apps/security/vault/app/gitrepository.yaml new file mode 100644 index 0000000..a787cdb --- /dev/null +++ b/kubernetes/apps/security/vault/app/gitrepository.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: vault-helm +spec: + interval: 12h + # Чарт берём прямо из git, а не из https://helm.releases.hashicorp.com: + # этот Helm-репозиторий отдаётся через CloudFront, который блокирует + # российские адреса (403 Forbidden), и HelmRelease не может получить index.yaml. + url: https://github.com/hashicorp/vault-helm + ref: + # hashicorp/vault-helm: Chart.yaml version 0.34.1 (appVersion Vault 2.0.4), + # чарт лежит в корне репозитория. + tag: v0.34.1 diff --git a/kubernetes/apps/security/vault/app/helmrelease.yaml b/kubernetes/apps/security/vault/app/helmrelease.yaml new file mode 100644 index 0000000..a70dbb9 --- /dev/null +++ b/kubernetes/apps/security/vault/app/helmrelease.yaml @@ -0,0 +1,70 @@ +--- +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: vault +spec: + interval: 1h + chart: + spec: + chart: . + version: 0.34.1 + sourceRef: + kind: GitRepository + name: vault-helm + install: + crds: CreateReplace + upgrade: + crds: CreateReplace + cleanupOnFail: true + remediation: + retries: 3 + # Упавший первый релиз без этой настройки залипает: Flux пытается + # откатиться, предыдущей ревизии нет и HelmRelease навсегда остаётся False. + strategy: uninstall + values: + global: + enabled: true + tlsDisable: true + # authDelegator включает auth-метод kubernetes (service account -> токен Vault): + # через него External Secrets Operator будет читать секреты без хранения + # пароля/токена Vault в sops. + authDelegator: + enabled: true + # Агент-инжектор не используем — секреты раздаёт ESO. + injector: + enabled: false + csi: + enabled: false + server: + # Один под и один PVC на ZFS-зеркале (rpool через StorageClass + # proxmox-local-zfs) с интегрированным raft-хранилищем — single-node Vault. + standalone: + enabled: true + config: | + ui = true + + listener "tcp" { + tls_disable = 1 + address = "[::]:8200" + cluster_address = "[::]:8201" + } + + storage "raft" { + path = "/vault/data" + } + + service_registration "kubernetes" {} + dataStorage: + enabled: true + size: 5Gi + storageClass: proxmox-local-zfs + accessMode: ReadWriteOnce + service: + enabled: true + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + memory: 1Gi diff --git a/kubernetes/apps/security/vault/app/httproute.yaml b/kubernetes/apps/security/vault/app/httproute.yaml new file mode 100644 index 0000000..8d25274 --- /dev/null +++ b/kubernetes/apps/security/vault/app/httproute.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: vault +spec: + parentRefs: + - name: envoy-internal + namespace: network + # https-листенер шлюза принимает маршруты из любых namespace, а http — только + # из network, поэтому цепляемся именно к https. + sectionName: https + hostnames: + - "vault.${SECRET_DOMAIN}" + rules: + - backendRefs: + - name: vault + port: 8200 diff --git a/kubernetes/apps/security/vault/app/kustomization.yaml b/kubernetes/apps/security/vault/app/kustomization.yaml new file mode 100644 index 0000000..c95c4c2 --- /dev/null +++ b/kubernetes/apps/security/vault/app/kustomization.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - ./gitrepository.yaml + - ./helmrelease.yaml + - ./httproute.yaml diff --git a/kubernetes/apps/security/vault/ks.yaml b/kubernetes/apps/security/vault/ks.yaml new file mode 100644 index 0000000..c7bb6b0 --- /dev/null +++ b/kubernetes/apps/security/vault/ks.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: vault +spec: + interval: 1h + path: ./kubernetes/apps/security/vault/app + postBuild: + substituteFrom: + - name: cluster-secrets + kind: Secret + prune: true + sourceRef: + kind: GitRepository + name: flux-system + namespace: flux-system + targetNamespace: security + # wait выключен сознательно: PVC у Vault создаётся дефолтным StorageClass'ом из + # proxmox-csi, а готовность пода Vault наступает только после ручного + # `vault operator init` + unseal (readinessProbe `vault status` возвращает exit 2, + # пока хранилище запечатано). С wait: true Kustomization висела бы красной всё + # это время, хотя чарт установлен корректно. + wait: false -- 2.54.0 From 372e26ece82f211ae1585d7b2e13f1c0f15f0387 Mon Sep 17 00:00:00 2001 From: hermes-agent Date: Thu, 17 Sep 2026 17:17:29 +0000 Subject: [PATCH 2/3] =?UTF-8?q?feat(vault):=20just=20vault=20unseal/status?= =?UTF-8?q?=20=E2=80=94=20=D0=B4=D0=BE=D0=BB=D0=B8=20=D0=B8=D0=B7=20gitign?= =?UTF-8?q?ored=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + justfile | 3 ++ vault/mod.just | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+) create mode 100644 vault/mod.just diff --git a/.gitignore b/.gitignore index 0247c90..d8d3385 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ /deploy.key /deploy.key.pub /flux-webhook-token.txt +/vault-unseal-keys +/vault-root-token # Template config files /cluster.toml # Kubernetes diff --git a/justfile b/justfile index 897c1d3..9bb1aa7 100644 --- a/justfile +++ b/justfile @@ -14,6 +14,9 @@ mod? kube 'kubernetes' [group('talos')] mod? talos 'talos' +[group('security')] +mod? vault 'vault' + [private] log lvl msg *args: gum log -t rfc3339 -s -l "{{ lvl }}" "{{ msg }}" {{ args }} diff --git a/vault/mod.just b/vault/mod.just new file mode 100644 index 0000000..464b751 --- /dev/null +++ b/vault/mod.just @@ -0,0 +1,100 @@ +set no-exit-message +set quiet +set shell := ['bash', '-euo', 'pipefail', '-c'] +set script-interpreter := ['bash', '-euo', 'pipefail'] +set default-list +set default-script + +# Unseal-ключами Vault владеем вручную: файл вне git (см. .gitignore), по одному +# ключу на строку, строки, начинающиеся с '#', игнорируются. +# +# Почему не sops: age-ключ sops лежит в самом кластере (секрет sops-age), поэтому +# любой sops-файл из этого репозитория расшифрует тот, у кого есть доступ к +# кластеру, и смысл ручного unseal (PVC без ключей бесполезен) пропадёт. +# Копии ключей держи в менеджере паролей и на бумаге: они показываются один раз +# при `vault operator init`. +keys_file := env('VAULT_UNSEAL_KEYS', clean(justfile_directory() + '/vault-unseal-keys')) +vault_namespace := env('VAULT_NAMESPACE', 'security') +vault_pod := env('VAULT_POD', 'vault-0') + +# Показать состояние Vault. +[doc('Показать состояние Vault (sealed/unsealed и прогресс распечатывания)')] +[group('vault')] +status: + #!/usr/bin/env bash + set -uo pipefail + + for cmd in kubectl jq; do + command -v "$cmd" > /dev/null || { echo "vault: не найден $cmd (активируй mise в каталоге репозитория)" >&2; exit 1; } + done + + kubectl -n "{{ vault_namespace }}" exec -i "{{ vault_pod }}" -- vault status || true + +# Распечатать Vault ключами из файла. +[doc('Распечатать Vault ключами из файла vault-unseal-keys')] +[group('vault')] +unseal: + #!/usr/bin/env bash + set -euo pipefail + + keys_file="{{ keys_file }}" + ns="{{ vault_namespace }}" + pod="{{ vault_pod }}" + + for cmd in kubectl jq; do + command -v "$cmd" > /dev/null || { echo "vault: не найден $cmd (активируй mise в каталоге репозитория)" >&2; exit 1; } + done + + if ! kubectl -n "$ns" get pod "$pod" > /dev/null 2>&1; then + echo "vault: под $ns/$pod недоступен — Vault не запущен или kubeconfig не тот" >&2 + exit 1 + fi + + if [[ ! -f "$keys_file" ]]; then + echo "vault: нет файла с unseal-ключами: $keys_file" >&2 + echo "" >&2 + echo "Создай его в корне репозитория (в git он не попадёт, см. .gitignore)" >&2 + echo "и положи по одному ключу на строку, например:" >&2 + echo "" >&2 + echo " <ключ 1 из vault operator init>" >&2 + echo " <ключ 2 из vault operator init>" >&2 + echo " <ключ 3 из vault operator init>" >&2 + echo "" >&2 + echo "Vault инициализируется с 5 долями и порогом 3, ключи показываются один раз." >&2 + exit 1 + fi + + if find "$keys_file" -perm /077 -print -quit | grep -q .; then + echo "vault: у файла ключей слишком широкие права — сделай chmod 600 $keys_file" >&2 + fi + + sealed() { + kubectl -n "$ns" exec -i "$pod" -- vault status -format=json 2> /dev/null | jq -r '.sealed | tostring' || true + } + + if [[ "$(sealed)" == "false" ]]; then + echo "vault: уже распечатан, ничего не делаю" + exit 0 + fi + + used=0 + while IFS= read -r key || [[ -n "$key" ]]; do + key="$(printf '%s' "$key" | tr -d '[:space:]')" + [[ -z "$key" || "$key" == \#* ]] && continue + + used=$((used + 1)) + echo "vault: отправляю долю $used" + kubectl -n "$ns" exec -i "$pod" -- vault operator unseal "$key" || true + + if [[ "$(sealed)" == "false" ]]; then + echo "vault: распечатан, использовано долей: $used" + break + fi + done < "$keys_file" + + if [[ "$(sealed)" != "false" ]]; then + echo "vault: всё ещё запечатан — проверь, что в файле есть порог (3) верных долей" >&2 + exit 1 + fi + + kubectl -n "$ns" exec -i "$pod" -- vault status || true -- 2.54.0 From 869817c4db444c2becaf92ab521180efb947a694 Mon Sep 17 00:00:00 2001 From: hermes-agent Date: Thu, 17 Sep 2026 17:27:14 +0000 Subject: [PATCH 3/3] =?UTF-8?q?fix(vault):=20remediation=20rollback=20?= =?UTF-8?q?=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20uninstall=20=E2=80=94=20?= =?UTF-8?q?Vault=20=D0=BA=D1=80=D0=B8=D1=82=D0=B8=D1=87=D0=B5=D0=BD=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20ESO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kubernetes/apps/security/vault/app/helmrelease.yaml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/kubernetes/apps/security/vault/app/helmrelease.yaml b/kubernetes/apps/security/vault/app/helmrelease.yaml index a70dbb9..74d739b 100644 --- a/kubernetes/apps/security/vault/app/helmrelease.yaml +++ b/kubernetes/apps/security/vault/app/helmrelease.yaml @@ -19,9 +19,14 @@ spec: cleanupOnFail: true remediation: retries: 3 - # Упавший первый релиз без этой настройки залипает: Flux пытается - # откатиться, предыдущей ревизии нет и HelmRelease навсегда остаётся False. - strategy: uninstall + # Сознательно rollback (дефолт), а не uninstall как у proxmox-csi. Данные + # uninstall переживут: PVC данных чарт не рендерит, он отдаёт + # volumeClaimTemplates (helper vault.volumeclaims), а PVC создаёт контроллер + # StatefulSet — это не ресурс Helm, и k8s удаляет такие PVC только по + # persistentVolumeClaimRetentionPolicy (у чарта пусто = дефолт Retain). + # Но uninstall уносит сам сервис, а от Vault зависят ESO и приложения — + # важнее остаться на прошлой рабочей ревизии. + strategy: rollback values: global: enabled: true -- 2.54.0