feat(security): Vault — self-hosted стор секретов (standalone + raft) #6
@@ -7,6 +7,8 @@
|
|||||||
/deploy.key
|
/deploy.key
|
||||||
/deploy.key.pub
|
/deploy.key.pub
|
||||||
/flux-webhook-token.txt
|
/flux-webhook-token.txt
|
||||||
|
/vault-unseal-keys
|
||||||
|
/vault-root-token
|
||||||
# Template config files
|
# Template config files
|
||||||
/cluster.toml
|
/cluster.toml
|
||||||
# Kubernetes
|
# Kubernetes
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ mod? kube 'kubernetes'
|
|||||||
[group('talos')]
|
[group('talos')]
|
||||||
mod? talos 'talos'
|
mod? talos 'talos'
|
||||||
|
|
||||||
|
[group('security')]
|
||||||
|
mod? vault 'vault'
|
||||||
|
|
||||||
[private]
|
[private]
|
||||||
log lvl msg *args:
|
log lvl msg *args:
|
||||||
gum log -t rfc3339 -s -l "{{ lvl }}" "{{ msg }}" {{ args }}
|
gum log -t rfc3339 -s -l "{{ lvl }}" "{{ msg }}" {{ args }}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: security
|
||||||
|
|
||||||
|
components:
|
||||||
|
- ../../components/sops
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- ./namespace.yaml
|
||||||
|
- ./vault/ks.yaml
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: security
|
||||||
|
annotations:
|
||||||
|
kustomize.toolkit.fluxcd.io/prune: disabled
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
# Сознательно 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
|
||||||
|
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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ./gitrepository.yaml
|
||||||
|
- ./helmrelease.yaml
|
||||||
|
- ./httproute.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
|
||||||
+100
@@ -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
|
||||||
Reference in New Issue
Block a user