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