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:
288
addons/external-secrets/README.md
Normal file
288
addons/external-secrets/README.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user