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

@@ -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
```