feat: добавлены аддоны SMTP Relay, HashiCorp Vault, External Secrets Operator

- SMTP Relay (bokysan/mail): Postfix relay через Yandex SMTP, порт 465 с
  TLS wrappermode, trusted networks only (pod/service CIDR), без аутентификации
  внутри кластера — поды отправляют на smtp-relay:25
- HashiCorp Vault (hashicorp/vault): standalone и HA (Raft) режимы,
  auto-unseal: k8s Secret (homelab), AWS KMS, GCP CKMS, Azure Key Vault,
  Transit; Vault Agent Injector по умолчанию; Job инициализации + Unsealer
  Deployment для k8s режима; README с полным гайдом по injection в YAML/Helm
- External Secrets Operator (ESO): синхронизирует Vault секреты в k8s Secrets,
  ClusterSecretStore с AppRole auth, README с примерами ExternalSecret в
  YAML манифестах, Helm чартах и ArgoCD

Обновлены: addons.yml (3 новых флага + секции), vault.yml.example
(smtp_relay_password, aws_kms_*, eso_approle_secret_id),
playbooks/addons.yml, Makefile
This commit is contained in:
Sergey Antropoff
2026-04-25 18:31:06 +03:00
parent a209b8a9bf
commit 3765bc87b6
20 changed files with 1599 additions and 0 deletions

View File

@@ -57,6 +57,7 @@ DOCKER_RUN := docker run --rm -it \
addon-loki addon-promtail addon-tempo addon-pushgateway \
addon-harbor addon-gitea addon-owncloud addon-nextcloud \
addon-csi-s3 addon-csi-ceph addon-csi-glusterfs addon-vaultwarden \
addon-smtp-relay addon-vault addon-external-secrets \
add-node remove-node \
add-etcd-node remove-etcd-node \
etcd-backup etcd-restore etcd-list-snapshots \
@@ -386,6 +387,18 @@ addon-vaultwarden: _check_env _check_image ## Установить Vaultwarden
@printf "$(CYAN)Устанавливаю Vaultwarden...$(NC)\n"
$(DOCKER_RUN) addon vaultwarden $(ARGS)
addon-smtp-relay: _check_env _check_image ## Установить SMTP Relay — Postfix → Yandex (уведомления из подов без внешней авторизации)
@printf "$(CYAN)Устанавливаю SMTP Relay...$(NC)\n"
$(DOCKER_RUN) addon smtp-relay $(ARGS)
addon-vault: _check_env _check_image ## Установить HashiCorp Vault — менеджер секретов (ARGS="-e vault_mode=ha -e vault_auto_unseal_type=k8s")
@printf "$(CYAN)Устанавливаю HashiCorp Vault...$(NC)\n"
$(DOCKER_RUN) addon vault $(ARGS)
addon-external-secrets: _check_env _check_image ## Установить External Secrets Operator → Vault/AWS/GCP (ARGS="-e external_secrets_vault_role_id=...")
@printf "$(CYAN)Устанавливаю External Secrets Operator...$(NC)\n"
$(DOCKER_RUN) addon external-secrets $(ARGS)
# Generic цель — любой аддон из addons/<name>/playbook.yml
addon-%: _check_env _check_image
@if [ ! -f "addons/$*/playbook.yml" ]; then \

View File

@@ -0,0 +1,288 @@
# External Secrets Operator (ESO)
Синхронизирует секреты из внешних хранилищ (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager и др.) в нативные Kubernetes Secrets.
**Когда использовать ESO вместо Vault Agent Injector:**
- Нужны стандартные k8s Secrets (`secretRef`, `envFrom`) — Helm чарты не менять
- Секреты нужны до старта пода (init containers, admission webhooks)
- Нужна синхронизация между namespace
- Нет желания добавлять Vault annotations в каждый Deployment
## Установка
```bash
# Базовая установка (настрой ClusterSecretStore вручную после)
make addon-external-secrets
# Сразу с настроенным ClusterSecretStore → Vault (если AppRole уже создан)
make addon-external-secrets ARGS="-e external_secrets_vault_role_id=<role-id>"
```
## Настройка AppRole в Vault
ESO использует AppRole для аутентификации в Vault. Выполни один раз:
```bash
# 1. Port-forward к Vault
kubectl port-forward -n vault svc/vault 8200:8200 &
export VAULT_ADDR=http://localhost:8200
vault login <root_token>
# 2. Включить AppRole auth (если ещё не включён)
vault auth enable approle
# 3. Создать политику для ESO (read-only на все секреты)
vault policy write eso-policy - <<EOF
path "secret/data/*" {
capabilities = ["read"]
}
path "secret/metadata/*" {
capabilities = ["read", "list"]
}
EOF
# 4. Создать AppRole роль
vault write auth/approle/role/eso-role \
secret_id_ttl="0" \
token_ttl="1h" \
token_max_ttl="24h" \
token_policies="eso-policy"
# 5. Получить Role ID (публичный, вставить в addons.yml)
vault read -field=role_id auth/approle/role/eso-role/role-id
# → Скопируй в external_secrets_vault_role_id в group_vars/all/addons.yml
# 6. Получить Secret ID (секретный, вставить в vault.yml)
vault write -f -field=secret_id auth/approle/role/eso-role/secret-id
# → Скопируй в vault_eso_approle_secret_id в group_vars/all/vault.yml
# 7. Применить ClusterSecretStore
make addon-external-secrets ARGS="-e external_secrets_vault_role_id=<role-id>"
```
## Проверка подключения
```bash
# Статус ClusterSecretStore (должен быть Valid)
kubectl get clustersecretstore vault-backend
# Подробности
kubectl describe clustersecretstore vault-backend
```
## Использование в YAML манифестах
### Простой ExternalSecret — синхронизация одного секрета
```yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-db-secret
namespace: myapp
spec:
refreshInterval: 1h # как часто синхронизировать
secretStoreRef:
name: vault-backend # ClusterSecretStore
kind: ClusterSecretStore
target:
name: myapp-db-credentials # имя k8s Secret который будет создан
creationPolicy: Owner # ESO управляет Secret (авто-удаление)
data:
- secretKey: DB_PASSWORD # ключ в k8s Secret
remoteRef:
key: myapp/database # путь в Vault: secret/data/myapp/database
property: password # поле внутри секрета
- secretKey: DB_USER
remoteRef:
key: myapp/database
property: username
```
После применения ESO создаст k8s Secret `myapp-db-credentials`.
### Использование в Deployment (через envFrom)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: myapp
spec:
template:
spec:
containers:
- name: app
image: myapp:latest
envFrom:
- secretRef:
name: myapp-db-credentials # создан ExternalSecret выше
# ИЛИ выборочно:
env:
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: myapp-db-credentials
key: DB_PASSWORD
```
### Весь секрет целиком (все поля из Vault)
```yaml
spec:
dataFrom:
- extract:
key: myapp/config # все поля из secret/data/myapp/config станут ключами в k8s Secret
```
### ExternalSecret с template (форматирование)
```yaml
spec:
target:
name: myapp-connection-string
template:
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@postgres:5432/mydb"
dataFrom:
- extract:
key: myapp/database
```
## Использование в Helm чартах
### Способ 1: ExternalSecret в том же Helm чарте
```yaml
# templates/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "myapp.fullname" . }}-secrets
namespace: {{ .Release.Namespace }}
spec:
refreshInterval: {{ .Values.secrets.refreshInterval | default "1h" }}
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: {{ include "myapp.fullname" . }}-secrets
creationPolicy: Owner
dataFrom:
- extract:
key: {{ .Values.secrets.vaultPath }} # задаётся в values.yaml
```
```yaml
# values.yaml
secrets:
vaultPath: "myapp/production/config"
refreshInterval: "30m"
# Deployment использует стандартный envFrom:
envFrom:
- secretRef:
name: "{{ include "myapp.fullname" . }}-secrets"
```
### Способ 2: ESO как внешняя зависимость (рекомендуется)
Секреты создаются заранее через отдельный ExternalSecret, Helm чарт просто ссылается на k8s Secret:
```bash
# 1. Создать ExternalSecret (один раз, в GitOps репозитории)
kubectl apply -f external-secret.yaml
# 2. Установить Helm чарт — он найдёт уже существующий Secret
helm install myapp ./mychart --set existingSecret=myapp-db-credentials
```
```yaml
# values.yaml чарта
existingSecret: "" # если задан — используется, иначе chart создаёт свой
# templates/deployment.yaml
{{- if .Values.existingSecret }}
envFrom:
- secretRef:
name: {{ .Values.existingSecret }}
{{- end }}
```
### Способ 3: ArgoCD + ESO
```yaml
# argocd Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
spec:
source:
helm:
values: |
# Секреты создаются ESO автоматически — чарт просто ссылается
envFrom:
- secretRef:
name: myapp-vault-secrets
```
## PushSecret — запись секретов в Vault из k8s
ESO поддерживает обратную синхронизацию (k8s → Vault):
```yaml
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-to-vault
namespace: myapp
spec:
refreshInterval: 10s
secretStoreRefs:
- name: vault-backend
kind: ClusterSecretStore
selector:
secret:
name: my-local-secret # существующий k8s Secret
data:
- match:
secretKey: password
remoteRef:
remoteKey: myapp/config
property: password
```
## Управление несколькими SecretStore
### Namespace-scoped SecretStore (для разных Vault paths/политик)
```yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-myapp # только для namespace myapp
namespace: myapp
spec:
provider:
vault:
server: "http://vault.vault.svc.cluster.local:8200"
path: "myapp" # ограниченный путь
version: "v2"
auth:
appRole:
path: "approle"
roleId: "<myapp-role-id>"
secretRef:
name: vault-approle-myapp
key: secretId
```
## Мониторинг
Если `addon_prometheus_stack: true`, метрики ESO доступны в Grafana:
- `external_secrets_sync_calls_total` — количество синхронизаций
- `external_secrets_sync_call_errors_total` — ошибки
- Дашборд: импортируй Grafana Dashboard ID 21045
```bash
# Статус всех ExternalSecrets в кластере
kubectl get externalsecrets --all-namespaces
# Подробности (условие Ready/SecretSynced)
kubectl describe externalsecret myapp-db-secret -n myapp
```

View File

@@ -0,0 +1,7 @@
---
- name: Install External Secrets Operator
hosts: k3s_master[0]
gather_facts: false
become: true
roles:
- role: "{{ playbook_dir }}/../addons/external-secrets/role"

View File

@@ -0,0 +1,37 @@
---
external_secrets_version: "" # "" = автоматически последняя версия чарта
external_secrets_namespace: "external-secrets"
external_secrets_chart_repo: "https://charts.external-secrets.io"
# ── Vault backend ─────────────────────────────────────────────────────────────
# Адрес Vault (по умолчанию — внутренний Service addon_vault)
external_secrets_vault_url: "http://vault.vault.svc.cluster.local:8200"
# Путь к секретам в Vault (KV v2)
external_secrets_vault_kv_path: "secret"
external_secrets_vault_kv_version: "v2"
# ── Vault AppRole аутентификация для ESO ─────────────────────────────────────
# Role ID задаётся при создании AppRole в Vault (шаг 3 в README.md)
external_secrets_vault_role_id: ""
# Secret ID задаётся в vault.yml: vault_eso_approle_secret_id
external_secrets_vault_secret_id: "{{ vault_eso_approle_secret_id | default('') }}"
# AppRole path (по умолчанию: approle)
external_secrets_vault_approle_path: "approle"
# ── ClusterSecretStore ────────────────────────────────────────────────────────
# Имя глобального ClusterSecretStore для Vault
external_secrets_vault_store_name: "vault-backend"
# ── Метрики ───────────────────────────────────────────────────────────────────
external_secrets_metrics_enabled: true
# ── Ресурсы ───────────────────────────────────────────────────────────────────
external_secrets_resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi

View File

@@ -0,0 +1,94 @@
---
- name: Add External Secrets Helm repo
kubernetes.core.helm_repository:
name: external-secrets
repo_url: "{{ external_secrets_chart_repo }}"
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Get latest External Secrets chart version
ansible.builtin.shell: |
helm search repo external-secrets/external-secrets --output json | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['version'])"
register: _eso_latest_version
changed_when: false
when: external_secrets_version == ""
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Set External Secrets chart version
ansible.builtin.set_fact:
_eso_version: "{{ external_secrets_version if external_secrets_version != '' else _eso_latest_version.stdout | trim }}"
- name: Install External Secrets Operator via Helm
kubernetes.core.helm:
name: external-secrets
chart_ref: external-secrets/external-secrets
chart_version: "{{ _eso_version }}"
release_namespace: "{{ external_secrets_namespace }}"
create_namespace: true
wait: true
timeout: "5m0s"
values:
resources:
requests:
cpu: "{{ external_secrets_resources.requests.cpu }}"
memory: "{{ external_secrets_resources.requests.memory }}"
limits:
cpu: "{{ external_secrets_resources.limits.cpu }}"
memory: "{{ external_secrets_resources.limits.memory }}"
serviceMonitor:
enabled: "{{ external_secrets_metrics_enabled | bool and addon_prometheus_stack | default(false) | bool }}"
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Wait for External Secrets Operator to be ready
ansible.builtin.command: >
k3s kubectl -n {{ external_secrets_namespace }}
rollout status deployment/external-secrets --timeout=120s
changed_when: false
retries: 3
delay: 10
# ─── ClusterSecretStore → Vault (только если role_id задан) ──────────────────
- name: Template ClusterSecretStore for Vault
ansible.builtin.template:
src: cluster-secret-store.yaml.j2
dest: /tmp/vault-cluster-secret-store.yaml
mode: '0644'
when: external_secrets_vault_role_id != ""
- name: Apply ClusterSecretStore and AppRole credentials Secret
ansible.builtin.command: k3s kubectl apply -f /tmp/vault-cluster-secret-store.yaml
changed_when: true
when: external_secrets_vault_role_id != ""
- name: Show External Secrets Operator access info
ansible.builtin.debug:
msg:
- "External Secrets Operator установлен в namespace: {{ external_secrets_namespace }}"
- ""
- "{% if external_secrets_vault_role_id != '' %}ClusterSecretStore '{{ external_secrets_vault_store_name }}' создан → {{ external_secrets_vault_url }}"
- "{% else %}ClusterSecretStore НЕ создан — задай external_secrets_vault_role_id"
- " Следуй шагам в addons/external-secrets/README.md (раздел 'Настройка AppRole в Vault')"
- "{% endif %}"
- ""
- "Документация: addons/external-secrets/README.md"
- ""
- "Быстрый пример ExternalSecret:"
- " apiVersion: external-secrets.io/v1beta1"
- " kind: ExternalSecret"
- " metadata:"
- " name: my-secret"
- " spec:"
- " refreshInterval: 1h"
- " secretStoreRef:"
- " name: {{ external_secrets_vault_store_name }}"
- " kind: ClusterSecretStore"
- " target:"
- " name: my-k8s-secret"
- " data:"
- " - secretKey: password"
- " remoteRef:"
- " key: myapp/config"
- " property: password"

View File

@@ -0,0 +1,31 @@
---
# Kubernetes Secret с AppRole credentials для ESO → Vault
apiVersion: v1
kind: Secret
metadata:
name: vault-approle-credentials
namespace: {{ external_secrets_namespace }}
type: Opaque
stringData:
roleId: "{{ external_secrets_vault_role_id }}"
secretId: "{{ external_secrets_vault_secret_id }}"
---
# ClusterSecretStore: глобальная точка доступа к Vault для всех namespace
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: {{ external_secrets_vault_store_name }}
spec:
provider:
vault:
server: "{{ external_secrets_vault_url }}"
path: "{{ external_secrets_vault_kv_path }}"
version: "{{ external_secrets_vault_kv_version }}"
auth:
appRole:
path: "{{ external_secrets_vault_approle_path }}"
roleId: "{{ external_secrets_vault_role_id }}"
secretRef:
name: vault-approle-credentials
namespace: {{ external_secrets_namespace }}
key: secretId

View File

@@ -0,0 +1,7 @@
---
- name: Install SMTP Relay (Postfix → Yandex)
hosts: k3s_master[0]
gather_facts: false
become: true
roles:
- role: "{{ playbook_dir }}/../addons/smtp-relay/role"

View File

@@ -0,0 +1,33 @@
---
smtp_relay_namespace: "smtp-relay"
smtp_relay_chart_repo: "https://bokysan.github.io/docker-postfix/"
smtp_relay_version: "" # "" = автоматически последняя версия чарта
# ── Upstream SMTP сервер ──────────────────────────────────────────────────────
smtp_relay_host: "smtp.yandex.ru"
smtp_relay_port: 465 # 465 = SMTPS (TLS wrapper), 587 = STARTTLS
smtp_relay_username: "sergey@antropoff.ru"
# Пароль задаётся в vault.yml: vault_smtp_relay_password
smtp_relay_password: "{{ vault_smtp_relay_password | default('') }}"
# Имя отправителя по умолчанию (for envelope-from rewriting)
smtp_relay_from: "vault@antropoff.ru"
# Разрешённые домены отправителей (через пробел)
smtp_relay_allowed_sender_domains: "antropoff.ru"
# ── Доступ к relay ────────────────────────────────────────────────────────────
# Только доверенные сети (pod CIDR + service CIDR + localhost)
# Задай значения соответствующие твоему кластеру
smtp_relay_mynetworks: "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8"
# ── Развёртывание ─────────────────────────────────────────────────────────────
smtp_relay_replicas: 1
smtp_relay_resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi

View File

@@ -0,0 +1,68 @@
---
- name: Add bokysan Helm repo (docker-postfix SMTP relay)
kubernetes.core.helm_repository:
name: bokysan
repo_url: "{{ smtp_relay_chart_repo }}"
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Get latest smtp-relay chart version
ansible.builtin.shell: |
helm search repo bokysan/mail --output json | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['version'])"
register: _smtp_relay_latest_version
changed_when: false
when: smtp_relay_version == ""
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Set smtp-relay chart version
ansible.builtin.set_fact:
_smtp_relay_version: "{{ smtp_relay_version if smtp_relay_version != '' else _smtp_relay_latest_version.stdout | trim }}"
- name: Template SMTP Relay values
ansible.builtin.template:
src: smtp-relay-values.yaml.j2
dest: /tmp/smtp-relay-values.yaml
mode: '0600'
- name: Install SMTP Relay via Helm
kubernetes.core.helm:
name: smtp-relay
chart_ref: bokysan/mail
chart_version: "{{ _smtp_relay_version }}"
release_namespace: "{{ smtp_relay_namespace }}"
create_namespace: true
wait: true
timeout: "3m0s"
values_files:
- /tmp/smtp-relay-values.yaml
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Wait for SMTP Relay to be ready
ansible.builtin.command: >
k3s kubectl -n {{ smtp_relay_namespace }}
rollout status deployment/smtp-relay --timeout=120s
changed_when: false
retries: 3
delay: 10
- name: Show SMTP Relay access info
ansible.builtin.debug:
msg:
- "SMTP Relay установлен в namespace: {{ smtp_relay_namespace }}"
- "Service: smtp-relay.{{ smtp_relay_namespace }}.svc.cluster.local:25"
- "Upstream: {{ smtp_relay_host }}:{{ smtp_relay_port }} (TLS wrappermode)"
- "Username: {{ smtp_relay_username }}"
- "Trusted networks: {{ smtp_relay_mynetworks }}"
- "Allowed sender domains: {{ smtp_relay_allowed_sender_domains }}"
- ""
- "Использование в подах (пример):"
- " SMTP_HOST=smtp-relay.{{ smtp_relay_namespace }}.svc.cluster.local"
- " SMTP_PORT=25"
- " SMTP_FROM={{ smtp_relay_from }}"
- " (без аутентификации — relay принимает из trusted networks)"
- ""
- "Тест: kubectl run -it --rm smtp-test --image=alpine --restart=Never -- sh"
- " apk add mailx && echo 'test' | mail -s 'test' -r {{ smtp_relay_from }} you@example.com -S smtp=smtp://smtp-relay.{{ smtp_relay_namespace }}.svc.cluster.local"

View File

@@ -0,0 +1,33 @@
replicaCount: {{ smtp_relay_replicas }}
config:
general:
# Upstream relay — скобки отключают MX-lookup, порт явный
RELAYHOST: "[{{ smtp_relay_host }}]:{{ smtp_relay_port }}"
RELAYHOST_USERNAME: "{{ smtp_relay_username }}"
# Только эти сети могут использовать relay
MYNETWORKS: "{{ smtp_relay_mynetworks }}"
# Разрешённые домены отправителей
ALLOWED_SENDER_DOMAINS: "{{ smtp_relay_allowed_sender_domains }}"
# TLS: encrypt = обязательный TLS
RELAYHOST_TLS_LEVEL: "encrypt"
# Port 465 = SMTPS (TLS wrapper mode), требует явного флага
POSTFIX_smtp_tls_wrappermode: "yes"
# Запрет открытого релея — принимаем только от MYNETWORKS
POSTFIX_smtpd_relay_restrictions: "permit_mynetworks reject"
POSTFIX_smtpd_recipient_restrictions: "permit_mynetworks reject_unauth_destination"
secret:
RELAYHOST_PASSWORD: "{{ smtp_relay_password }}"
resources:
requests:
cpu: "{{ smtp_relay_resources.requests.cpu }}"
memory: "{{ smtp_relay_resources.requests.memory }}"
limits:
cpu: "{{ smtp_relay_resources.limits.cpu }}"
memory: "{{ smtp_relay_resources.limits.memory }}"
service:
type: ClusterIP
port: 25

284
addons/vault/README.md Normal file
View File

@@ -0,0 +1,284 @@
# HashiCorp Vault
Self-hosted менеджер секретов с шифрованием, аудитом и fine-grained политиками доступа.
## Установка
```bash
# Standalone (по умолчанию, 1 Pod)
make addon-vault
# HA (3 Pods с Raft, нужны минимум 3 ноды)
make addon-vault ARGS="-e vault_mode=ha"
# С авто-unseal через k8s Secret (homelab/dev)
make addon-vault ARGS="-e vault_auto_unseal_type=k8s"
# С AWS KMS авто-unseal (production)
make addon-vault ARGS="-e vault_auto_unseal_type=aws -e vault_aws_kms_region=us-east-1 -e vault_aws_kms_key_id=..."
# С Ingress
make addon-vault ARGS="-e vault_ingress_enabled=true -e vault_ingress_host=vault.example.com"
```
## Первичная инициализация (ручной режим)
После установки Vault нужно инициализировать (только один раз):
```bash
# Инициализация (генерирует unseal keys + root token)
kubectl exec -n vault vault-0 -- vault operator init \
-key-shares=5 \
-key-threshold=3
# ВАЖНО: сохрани 5 unseal keys и root token в надёжное место!
# Пример вывода:
# Unseal Key 1: abc...
# Unseal Key 2: def...
# ...
# Initial Root Token: hvs.xxxxx
# Unseal (нужно подать 3 из 5 ключей)
kubectl exec -n vault vault-0 -- vault operator unseal <key1>
kubectl exec -n vault vault-0 -- vault operator unseal <key2>
kubectl exec -n vault vault-0 -- vault operator unseal <key3>
# Проверка
kubectl exec -n vault vault-0 -- vault status
```
## Авто-unseal режимы
### k8s Secret (homelab, не для production)
```bash
make addon-vault ARGS="-e vault_auto_unseal_type=k8s"
```
Vault автоматически инициализируется и unseal keys сохраняются в k8s Secret `vault-unseal-keys`.
Unsealer Deployment следит за состоянием и unseals при перезапуске.
**Получить root token:**
```bash
kubectl get secret vault-unseal-keys -n vault \
-o jsonpath='{.data.root_token}' | base64 -d
```
### AWS KMS (production)
```yaml
# group_vars/all/addons.yml
vault_auto_unseal_type: "aws"
vault_aws_kms_region: "us-east-1"
vault_aws_kms_key_id: "arn:aws:kms:us-east-1:..."
# group_vars/all/vault.yml
vault_aws_kms_access_key: "AKIAIOSFODNN7EXAMPLE"
vault_aws_kms_secret_key: "wJalrXUtnFEMI/K7MDENG/..."
```
### Transit Seal (через другой Vault)
```yaml
vault_auto_unseal_type: "transit"
vault_transit_address: "https://vault-primary.example.com"
vault_transit_key_name: "autounseal"
# vault.yml:
vault_transit_seal_token: "hvs.CAESIxxxxxx"
```
## Работа с секретами
### CLI (локально через port-forward)
```bash
# Port-forward
kubectl port-forward -n vault svc/vault 8200:8200 &
# Авторизация
export VAULT_ADDR=http://localhost:8200
vault login <root_token>
# Включить KV v2 engine
vault secrets enable -path=secret kv-v2
# Записать секрет
vault kv put secret/myapp/config \
db_password="supersecret" \
api_key="abc123"
# Прочитать секрет
vault kv get secret/myapp/config
vault kv get -field=db_password secret/myapp/config
```
### CLI (из пода в кластере)
```bash
kubectl exec -n vault vault-0 -- env VAULT_ADDR=http://localhost:8200 \
vault kv put secret/myapp/config db_password="supersecret"
```
## Kubernetes Auth Method
Позволяет подам авторизоваться через их ServiceAccount токен:
```bash
# Включить kubernetes auth
vault auth enable kubernetes
# Настроить
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc.cluster.local:443"
# Создать политику доступа
vault policy write myapp-policy - <<EOF
path "secret/data/myapp/*" {
capabilities = ["read"]
}
EOF
# Создать роль (привязка: namespace + serviceaccount → policy)
vault write auth/kubernetes/role/myapp-role \
bound_service_account_names="myapp-sa" \
bound_service_account_namespaces="myapp" \
policies="myapp-policy" \
ttl="1h"
```
## Vault Agent Injector — секреты в YAML манифестах
Injector (установлен по умолчанию) автоматически монтирует секреты через sidecar по annotations.
### Пример Pod с автоинжекцией секретов
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: myapp
spec:
template:
metadata:
annotations:
# Включаем инжектор
vault.hashicorp.com/agent-inject: "true"
# Адрес Vault (по умолчанию авто-определяется)
vault.hashicorp.com/role: "myapp-role"
# Инжектировать секрет как файл /vault/secrets/config
vault.hashicorp.com/agent-inject-secret-config: "secret/data/myapp/config"
# Шаблон (опционально — форматирует вывод)
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/myapp/config" -}}
DB_PASSWORD={{ .Data.data.db_password }}
API_KEY={{ .Data.data.api_key }}
{{- end }}
spec:
serviceAccountName: myapp-sa # должен совпадать с vault role
containers:
- name: app
image: myapp:latest
command: ["sh", "-c", "source /vault/secrets/config && ./myapp"]
```
### Создать ServiceAccount для пода
```bash
kubectl create serviceaccount myapp-sa -n myapp
```
### Инжекция как переменные окружения
```yaml
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp-role"
vault.hashicorp.com/agent-inject-secret-env: "secret/data/myapp/config"
vault.hashicorp.com/agent-inject-template-env: |
{{- with secret "secret/data/myapp/config" -}}
export DB_PASSWORD="{{ .Data.data.db_password }}"
export API_KEY="{{ .Data.data.api_key }}"
{{- end }}
```
```yaml
# В контейнере:
command: ["/bin/sh", "-c", "source /vault/secrets/env && exec myapp"]
```
## Vault в Helm чартах
### Через Vault Agent (annotations в values.yaml)
```yaml
# values.yaml вашего чарта
podAnnotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "myapp-role"
vault.hashicorp.com/agent-inject-secret-db: "secret/data/myapp/db"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "secret/data/myapp/db" -}}
{{ .Data.data.password }}
{{- end }}
serviceAccount:
create: true
name: myapp-sa
```
### Через External Secrets Operator (рекомендуется — см. ESO README)
```yaml
# Helm чарт не меняется — ESO создаёт k8s Secret автоматически
# Используй стандартный envFrom/secretRef в values.yaml
envFrom:
- secretRef:
name: myapp-secrets # создан ExternalSecret → Vault
```
## AppRole Auth (для сервисов без k8s ServiceAccount)
```bash
# Включить AppRole
vault auth enable approle
# Создать роль
vault write auth/approle/role/myservice \
secret_id_ttl="720h" \
token_ttl="1h" \
token_policies="myapp-policy"
# Получить Role ID (не секретный)
vault read auth/approle/role/myservice/role-id
# Получить Secret ID (секретный, одноразовый)
vault write -f auth/approle/role/myservice/secret-id
# Авторизация через AppRole
vault write auth/approle/login \
role_id="<role_id>" \
secret_id="<secret_id>"
```
## Аудит и мониторинг
```bash
# Включить аудит лог в stdout
vault audit enable file file_path=stdout
# Статус кластера (HA)
kubectl exec -n vault vault-0 -- vault operator members
# Grafana: метрики доступны если addon_prometheus_stack: true
# Дашборд: импортируй ID 12904 из Grafana.com
```
## Полезные команды
```bash
# Статус
kubectl exec -n vault vault-0 -- vault status
# Список engines
kubectl exec -n vault vault-0 -- vault secrets list
# Список auth methods
kubectl exec -n vault vault-0 -- vault auth list
# Seal (экстренная блокировка)
kubectl exec -n vault vault-0 -- vault operator seal
# Raft cluster (HA)
kubectl exec -n vault vault-0 -- vault operator raft list-peers
```

View File

@@ -0,0 +1,7 @@
---
- name: Install HashiCorp Vault
hosts: k3s_master[0]
gather_facts: false
become: true
roles:
- role: "{{ playbook_dir }}/../addons/vault/role"

View File

@@ -0,0 +1,76 @@
---
vault_version: "" # "" = автоматически последняя версия чарта
vault_namespace: "vault"
vault_chart_repo: "https://helm.releases.hashicorp.com"
# ── Режим развёртывания ───────────────────────────────────────────────────────
# standalone — 1 Pod с Raft storage (по умолчанию, подходит для dev/prod single-node)
# ha — 3 Pod с Raft (для продакшн HA кластера, требует минимум 3 ноды)
vault_mode: "standalone"
vault_ha_replicas: 3
# ── Vault Agent Injector ──────────────────────────────────────────────────────
# Инжектирует секреты в поды через sidecar (annotations: vault.hashicorp.com/...)
vault_injector_enabled: true
# ── Auto-unseal ───────────────────────────────────────────────────────────────
# none — ручной unseal через CLI/UI (по умолчанию)
# k8s — хранит unseal-ключи в k8s Secret + Deployment для авто-unseal (homelab)
# aws — AWS KMS (рекомендуется для продакшн)
# gcp — GCP Cloud KMS
# azure — Azure Key Vault
# transit — Vault Transit Seal (другой экземпляр Vault)
vault_auto_unseal_type: "none"
# K8S auto-unseal (vault_auto_unseal_type: k8s)
# ВНИМАНИЕ: unseal-ключи хранятся в k8s Secret — используй только в dev/homelab!
vault_auto_unseal_k8s_secret: "vault-unseal-keys"
vault_auto_unseal_k8s_shares: 5 # общее количество ключей
vault_auto_unseal_k8s_threshold: 3 # минимум для unseal
# AWS KMS auto-unseal (vault_auto_unseal_type: aws)
vault_aws_kms_region: ""
vault_aws_kms_key_id: ""
vault_aws_access_key: "{{ vault_aws_kms_access_key | default('') }}"
vault_aws_secret_key: "{{ vault_aws_kms_secret_key | default('') }}"
# GCP Cloud KMS auto-unseal (vault_auto_unseal_type: gcp)
vault_gcp_project: ""
vault_gcp_region: ""
vault_gcp_key_ring: ""
vault_gcp_crypto_key: ""
# Azure Key Vault auto-unseal (vault_auto_unseal_type: azure)
vault_azure_tenant_id: ""
vault_azure_client_id: ""
vault_azure_client_secret: "{{ vault_azure_kv_client_secret | default('') }}"
vault_azure_keyvault_name: ""
vault_azure_key_name: ""
# Vault Transit Seal (vault_auto_unseal_type: transit)
vault_transit_address: ""
vault_transit_token: "{{ vault_transit_seal_token | default('') }}"
vault_transit_key_name: "autounseal"
# ── Ingress ───────────────────────────────────────────────────────────────────
vault_ingress_enabled: false
vault_ingress_host: "vault-hc.example.com"
vault_ingress_class: "{{ ingress_nginx_class_name | default('nginx') }}"
vault_ingress_tls: true
vault_ingress_cert_issuer: "{{ cert_manager_default_issuer_name | default('letsencrypt-prod') }}"
# ── Хранилище ─────────────────────────────────────────────────────────────────
vault_storage_size: "10Gi"
vault_storage_class: ""
# ── Метрики ───────────────────────────────────────────────────────────────────
vault_metrics_enabled: true
# ── Ресурсы ───────────────────────────────────────────────────────────────────
vault_resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 1000m
memory: 512Mi

View File

@@ -0,0 +1,130 @@
---
- name: Add HashiCorp Helm repo
kubernetes.core.helm_repository:
name: hashicorp
repo_url: "{{ vault_chart_repo }}"
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Get latest Vault chart version
ansible.builtin.shell: |
helm search repo hashicorp/vault --output json | \
python3 -c "import sys,json; print(json.load(sys.stdin)[0]['version'])"
register: _vault_latest_version
changed_when: false
when: vault_version == ""
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Set Vault chart version
ansible.builtin.set_fact:
_vault_version: "{{ vault_version if vault_version != '' else _vault_latest_version.stdout | trim }}"
- name: Template Vault values
ansible.builtin.template:
src: vault-values.yaml.j2
dest: /tmp/vault-values.yaml
mode: '0600'
- name: Install HashiCorp Vault via Helm
kubernetes.core.helm:
name: vault
chart_ref: hashicorp/vault
chart_version: "{{ _vault_version }}"
release_namespace: "{{ vault_namespace }}"
create_namespace: true
wait: true
timeout: "5m0s"
values_files:
- /tmp/vault-values.yaml
environment:
KUBECONFIG: "{{ k3s_kubeconfig_path }}"
- name: Wait for Vault pods to be Running
ansible.builtin.command: >
k3s kubectl -n {{ vault_namespace }}
wait pod -l app.kubernetes.io/name=vault
--for=condition=Ready --timeout=120s
changed_when: false
retries: 5
delay: 15
failed_when: false
# ─── K8S AUTO-UNSEAL (только при vault_auto_unseal_type: k8s) ────────────────
- name: Template Vault init Job (k8s auto-unseal)
ansible.builtin.template:
src: vault-init-job.yaml.j2
dest: /tmp/vault-init-job.yaml
mode: '0644'
when: vault_auto_unseal_type == "k8s"
- name: Delete previous vault-init Job if exists
ansible.builtin.command: >
k3s kubectl delete job vault-init -n {{ vault_namespace }} --ignore-not-found
changed_when: false
when: vault_auto_unseal_type == "k8s"
- name: Apply Vault init Job
ansible.builtin.command: k3s kubectl apply -f /tmp/vault-init-job.yaml
changed_when: true
when: vault_auto_unseal_type == "k8s"
- name: Wait for Vault init Job to complete
ansible.builtin.command: >
k3s kubectl -n {{ vault_namespace }}
wait job/vault-init --for=condition=complete --timeout=300s
changed_when: false
retries: 3
delay: 10
when: vault_auto_unseal_type == "k8s"
- name: Template Vault unsealer Deployment
ansible.builtin.template:
src: vault-unsealer.yaml.j2
dest: /tmp/vault-unsealer.yaml
mode: '0644'
when: vault_auto_unseal_type == "k8s"
- name: Apply Vault unsealer Deployment
ansible.builtin.command: k3s kubectl apply -f /tmp/vault-unsealer.yaml
changed_when: true
when: vault_auto_unseal_type == "k8s"
# ─── Получаем статус ─────────────────────────────────────────────────────────
- name: Check Vault init status
ansible.builtin.command: >
k3s kubectl -n {{ vault_namespace }}
exec vault-0 -- vault status -format=json
register: _vault_status
changed_when: false
failed_when: false
- name: Parse Vault status
ansible.builtin.set_fact:
_vault_initialized: "{{ (_vault_status.stdout | from_json).initialized | default(false) }}"
_vault_sealed: "{{ (_vault_status.stdout | from_json).sealed | default(true) }}"
when: _vault_status.rc == 0
failed_when: false
- name: Show HashiCorp Vault access info
ansible.builtin.debug:
msg:
- "HashiCorp Vault установлен в namespace: {{ vault_namespace }}"
- "Режим: {{ vault_mode }}{% if vault_mode == 'ha' %} ({{ vault_ha_replicas }} реплики, Raft){% endif %}"
- "Auto-unseal: {{ vault_auto_unseal_type }}"
- "Инициализирован: {{ _vault_initialized | default('неизвестно') }}"
- "Sealed: {{ _vault_sealed | default('неизвестно') }}"
- ""
- "{% if vault_ingress_enabled %}UI: https://{{ vault_ingress_host }}{% else %}UI (port-forward): kubectl port-forward -n {{ vault_namespace }} svc/vault 8200:8200{% endif %}"
- "Internal API: http://vault.{{ vault_namespace }}.svc.cluster.local:8200"
- ""
- "{% if vault_auto_unseal_type == 'none' %}=== РУЧНАЯ ИНИЦИАЛИЗАЦИЯ (если ещё не выполнена) ==="
- " kubectl exec -n {{ vault_namespace }} vault-0 -- vault operator init"
- " # Сохрани unseal keys и root token!"
- " kubectl exec -n {{ vault_namespace }} vault-0 -- vault operator unseal <key1>"
- " kubectl exec -n {{ vault_namespace }} vault-0 -- vault operator unseal <key2>"
- " kubectl exec -n {{ vault_namespace }} vault-0 -- vault operator unseal <key3>"
- "{% elif vault_auto_unseal_type == 'k8s' %}Ключи сохранены в Secret: {{ vault_auto_unseal_k8s_secret }} (namespace: {{ vault_namespace }})"
- " kubectl get secret {{ vault_auto_unseal_k8s_secret }} -n {{ vault_namespace }} -o jsonpath='{.data.root_token}' | base64 -d"
- "{% endif %}"
- "Документация: addons/vault/README.md"

View File

@@ -0,0 +1,143 @@
---
# RBAC для Job инициализации и авто-unseal
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-init
namespace: {{ vault_namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vault-init
namespace: {{ vault_namespace }}
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vault-init
namespace: {{ vault_namespace }}
subjects:
- kind: ServiceAccount
name: vault-init
namespace: {{ vault_namespace }}
roleRef:
kind: Role
name: vault-init
apiGroup: rbac.authorization.k8s.io
---
# Job: инициализирует Vault и сохраняет ключи в k8s Secret
apiVersion: batch/v1
kind: Job
metadata:
name: vault-init
namespace: {{ vault_namespace }}
labels:
app: vault-init
spec:
backoffLimit: 5
ttlSecondsAfterFinished: 300
template:
metadata:
labels:
app: vault-init
spec:
serviceAccountName: vault-init
restartPolicy: OnFailure
containers:
- name: vault-init
image: hashicorp/vault:latest
env:
- name: VAULT_ADDR
value: "http://vault.{{ vault_namespace }}.svc.cluster.local:8200"
- name: VAULT_NAMESPACE
value: "{{ vault_namespace }}"
- name: UNSEAL_SECRET_NAME
value: "{{ vault_auto_unseal_k8s_secret }}"
- name: KEY_SHARES
value: "{{ vault_auto_unseal_k8s_shares }}"
- name: KEY_THRESHOLD
value: "{{ vault_auto_unseal_k8s_threshold }}"
command:
- /bin/sh
- -c
- |
set -e
echo "Waiting for Vault to start..."
until vault status 2>&1 | grep -q "Initialized"; do
echo " Vault not ready yet, waiting..."
sleep 5
done
# Проверяем: уже инициализирован?
INIT_STATUS=$(vault status -format=json 2>/dev/null | grep -o '"initialized":[^,}]*' | grep -o '[^:]*$' | tr -d ' ')
if [ "$INIT_STATUS" = "true" ]; then
echo "Vault is already initialized."
# Проверяем: уже unlocked (секрет существует)?
if kubectl get secret "$UNSEAL_SECRET_NAME" -n "$VAULT_NAMESPACE" >/dev/null 2>&1; then
echo "Unseal secret already exists. Nothing to do."
exit 0
fi
echo "WARNING: Vault initialized but unseal keys not found in cluster. Manual unseal required."
exit 0
fi
echo "Initializing Vault with $KEY_SHARES shares, threshold $KEY_THRESHOLD..."
INIT_OUTPUT=$(vault operator init \
-key-shares="$KEY_SHARES" \
-key-threshold="$KEY_THRESHOLD" \
-format=json)
echo "Saving unseal keys and root token to Secret '$UNSEAL_SECRET_NAME'..."
# Формируем Secret с ключами
ROOT_TOKEN=$(echo "$INIT_OUTPUT" | grep -o '"root_token":"[^"]*"' | cut -d'"' -f4)
# Создаём kubectl secret
SECRET_ARGS="--from-literal=root_token=$ROOT_TOKEN"
i=1
echo "$INIT_OUTPUT" | grep -o '"unseal_keys_b64":\[[^]]*\]' | grep -o '"[A-Za-z0-9+/=]*"' | while read KEY; do
KEY_VAL=$(echo "$KEY" | tr -d '"')
SECRET_ARGS="$SECRET_ARGS --from-literal=unseal_key_$i=$KEY_VAL"
i=$((i + 1))
done
# Используем python для надёжного парсинга JSON
python3 - <<PYEOF
import json, subprocess, sys
data = json.loads("""$INIT_OUTPUT""")
root_token = data['root_token']
keys = data['unseal_keys_b64']
args = ['kubectl', 'create', 'secret', 'generic', '$UNSEAL_SECRET_NAME',
'-n', '$VAULT_NAMESPACE',
f'--from-literal=root_token={root_token}']
for i, key in enumerate(keys, 1):
args.append(f'--from-literal=unseal_key_{i}={key}')
result = subprocess.run(args, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: {result.stderr}", file=sys.stderr)
sys.exit(1)
print(f"Secret created with {len(keys)} unseal keys.")
PYEOF
echo ""
echo "=== Unsealing Vault ==="
THRESHOLD={{ vault_auto_unseal_k8s_threshold }}
for i in $(seq 1 $THRESHOLD); do
KEY=$(kubectl get secret "$UNSEAL_SECRET_NAME" -n "$VAULT_NAMESPACE" \
-o jsonpath="{.data.unseal_key_$i}" | base64 -d)
vault operator unseal "$KEY"
done
echo ""
echo "=== Vault initialized and unsealed ==="
echo "Root token saved in Secret: $UNSEAL_SECRET_NAME (key: root_token)"
echo "ВАЖНО: Root token предназначен только для первичной настройки!"
echo " Создай AppRole/UserPass и отзови root token: vault token revoke <root_token>"

View File

@@ -0,0 +1,90 @@
---
# RBAC для авто-unsealer Deployment
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-unsealer
namespace: {{ vault_namespace }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vault-unsealer
namespace: {{ vault_namespace }}
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vault-unsealer
namespace: {{ vault_namespace }}
subjects:
- kind: ServiceAccount
name: vault-unsealer
namespace: {{ vault_namespace }}
roleRef:
kind: Role
name: vault-unsealer
apiGroup: rbac.authorization.k8s.io
---
# Deployment: следит за Vault и unseals при перезапуске
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-unsealer
namespace: {{ vault_namespace }}
labels:
app: vault-unsealer
spec:
replicas: 1
selector:
matchLabels:
app: vault-unsealer
template:
metadata:
labels:
app: vault-unsealer
spec:
serviceAccountName: vault-unsealer
containers:
- name: unsealer
image: hashicorp/vault:latest
env:
- name: VAULT_ADDR
value: "http://vault.{{ vault_namespace }}.svc.cluster.local:8200"
- name: VAULT_NAMESPACE_K8S
value: "{{ vault_namespace }}"
- name: UNSEAL_SECRET_NAME
value: "{{ vault_auto_unseal_k8s_secret }}"
- name: KEY_THRESHOLD
value: "{{ vault_auto_unseal_k8s_threshold }}"
command:
- /bin/sh
- -c
- |
while true; do
STATUS=$(vault status -format=json 2>/dev/null)
if echo "$STATUS" | grep -q '"sealed":true'; then
echo "$(date): Vault is sealed — unsealing..."
for i in $(seq 1 $KEY_THRESHOLD); do
KEY=$(kubectl get secret "$UNSEAL_SECRET_NAME" \
-n "$VAULT_NAMESPACE_K8S" \
-o "jsonpath={.data.unseal_key_$i}" 2>/dev/null | base64 -d)
if [ -n "$KEY" ]; then
vault operator unseal "$KEY" >/dev/null
fi
done
echo "$(date): Unseal applied ($KEY_THRESHOLD keys)."
fi
sleep 30
done
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
cpu: 100m
memory: 64Mi

View File

@@ -0,0 +1,181 @@
global:
enabled: true
injector:
enabled: {{ vault_injector_enabled | lower }}
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 128Mi
server:
resources:
requests:
cpu: "{{ vault_resources.requests.cpu }}"
memory: "{{ vault_resources.requests.memory }}"
limits:
cpu: "{{ vault_resources.limits.cpu }}"
memory: "{{ vault_resources.limits.memory }}"
dataStorage:
enabled: true
size: "{{ vault_storage_size }}"
{% if vault_storage_class %}
storageClass: "{{ vault_storage_class }}"
{% endif %}
serviceMonitor:
enabled: {{ (vault_metrics_enabled | bool and addon_prometheus_stack | default(false) | bool) | lower }}
{% if vault_ingress_enabled | bool %}
ingress:
enabled: true
ingressClassName: "{{ vault_ingress_class }}"
hosts:
- host: "{{ vault_ingress_host }}"
paths:
- /
{% if vault_ingress_tls | bool %}
tls:
- secretName: vault-tls
hosts:
- "{{ vault_ingress_host }}"
annotations:
cert-manager.io/cluster-issuer: "{{ vault_ingress_cert_issuer }}"
{% endif %}
{% endif %}
{% if vault_auto_unseal_type == "aws" %}
extraEnvironmentVars:
AWS_ACCESS_KEY_ID: "{{ vault_aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ vault_aws_secret_key }}"
{% elif vault_auto_unseal_type == "gcp" %}
extraVolumes:
- type: secret
name: vault-gcp-credentials
extraEnvironmentVars:
GOOGLE_APPLICATION_CREDENTIALS: "/vault/userconfig/vault-gcp-credentials/credentials.json"
{% elif vault_auto_unseal_type == "azure" %}
extraEnvironmentVars:
AZURE_TENANT_ID: "{{ vault_azure_tenant_id }}"
AZURE_CLIENT_ID: "{{ vault_azure_client_id }}"
AZURE_CLIENT_SECRET: "{{ vault_azure_client_secret }}"
{% endif %}
{% if vault_mode == "standalone" %}
standalone:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "file" {
path = "/vault/data"
}
{% if vault_auto_unseal_type == "aws" %}
seal "awskms" {
region = "{{ vault_aws_kms_region }}"
kms_key_id = "{{ vault_aws_kms_key_id }}"
}
{% elif vault_auto_unseal_type == "gcp" %}
seal "gcpckms" {
project = "{{ vault_gcp_project }}"
region = "{{ vault_gcp_region }}"
key_ring = "{{ vault_gcp_key_ring }}"
crypto_key = "{{ vault_gcp_crypto_key }}"
}
{% elif vault_auto_unseal_type == "azure" %}
seal "azurekeyvault" {
vault_name = "{{ vault_azure_keyvault_name }}"
key_name = "{{ vault_azure_key_name }}"
}
{% elif vault_auto_unseal_type == "transit" %}
seal "transit" {
address = "{{ vault_transit_address }}"
token = "{{ vault_transit_token }}"
key_name = "{{ vault_transit_key_name }}"
mount_path = "transit/"
disable_renewal = "false"
}
{% endif %}
ha:
enabled: false
{% endif %}
{% if vault_mode == "ha" %}
standalone:
enabled: false
ha:
enabled: true
replicas: {{ vault_ha_replicas }}
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
storage "raft" {
path = "/vault/data"
{% for i in range(vault_ha_replicas) %}
retry_join {
leader_api_addr = "http://vault-{{ i }}.vault-internal:8200"
}
{% endfor %}
}
service_registration "kubernetes" {}
{% if vault_auto_unseal_type == "aws" %}
seal "awskms" {
region = "{{ vault_aws_kms_region }}"
kms_key_id = "{{ vault_aws_kms_key_id }}"
}
{% elif vault_auto_unseal_type == "gcp" %}
seal "gcpckms" {
project = "{{ vault_gcp_project }}"
region = "{{ vault_gcp_region }}"
key_ring = "{{ vault_gcp_key_ring }}"
crypto_key = "{{ vault_gcp_crypto_key }}"
}
{% elif vault_auto_unseal_type == "azure" %}
seal "azurekeyvault" {
vault_name = "{{ vault_azure_keyvault_name }}"
key_name = "{{ vault_azure_key_name }}"
}
{% elif vault_auto_unseal_type == "transit" %}
seal "transit" {
address = "{{ vault_transit_address }}"
token = "{{ vault_transit_token }}"
key_name = "{{ vault_transit_key_name }}"
mount_path = "transit/"
disable_renewal = "false"
}
{% endif %}
{% endif %}
ui:
enabled: true
serviceType: ClusterIP

View File

@@ -33,6 +33,9 @@ addon_csi_s3: false # CSI S3 Driver (объектное хран
addon_csi_ceph: false # CSI Ceph / Rook-Ceph (distributed block + filesystem storage)
addon_csi_glusterfs: false # CSI GlusterFS Driver (требует внешний GlusterFS + Heketi)
addon_vaultwarden: false # Vaultwarden (self-hosted Bitwarden-совместимый менеджер паролей)
addon_smtp_relay: false # SMTP Relay (Postfix → Yandex/другой SMTP) — для уведомлений из подов
addon_vault: false # HashiCorp Vault (секреты, PKI, динамические credentials)
addon_external_secrets: false # External Secrets Operator → Vault/AWS/GCP (k8s Secret sync)
# ─── NFS Server ───────────────────────────────────────────────────────────────
nfs_exports:
@@ -287,6 +290,37 @@ minio_api_ingress_host: "s3.example.com"
# vaultwarden_smtp_security: "force_tls" # force_tls | starttls | off
# vaultwarden_smtp_username: "user@example.com"
# ─── SMTP Relay ───────────────────────────────────────────────────────────────
# Postfix relay для отправки почты из подов через внешний SMTP (Yandex/Gmail/etc).
# Пароль задаётся в vault.yml: vault_smtp_relay_password
# Использование: SMTP_HOST=smtp-relay.smtp-relay.svc.cluster.local, SMTP_PORT=25
# smtp_relay_host: "smtp.yandex.ru"
# smtp_relay_port: 465
# smtp_relay_username: "sergey@antropoff.ru"
# smtp_relay_from: "vault@antropoff.ru"
# smtp_relay_allowed_sender_domains: "antropoff.ru"
# smtp_relay_mynetworks: "10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 127.0.0.0/8"
# ─── HashiCorp Vault ──────────────────────────────────────────────────────────
# Менеджер секретов. README: addons/vault/README.md
# vault_mode: "standalone" # standalone (1 pod) | ha (3 pods Raft)
# vault_auto_unseal_type: "none" # none | k8s | aws | gcp | azure | transit
# k8s: ключи в Secret (homelab), aws/gcp/azure/transit (production)
# vault_injector_enabled: true # Vault Agent Injector (авто-инжекция в поды)
# vault_ingress_enabled: false
# vault_ingress_host: "vault-hc.example.com"
# vault_storage_size: "10Gi"
# Для AWS KMS unseal (vault.yml): vault_aws_kms_access_key, vault_aws_kms_secret_key
# ─── External Secrets Operator ────────────────────────────────────────────────
# Синхронизирует секреты из Vault/AWS/GCP в k8s Secrets. README: addons/external-secrets/README.md
# Требует предварительно созданного AppRole в Vault (шаги в README).
# external_secrets_vault_url: "http://vault.vault.svc.cluster.local:8200"
# external_secrets_vault_kv_path: "secret"
# external_secrets_vault_role_id: "" # после создания AppRole в Vault
# external_secrets_vault_store_name: "vault-backend"
# Пароль задаётся в vault.yml: vault_eso_approle_secret_id
# ─── etcd backup ──────────────────────────────────────────────────────────────
etcd_backup_dir: "{{ k3s_data_dir }}/server/db/snapshots"
etcd_backup_retention: 5 # сколько снимков хранить

View File

@@ -69,3 +69,22 @@ vault_vaultwarden_smtp_password: "fntwztnkacanpbwa" # пароль SMTP (Y
# ─── CSI GlusterFS / Heketi ────────────────────────────────────────────────────
vault_glusterfs_heketi_secret: "changeme-heketi" # пароль Heketi admin
# ─── SMTP Relay ────────────────────────────────────────────────────────────────
vault_smtp_relay_password: "fntwztnkacanpbwa" # Yandex App Password для sergey@antropoff.ru
# ─── HashiCorp Vault (auto-unseal: aws) ────────────────────────────────────────
# Используется только при vault_auto_unseal_type: aws
vault_aws_kms_access_key: "" # AWS IAM Access Key ID
vault_aws_kms_secret_key: "" # AWS IAM Secret Access Key
# Vault Transit Seal (vault_auto_unseal_type: transit)
vault_transit_seal_token: "" # token для доступа к transit engine другого Vault
# Azure Key Vault unseal (vault_auto_unseal_type: azure)
vault_azure_kv_client_secret: ""
# ─── External Secrets Operator → HashiCorp Vault ───────────────────────────────
# Получить после шага 6 в addons/external-secrets/README.md:
# vault write -f -field=secret_id auth/approle/role/eso-role/secret-id
vault_eso_approle_secret_id: "" # AppRole Secret ID для ESO → Vault

View File

@@ -231,3 +231,27 @@
when: addon_vaultwarden | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/vaultwarden/role"
- name: Install SMTP Relay
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_smtp_relay | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/smtp-relay/role"
- name: Install HashiCorp Vault
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_vault | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/vault/role"
- name: Install External Secrets Operator
hosts: k3s_master[0]
gather_facts: false
become: true
when: addon_external_secrets | default(false) | bool
roles:
- role: "{{ playbook_dir }}/../addons/external-secrets/role"