Fail-fast secrets in stage/prod via EVENTHUB_ENV. Refs EventHub/EventHubBack#25

This commit is contained in:
2026-07-07 19:52:04 +03:00
parent 018bf85b87
commit cf8e6bcc07
10 changed files with 855 additions and 583 deletions
+21 -4
View File
@@ -1,4 +1,21 @@
# .env.example — закоммитьте этот файл, а реальный .env держите в .gitignore # Скопируйте в .env (файл .env в .gitignore). Для локальной разработки: EVENTHUB_ENV=dev
RELEASE_COOKIE=ваш-очень-длинный-секретный-куки
GRAFANA_ADMIN_PASSWORD=сложный-уникальный-пароль # Режим: dev | stage | prod (stage/prod — обязательные сильные секреты)
JWT_SECRET=суперсекрет EVENTHUB_ENV=dev
RELEASE_COOKIE=ваш-очень-длинный-секретный-куки-минимум-32-символа
GRAFANA_ADMIN_PASSWORD=сложный-уникальный-пароль
# JWT (минимум 32 символа, не из denylist — см. infra_secrets)
JWT_SECRET=замените-на-случайную-строку-минимум-32-символа-для-user-jwt
ADMIN_JWT_SECRET=замените-на-другую-случайную-строку-минимум-32-символа
# Seed админов (обязательны в stage/prod при пустой таблице admin; в dev — опционально)
ADMIN_SUPER_EMAIL=superadmin@eventhub.local
ADMIN_SUPER_PASSWORD=замените-сильный-пароль-суперадмина
ADMIN_EMAIL=admin@eventhub.local
ADMIN_PASSWORD=замените-сильный-пароль-админа
ADMIN_MODER_EMAIL=moderator@eventhub.local
ADMIN_MODER_PASSWORD=замените-сильный-пароль-модератора
ADMIN_SUPPORT_EMAIL=support@eventhub.local
ADMIN_SUPPORT_PASSWORD=замените-сильный-пароль-поддержки
+14 -12
View File
@@ -1,12 +1,14 @@
RELEASE_COOKIE=FuNHSU3SIiW8ICu2XIbl0pvpxAhpBvO2Wki7CT6q2bpHDSlQ8p7gtUgjnyXpPJU5 EVENTHUB_ENV=stage
GRAFANA_ADMIN_PASSWORD=admin RELEASE_COOKIE=FuNHSU3SIiW8ICu2XIbl0pvpxAhpBvO2Wki7CT6q2bpHDSlQ8p7gtUgjnyXpPJU5
JWT_SECRET=IDhKNFRLZFDYgPvVXwg4T7hAviQ2I1VGeEn5vXufQXK3AAErZuhMWy8J1uxJ0kGF GRAFANA_ADMIN_PASSWORD=замените-сильный-пароль-grafana
TRAEFIK_LOG_PATH=/app/traefik/logs/access.log JWT_SECRET=IDhKNFRLZFDYgPvVXwg4T7hAviQ2I1VGeEn5vXufQXK3AAErZuhMWy8J1uxJ0kGF
ADMIN_SUPER_EMAIL=superadmin@eventhub.local ADMIN_JWT_SECRET=K3AAErZuhMWy8J1uxJ0kGFIDhKNFRLZFDYgPvVXwg4T7hAviQ2I1VGeEn5vXufQ
ADMIN_SUPER_PASSWORD=123456 TRAEFIK_LOG_PATH=/app/traefik/logs/access.log
ADMIN_EMAIL=admin@eventhub.local ADMIN_SUPER_EMAIL=superadmin@eventhub.local
ADMIN_PASSWORD=123456 ADMIN_SUPER_PASSWORD=замените-сильный-пароль-суперадмина-мин32
ADMIN_MODER_EMAIL=moderator@eventhub.local ADMIN_EMAIL=admin@eventhub.local
ADMIN_MODER_PASSWORD=123456 ADMIN_PASSWORD=замените-сильный-пароль-админа-минимум32
ADMIN_SUPPORT_EMAIL=support@eventhub.local ADMIN_MODER_EMAIL=moderator@eventhub.local
ADMIN_SUPPORT_PASSWORD=123456 ADMIN_MODER_PASSWORD=замените-сильный-пароль-модератора-мин32
ADMIN_SUPPORT_EMAIL=support@eventhub.local
ADMIN_SUPPORT_PASSWORD=замените-сильный-пароль-поддержки-мин32
+86 -80
View File
@@ -1,81 +1,87 @@
version: "3.8" version: "3.8"
services: services:
# ================== Балансировщик ================== # ================== Балансировщик ==================
traefik: traefik:
image: traefik:latest image: traefik:latest
command: command:
- "--log.level=DEBUG" - "--log.level=DEBUG"
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
ports: ports:
- "80:80" - "80:80"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
networks: networks:
- eventhub-net - eventhub-net
deploy: deploy:
replicas: 1 replicas: 1
restart_policy: restart_policy:
condition: any condition: any
# ================== Кластер EventHub ================== # ================== Кластер EventHub ==================
eventhub: eventhub:
image: git.sabilin.com/eventhub/eventhub:stage image: git.sabilin.com/eventhub/eventhub:stage
hostname: "eventhub-node{{.Task.Slot}}" hostname: "eventhub-node{{.Task.Slot}}"
environment: environment:
- NODE_NAME=eventhub-node{{.Task.Slot}} - EVENTHUB_ENV=stage
- RELEASE_COOKIE=${RELEASE_COOKIE} - NODE_NAME=eventhub-node{{.Task.Slot}}
- JWT_SECRET=${JWT_SECRET} - RELEASE_COOKIE=${RELEASE_COOKIE}
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL} - JWT_SECRET=${JWT_SECRET}
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD} - ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
- ADMIN_EMAIL=${ADMIN_EMAIL} - ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL}
- ADMIN_PASSWORD=${ADMIN_PASSWORD} - ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD}
- CLUSTER_MODE=true - ADMIN_EMAIL=${ADMIN_EMAIL}
- DNS_NAME=eventhub-node - ADMIN_PASSWORD=${ADMIN_PASSWORD}
networks: - ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL}
eventhub-net: - ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD}
aliases: - ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL}
- eventhub-node - ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD}
volumes: - CLUSTER_MODE=true
- eventhub-data:/app/data - DNS_NAME=eventhub-node
healthcheck: networks:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"] eventhub-net:
interval: 10s aliases:
timeout: 5s - eventhub-node
retries: 3 volumes:
start_period: 30s - eventhub-data:/app/data
deploy: healthcheck:
replicas: 1 test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
endpoint_mode: dnsrr interval: 10s
restart_policy: timeout: 5s
condition: any retries: 3
start_period: 30s
# ================== Эмулятор ботов ================== deploy:
bot-emulator-users: replicas: 1
image: git.sabilin.com/eventhub/bot-emulator-users:stage endpoint_mode: dnsrr
environment: restart_policy:
- ADMIN_API_HOST=http://eventhub-node:8445 condition: any
- CLIENT_API_HOST=http://eventhub-node:8080
- ADMIN_EMAIL=${ADMIN_EMAIL} # ================== Эмулятор ботов ==================
- ADMIN_PASSWORD=${ADMIN_PASSWORD} bot-emulator-users:
- BOT_PASSWORD=${BOT_PASSWORD} image: git.sabilin.com/eventhub/bot-emulator-users:stage
- STARTUP_DELAY=30 environment:
- DEBUG=true - ADMIN_API_HOST=http://eventhub-node:8445
networks: - CLIENT_API_HOST=http://eventhub-node:8080
- eventhub-net - ADMIN_EMAIL=${ADMIN_EMAIL}
depends_on: - ADMIN_PASSWORD=${ADMIN_PASSWORD}
- eventhub - BOT_PASSWORD=${BOT_PASSWORD}
deploy: - STARTUP_DELAY=30
replicas: 0 - DEBUG=true
restart_policy: networks:
condition: any - eventhub-net
depends_on:
networks: - eventhub
eventhub-net: deploy:
driver: overlay replicas: 0
restart_policy:
volumes: condition: any
networks:
eventhub-net:
driver: overlay
volumes:
eventhub-data: eventhub-data:
+229 -225
View File
@@ -1,226 +1,230 @@
# docker/docker-compose.swarm.yml # docker/docker-compose.swarm.yml
version: "3.8" version: "3.8"
services: services:
# ================== Балансировщик ================== # ================== Балансировщик ==================
traefik: traefik:
image: traefik:latest image: traefik:latest
command: command:
# - "--log.level=DEBUG" # - "--log.level=DEBUG"
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
# - "--providers.swarm.endpoint=unix:///var/run/docker.sock" # провайдер Swarm # - "--providers.swarm.endpoint=unix:///var/run/docker.sock" # провайдер Swarm
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--providers.file.filename=/etc/traefik/dynamic_conf.yml" - "--providers.file.filename=/etc/traefik/dynamic_conf.yml"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443" - "--entrypoints.websecure.address=:443"
- "--metrics.prometheus=true" - "--metrics.prometheus=true"
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0" - "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
- "--metrics.prometheus.addEntryPointsLabels=true" - "--metrics.prometheus.addEntryPointsLabels=true"
- "--metrics.prometheus.addServicesLabels=true" - "--metrics.prometheus.addServicesLabels=true"
- "--accesslog=true" - "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log" - "--accesslog.filepath=/var/log/traefik/access.log"
- "--accesslog.format=json" - "--accesslog.format=json"
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik" - "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
- "--experimental.plugins.coraza.version=v0.3.0" - "--experimental.plugins.coraza.version=v0.3.0"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
- "8080:8080" - "8080:8080"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/certs:/etc/traefik/certs:ro" - "./traefik/certs:/etc/traefik/certs:ro"
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro" - "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
- "traefik-logs:/var/log/traefik" - "traefik-logs:/var/log/traefik"
- "traefik-plugins:/plugins-storage" - "traefik-plugins:/plugins-storage"
networks: networks:
- eventhub-net - eventhub-net
deploy: deploy:
replicas: 1 replicas: 1
restart_policy: restart_policy:
condition: any condition: any
# ================== Сервис заглушка ================== # ================== Сервис заглушка ==================
fallback: fallback:
image: fallback:latest image: fallback:latest
networks: networks:
- eventhub-net - eventhub-net
deploy: deploy:
replicas: 0 replicas: 0
restart_policy: restart_policy:
condition: any condition: any
# ================== Кластер EventHub ================== # ================== Кластер EventHub ==================
eventhub: eventhub:
image: eventhub:latest image: eventhub:latest
hostname: "eventhub-node{{.Task.Slot}}" hostname: "eventhub-node{{.Task.Slot}}"
environment: environment:
- NODE_NAME=eventhub-node{{.Task.Slot}} - EVENTHUB_ENV=prod
- RELEASE_COOKIE=${RELEASE_COOKIE:-eventhub_cookie} - NODE_NAME=eventhub-node{{.Task.Slot}}
- JWT_SECRET=${JWT_SECRET:-eventhub_top_secret} - RELEASE_COOKIE=${RELEASE_COOKIE}
- ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL:-superadmin@eventhub.local} - JWT_SECRET=${JWT_SECRET}
- ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD:-123456} - ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
- ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL:-moderator@eventhub.local} - ADMIN_SUPER_EMAIL=${ADMIN_SUPER_EMAIL}
- ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD:-123456} - ADMIN_SUPER_PASSWORD=${ADMIN_SUPER_PASSWORD}
- ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL:-support@eventhub.local} - ADMIN_EMAIL=${ADMIN_EMAIL}
- ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD:-123456} - ADMIN_PASSWORD=${ADMIN_PASSWORD}
- CLUSTER_MODE=true - ADMIN_MODER_EMAIL=${ADMIN_MODER_EMAIL}
- DNS_NAME=eventhub-node - ADMIN_MODER_PASSWORD=${ADMIN_MODER_PASSWORD}
networks: - ADMIN_SUPPORT_EMAIL=${ADMIN_SUPPORT_EMAIL}
eventhub-net: - ADMIN_SUPPORT_PASSWORD=${ADMIN_SUPPORT_PASSWORD}
aliases: - CLUSTER_MODE=true
- eventhub-node - DNS_NAME=eventhub-node
volumes: networks:
- type: volume eventhub-net:
source: eventhub-data aliases:
target: /app/data - eventhub-node
deploy: volumes:
replicas: 1 - type: volume
endpoint_mode: dnsrr source: eventhub-data
restart_policy: target: /app/data
condition: any deploy:
labels: replicas: 1
- "traefik.enable=true" endpoint_mode: dnsrr
restart_policy:
# ================== Admin UI ================== condition: any
admin-ui: labels:
image: eventhub-admin-ui:latest - "traefik.enable=true"
networks:
- eventhub-net # ================== Admin UI ==================
depends_on: admin-ui:
- eventhub image: eventhub-admin-ui:latest
deploy: networks:
replicas: 1 - eventhub-net
restart_policy: depends_on:
condition: any - eventhub
labels: deploy:
- "traefik.enable=true" replicas: 1
restart_policy:
# ================== Мониторинг ================== condition: any
prometheus: labels:
image: prom/prometheus:latest - "traefik.enable=true"
command:
- '--config.file=/etc/prometheus/prometheus.yml' # ================== Мониторинг ==================
- '--storage.tsdb.path=/prometheus' prometheus:
- '--web.console.libraries=/usr/share/prometheus/console_libraries' image: prom/prometheus:latest
- '--web.console.templates=/usr/share/prometheus/consoles' command:
- '--storage.tsdb.retention.time=30d' - '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.size=15GB' - '--storage.tsdb.path=/prometheus'
volumes: - '--web.console.libraries=/usr/share/prometheus/console_libraries'
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - '--web.console.templates=/usr/share/prometheus/consoles'
- prometheus-data:/prometheus - '--storage.tsdb.retention.time=30d'
networks: - '--storage.tsdb.retention.size=15GB'
- eventhub-net volumes:
ports: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- "9090:9090" - prometheus-data:/prometheus
deploy: networks:
replicas: 0 - eventhub-net
restart_policy: ports:
condition: any - "9090:9090"
deploy:
grafana: replicas: 0
image: grafana/grafana:latest restart_policy:
environment: condition: any
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false grafana:
- GF_USERS_ALLOW_SIGN_UP=false image: grafana/grafana:latest
- GF_AUTH_ANONYMOUS_ENABLED=false environment:
volumes: - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- ./grafana/provisioning:/etc/grafana/provisioning - GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false
- ./grafana/dashboards:/etc/grafana/dashboards - GF_USERS_ALLOW_SIGN_UP=false
- grafana-data:/var/lib/grafana - GF_AUTH_ANONYMOUS_ENABLED=false
networks: volumes:
- eventhub-net - ./grafana/provisioning:/etc/grafana/provisioning
ports: - ./grafana/dashboards:/etc/grafana/dashboards
- "3000:3000" - grafana-data:/var/lib/grafana
deploy: networks:
replicas: 0 - eventhub-net
restart_policy: ports:
condition: any - "3000:3000"
deploy:
# ================== Аналитика логов ================== replicas: 0
loglynx: restart_policy:
image: k0lin/loglynx:latest condition: any
user: root
ports: # ================== Аналитика логов ==================
- "6123:6123" loglynx:
volumes: image: k0lin/loglynx:latest
- traefik-logs:/app/traefik/logs:ro user: root
- loglynx-data:/app/data ports:
environment: - "6123:6123"
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH} volumes:
- SERVER_PORT=6123 - traefik-logs:/app/traefik/logs:ro
- DATABASE_PATH=/app/data/loglynx.db - loglynx-data:/app/data
networks: environment:
- eventhub-net - TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH}
deploy: - SERVER_PORT=6123
replicas: 0 - DATABASE_PATH=/app/data/loglynx.db
restart_policy: networks:
condition: any - eventhub-net
deploy:
# ================== Инструмент отладки ================== replicas: 0
observer_web: restart_policy:
image: observer_web:latest condition: any
environment:
- RELEASE_COOKIE=${RELEASE_COOKIE} # ================== Инструмент отладки ==================
networks: observer_web:
- eventhub-net image: observer_web:latest
ports: environment:
- "4000:4000" - RELEASE_COOKIE=${RELEASE_COOKIE}
deploy: networks:
replicas: 0 - eventhub-net
restart_policy: ports:
condition: any - "4000:4000"
deploy:
# ================== Ротация логов ================== replicas: 0
logrotate: restart_policy:
image: logrotate:latest condition: any
volumes:
- traefik-logs:/var/log/traefik:rw # ================== Ротация логов ==================
networks: logrotate:
- eventhub-net image: logrotate:latest
deploy: volumes:
replicas: 0 - traefik-logs:/var/log/traefik:rw
restart_policy: networks:
condition: any - eventhub-net
deploy:
bot-emulator-users: replicas: 0
image: bot-emulator-users:latest restart_policy:
environment: condition: any
- ADMIN_API_HOST=http://eventhub-node:8445
- CLIENT_API_HOST=http://eventhub-node:8080 bot-emulator-users:
- ADMIN_EMAIL=admin@eventhub.local image: bot-emulator-users:latest
- ADMIN_PASSWORD=123456 environment:
- BOT_PASSWORD=botpass123 - ADMIN_API_HOST=http://eventhub-node:8445
- MIN_DELAY=0.5 - CLIENT_API_HOST=http://eventhub-node:8080
- MAX_DELAY=3.0 - ADMIN_EMAIL=admin@eventhub.local
- LOOP_FOREVER=true - ADMIN_PASSWORD=123456
- BOT_REFRESH_INTERVAL=300 - BOT_PASSWORD=botpass123
- TICKET_CHANCE=0.07 - MIN_DELAY=0.5
- STARTUP_DELAY=30 - MAX_DELAY=3.0
- DEBUG=false - LOOP_FOREVER=true
networks: - BOT_REFRESH_INTERVAL=300
- eventhub-net - TICKET_CHANCE=0.07
depends_on: - STARTUP_DELAY=30
- eventhub - DEBUG=false
deploy: networks:
mode: replicated - eventhub-net
replicas: 1 depends_on:
restart_policy: - eventhub
condition: any deploy:
delay: 5s mode: replicated
max_attempts: 3 replicas: 1
window: 120s restart_policy:
condition: any
networks: delay: 5s
eventhub-net: max_attempts: 3
driver: overlay window: 120s
volumes: networks:
eventhub-data: eventhub-net:
# name: 'eventhub-data-{{.Task.Slot}}' driver: overlay
prometheus-data:
grafana-data: volumes:
traefik-logs: eventhub-data:
loglynx-data: # name: 'eventhub-data-{{.Task.Slot}}'
prometheus-data:
grafana-data:
traefik-logs:
loglynx-data:
traefik-plugins: traefik-plugins:
+218 -212
View File
@@ -1,213 +1,219 @@
# docker/docker-compose.yml # docker/docker-compose.yml
services: services:
# ================== Балансировщик (HTTPS/WSS, WAF, логи) ================== # ================== Балансировщик (HTTPS/WSS, WAF, логи) ==================
traefik: traefik:
image: traefik:latest image: traefik:latest
user: "0:1001" # группа docker на хосте user: "0:1001" # группа docker на хосте
command: command:
- "--api.insecure=true" - "--api.insecure=true"
- "--providers.docker=true" - "--providers.docker=true"
- "--providers.docker.exposedbydefault=false" - "--providers.docker.exposedbydefault=false"
- "--providers.file.filename=/etc/traefik/dynamic_conf.yml" - "--providers.file.filename=/etc/traefik/dynamic_conf.yml"
- "--entrypoints.web.address=:80" - "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443" - "--entrypoints.websecure.address=:443"
# Метрики # Метрики
- "--metrics.prometheus=true" - "--metrics.prometheus=true"
- "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0" - "--metrics.prometheus.buckets=0.1,0.3,1.2,5.0"
- "--metrics.prometheus.addEntryPointsLabels=true" - "--metrics.prometheus.addEntryPointsLabels=true"
- "--metrics.prometheus.addServicesLabels=true" - "--metrics.prometheus.addServicesLabels=true"
# Логи доступа (JSON) # Логи доступа (JSON)
- "--accesslog=true" - "--accesslog=true"
- "--accesslog.filepath=/var/log/traefik/access.log" - "--accesslog.filepath=/var/log/traefik/access.log"
- "--accesslog.format=json" - "--accesslog.format=json"
# Coraza WAF # Coraza WAF
- "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik" - "--experimental.plugins.coraza.modulename=github.com/jcchavezs/coraza-http-wasm-traefik"
- "--experimental.plugins.coraza.version=v0.2.0" - "--experimental.plugins.coraza.version=v0.2.0"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
# порт дашборда (опционально) # порт дашборда (опционально)
- "8080:8080" - "8080:8080"
volumes: volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro" - "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./traefik/certs:/etc/traefik/certs:ro" - "./traefik/certs:/etc/traefik/certs:ro"
- "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro" - "./traefik/dynamic_conf.yml:/etc/traefik/dynamic_conf.yml:ro"
- "traefik-logs:/var/log/traefik" # для LogLynx - "traefik-logs:/var/log/traefik" # для LogLynx
networks: networks:
- eventhub-net - eventhub-net
restart: unless-stopped restart: unless-stopped
# ================== Сервис-заглушка (Failover) ================== # ================== Сервис-заглушка (Failover) ==================
fallback: fallback:
build: build:
context: ./fallback context: ./fallback
dockerfile: Dockerfile dockerfile: Dockerfile
networks: networks:
- eventhub-net - eventhub-net
restart: unless-stopped restart: unless-stopped
profiles: ['ci'] profiles: ['ci']
# ================== Кластер EventHub (3 ноды) ================== # ================== Кластер EventHub (3 ноды) ==================
eventhub-node1: eventhub-node1:
build: build:
context: .. context: ..
dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
hostname: eventhub-node1.local hostname: eventhub-node1.local
environment: environment:
- NODE_NAME=eventhub-node1@eventhub-node1.local - EVENTHUB_ENV=dev
- RELEASE_COOKIE=${RELEASE_COOKIE} - NODE_NAME=eventhub-node1@eventhub-node1.local
- JWT_SECRET=${JWT_SECRET} - RELEASE_COOKIE=${RELEASE_COOKIE}
networks: - JWT_SECRET=${JWT_SECRET}
eventhub-net: - ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
aliases: networks:
- eventhub-node # ← общий алиас для DNS-лукапа eventhub-net:
volumes: aliases:
- eventhub-node1-data:/app/data - eventhub-node # ← общий алиас для DNS-лукапа
labels: volumes:
- "traefik.enable=true" - eventhub-node1-data:/app/data
restart: unless-stopped labels:
- "traefik.enable=true"
eventhub-node2: restart: unless-stopped
build:
context: .. eventhub-node2:
dockerfile: docker/Dockerfile build:
hostname: eventhub-node2.local context: ..
environment: dockerfile: docker/Dockerfile
- NODE_NAME=eventhub-node2@eventhub-node2.local hostname: eventhub-node2.local
- RELEASE_COOKIE=${RELEASE_COOKIE} environment:
- JWT_SECRET=${JWT_SECRET} - EVENTHUB_ENV=dev
networks: - NODE_NAME=eventhub-node2@eventhub-node2.local
eventhub-net: - RELEASE_COOKIE=${RELEASE_COOKIE}
aliases: - JWT_SECRET=${JWT_SECRET}
- eventhub-node # ← общий алиас для DNS-лукапа - ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
volumes: networks:
- eventhub-node2-data:/app/data eventhub-net:
labels: aliases:
- "traefik.enable=true" - eventhub-node # ← общий алиас для DNS-лукапа
restart: unless-stopped volumes:
- eventhub-node2-data:/app/data
eventhub-node3: labels:
build: - "traefik.enable=true"
context: .. restart: unless-stopped
dockerfile: docker/Dockerfile
hostname: eventhub-node3.local eventhub-node3:
environment: build:
- NODE_NAME=eventhub-node3@eventhub-node3.local context: ..
- RELEASE_COOKIE=${RELEASE_COOKIE} dockerfile: docker/Dockerfile
- JWT_SECRET=${JWT_SECRET} hostname: eventhub-node3.local
networks: environment:
eventhub-net: - EVENTHUB_ENV=dev
aliases: - NODE_NAME=eventhub-node3@eventhub-node3.local
- eventhub-node # ← общий алиас для DNS-лукапа - RELEASE_COOKIE=${RELEASE_COOKIE}
volumes: - JWT_SECRET=${JWT_SECRET}
- eventhub-node3-data:/app/data - ADMIN_JWT_SECRET=${ADMIN_JWT_SECRET}
labels: networks:
- "traefik.enable=true" eventhub-net:
restart: unless-stopped aliases:
- eventhub-node # ← общий алиас для DNS-лукапа
# ================== Admin ui ================== volumes:
admin-ui: - eventhub-node3-data:/app/data
build: labels:
context: ../../EventHubFrontAdmin # путь к проекту админки - "traefik.enable=true"
dockerfile: Dockerfile restart: unless-stopped
networks:
- eventhub-net # ================== Admin ui ==================
# restart: unless-stopped admin-ui:
restart: no build:
labels: context: ../../EventHubFrontAdmin # путь к проекту админки
- "traefik.enable=true" dockerfile: Dockerfile
profiles: ['ui'] networks:
# ================== Мониторинг ================== - eventhub-net
prometheus: # restart: unless-stopped
image: prom/prometheus:latest restart: no
command: labels:
- '--config.file=/etc/prometheus/prometheus.yml' - "traefik.enable=true"
- '--storage.tsdb.path=/prometheus' profiles: ['ui']
- '--web.console.libraries=/usr/share/prometheus/console_libraries' # ================== Мониторинг ==================
- '--web.console.templates=/usr/share/prometheus/consoles' prometheus:
- '--storage.tsdb.retention.time=30d' image: prom/prometheus:latest
- '--storage.tsdb.retention.size=15GB' command:
volumes: - '--config.file=/etc/prometheus/prometheus.yml'
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - '--storage.tsdb.path=/prometheus'
- prometheus-data:/prometheus - '--web.console.libraries=/usr/share/prometheus/console_libraries'
networks: - '--web.console.templates=/usr/share/prometheus/consoles'
- eventhub-net - '--storage.tsdb.retention.time=30d'
ports: - '--storage.tsdb.retention.size=15GB'
- "9090:9090" volumes:
restart: unless-stopped - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
profiles: ['ci'] - prometheus-data:/prometheus
networks:
grafana: - eventhub-net
image: grafana/grafana:latest ports:
environment: - "9090:9090"
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} restart: unless-stopped
- GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false profiles: ['ci']
- GF_USERS_ALLOW_SIGN_UP=false
- GF_AUTH_ANONYMOUS_ENABLED=false grafana:
volumes: image: grafana/grafana:latest
- ./grafana/provisioning:/etc/grafana/provisioning environment:
- ./grafana/dashboards:/etc/grafana/dashboards - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
- grafana-data:/var/lib/grafana - GF_SECURITY_DISABLE_INITIAL_ADMIN_PASSWORD_CHANGE=false
networks: - GF_USERS_ALLOW_SIGN_UP=false
- eventhub-net - GF_AUTH_ANONYMOUS_ENABLED=false
ports: volumes:
- "3000:3000" - ./grafana/provisioning:/etc/grafana/provisioning
restart: unless-stopped - ./grafana/dashboards:/etc/grafana/dashboards
profiles: ['ci'] - grafana-data:/var/lib/grafana
networks:
# ================== Аналитика логов ================== - eventhub-net
loglynx: ports:
image: k0lin/loglynx:latest - "3000:3000"
user: root restart: unless-stopped
restart: unless-stopped profiles: ['ci']
ports:
- "6123:6123" # ================== Аналитика логов ==================
volumes: loglynx:
- traefik-logs:/app/traefik/logs:ro image: k0lin/loglynx:latest
- loglynx-data:/app/data user: root
environment: restart: unless-stopped
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH} ports:
- SERVER_PORT=6123 - "6123:6123"
- DATABASE_PATH=/app/data/loglynx.db volumes:
networks: - traefik-logs:/app/traefik/logs:ro
- eventhub-net - loglynx-data:/app/data
profiles: ['ci'] environment:
- TRAEFIK_LOG_PATH=${TRAEFIK_LOG_PATH}
# ================== Инструмент отладки ================== - SERVER_PORT=6123
observer_web: - DATABASE_PATH=/app/data/loglynx.db
build: networks:
context: .. - eventhub-net
dockerfile: docker/ObserverWeb.Dockerfile profiles: ['ci']
environment:
- RELEASE_COOKIE=${RELEASE_COOKIE} # ================== Инструмент отладки ==================
networks: observer_web:
- eventhub-net build:
ports: context: ..
- "4000:4000" dockerfile: docker/ObserverWeb.Dockerfile
restart: unless-stopped environment:
profiles: ['ci'] - RELEASE_COOKIE=${RELEASE_COOKIE}
networks:
# ================== Ротация логов Traefik ================== - eventhub-net
logrotate: ports:
build: - "4000:4000"
context: ./logrotate restart: unless-stopped
dockerfile: Dockerfile profiles: ['ci']
volumes:
- traefik-logs:/var/log/traefik:rw # ================== Ротация логов Traefik ==================
networks: logrotate:
- eventhub-net build:
restart: unless-stopped context: ./logrotate
profiles: ['ci'] dockerfile: Dockerfile
volumes:
networks: - traefik-logs:/var/log/traefik:rw
eventhub-net: networks:
driver: bridge - eventhub-net
restart: unless-stopped
volumes: profiles: ['ci']
eventhub-node1-data:
eventhub-node2-data: networks:
eventhub-node3-data: eventhub-net:
prometheus-data: driver: bridge
grafana-data:
traefik-logs: volumes:
eventhub-node1-data:
eventhub-node2-data:
eventhub-node3-data:
prometheus-data:
grafana-data:
traefik-logs:
loglynx-data: loglynx-data:
+23 -22
View File
@@ -1,23 +1,24 @@
[ [
{eventhub, [ {eventhub, [
{http_port, "${HTTP_PORT:-8080}"}, {http_port, "${HTTP_PORT:-8080}"},
{ws_port, "${WS_PORT:-8081}"}, {ws_port, "${WS_PORT:-8081}"},
{admin_http_port, "${ADMIN_HTTP_PORT:-8445}"}, {admin_http_port, "${ADMIN_HTTP_PORT:-8445}"},
{admin_ws_port, "${ADMIN_WS_PORT:-8446}"}, {admin_ws_port, "${ADMIN_WS_PORT:-8446}"},
{jwt_secret, <<"${JWT_SECRET:-change_me_in_production}">>} {jwt_secret, <<"${JWT_SECRET:-change_me_in_production}">>},
]}, {admin_jwt_secret, <<"${ADMIN_JWT_SECRET:-change_me_in_production}">>}
{kernel, [ ]},
{logger_level, info}, {kernel, [
{logger, [ {logger_level, info},
{handler, default, logger_std_h, {logger, [
#{formatter => {logger_formatter, #{template => [time, " ", level, ": ", msg, "\n"]}}} {handler, default, logger_std_h,
} #{formatter => {logger_formatter, #{template => [time, " ", level, ": ", msg, "\n"]}}}
]} }
]}, ]}
{mnesia, [ ]},
{dir, "data/Mnesia.eventhub@${NODE_NAME}"} {mnesia, [
]}, {dir, "data/Mnesia.eventhub@${NODE_NAME}"}
{ cowboy_swagger, [ ]},
{ static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" } { cowboy_swagger, [
]} { static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" }
]}
]. ].
+55 -26
View File
@@ -3,6 +3,15 @@
-export([start/2, stop/1]). -export([start/2, stop/1]).
start(_StartType, _StartArgs) -> start(_StartType, _StartArgs) ->
case infra_secrets:validate_startup() of
ok ->
start_application();
{error, Reason} ->
io:format("~nFATAL: secret validation failed: ~p~n", [Reason]),
{error, Reason}
end.
start_application() ->
case infra_sup:start_link() of case infra_sup:start_link() of
{ok, Pid} -> {ok, Pid} ->
% Определяем список узлов кластера, если режим CLUSTER_MODE=true % Определяем список узлов кластера, если режим CLUSTER_MODE=true
@@ -48,8 +57,10 @@ start(_StartType, _StartArgs) ->
start_swagger_http(), % Swagger UI и спецификация (8447) start_swagger_http(), % Swagger UI и спецификация (8447)
application:ensure_all_started(prometheus), application:ensure_all_started(prometheus),
application:ensure_all_started(prometheus_cowboy), application:ensure_all_started(prometheus_cowboy),
init_default_admins(), case init_default_admins() of
{ok, Pid}; ok -> {ok, Pid};
{error, Reason} -> {error, Reason}
end;
Error -> Error ->
Error Error
end. end.
@@ -199,31 +210,49 @@ start_swagger_http() ->
init_default_admins() -> init_default_admins() ->
case core_admin:list_all() of case core_admin:list_all() of
[] -> [] ->
% Суперадмин seed_default_admins();
SuperEmail = list_to_binary(os:getenv("ADMIN_SUPER_EMAIL", "superadmin@eventhub.local")),
SuperPass = list_to_binary(os:getenv("ADMIN_SUPER_PASSWORD", "123456")),
{ok, _} = core_admin:create(SuperEmail, SuperPass, superadmin),
io:format("Default superadmin created: ~s~n", [SuperEmail]),
% Админ
AdminEmail = list_to_binary(os:getenv("ADMIN_EMAIL", "admin@eventhub.local")),
AdminPass = list_to_binary(os:getenv("ADMIN_PASSWORD", "123456")),
{ok, _} = core_admin:create(AdminEmail, AdminPass, admin),
io:format("Default admin created: ~s~n", [AdminEmail]),
% Модератор
ModerEmail = list_to_binary(os:getenv("ADMIN_MODER_EMAIL", "moderator@eventhub.local")),
ModerPass = list_to_binary(os:getenv("ADMIN_MODER_PASSWORD", "123456")),
{ok, _} = core_admin:create(ModerEmail, ModerPass, moderator),
io:format("Default moderator created: ~s~n", [ModerEmail]),
% Поддержка
SupportEmail = list_to_binary(os:getenv("ADMIN_SUPPORT_EMAIL", "support@eventhub.local")),
SupportPass = list_to_binary(os:getenv("ADMIN_SUPPORT_PASSWORD", "123456")),
{ok, _} = core_admin:create(SupportEmail, SupportPass, support),
io:format("Default support created: ~s~n", [SupportEmail]);
_ -> _ ->
io:format("Admins already exist. Skipping creation.~n") io:format("Admins already exist. Skipping creation.~n"),
ok
end.
seed_default_admins() ->
Admins = [
{superadmin, "ADMIN_SUPER_EMAIL", "superadmin@eventhub.local",
"ADMIN_SUPER_PASSWORD", "123456"},
{admin, "ADMIN_EMAIL", "admin@eventhub.local",
"ADMIN_PASSWORD", "123456"},
{moderator, "ADMIN_MODER_EMAIL", "moderator@eventhub.local",
"ADMIN_MODER_PASSWORD", "123456"},
{support, "ADMIN_SUPPORT_EMAIL", "support@eventhub.local",
"ADMIN_SUPPORT_PASSWORD", "123456"}
],
lists:foldl(fun seed_admin/2, ok, Admins).
seed_admin({Role, EmailKey, DefaultEmail, PassKey, DefaultPass}, ok) ->
case admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) of
{error, _} = Err -> Err;
{ok, {Email, Pass}} ->
{ok, _} = core_admin:create(Email, Pass, Role),
io:format("Default ~p created: ~s~n", [Role, Email]),
ok
end;
seed_admin(_, Err) -> Err.
admin_credentials(EmailKey, DefaultEmail, PassKey, DefaultPass) ->
case infra_secrets:is_dev() of
true ->
{ok, {list_to_binary(os:getenv(EmailKey, DefaultEmail)),
list_to_binary(os:getenv(PassKey, DefaultPass))}};
false ->
case infra_secrets:require_admin_seed_email(EmailKey) of
{error, _} = Err -> Err;
{ok, Email} ->
case infra_secrets:require_admin_seed_password(PassKey) of
{error, _} = Err -> Err;
{ok, Pass} -> {ok, {Email, Pass}}
end
end
end. end.
get_env_int(Key, Default) -> get_env_int(Key, Default) ->
+10 -2
View File
@@ -24,7 +24,11 @@ get_user_secret() ->
get_user_secret_from_env() -> get_user_secret_from_env() ->
case os:getenv("JWT_SECRET") of case os:getenv("JWT_SECRET") of
false -> <<"user-secret-key-32-bytes-minimum!">>; false ->
case infra_secrets:is_dev() of
true -> <<"user-secret-key-32-bytes-minimum!">>;
false -> error({missing_secret, jwt_secret})
end;
S -> list_to_binary(S) S -> list_to_binary(S)
end. end.
@@ -37,7 +41,11 @@ get_admin_secret() ->
get_admin_secret_from_env() -> get_admin_secret_from_env() ->
case os:getenv("ADMIN_JWT_SECRET") of case os:getenv("ADMIN_JWT_SECRET") of
false -> <<"admin-secret-key-32-bytes-minimum!">>; false ->
case infra_secrets:is_dev() of
true -> <<"admin-secret-key-32-bytes-minimum!">>;
false -> error({missing_secret, admin_jwt_secret})
end;
S -> list_to_binary(S) S -> list_to_binary(S)
end. end.
+137
View File
@@ -0,0 +1,137 @@
%%%-------------------------------------------------------------------
%%% @doc Валидация секретов и паролей при старте (EVENTHUB_ENV).
%%% dev — допускаются documented fallbacks; stage/prod — fail-fast.
%%% @end
%%%-------------------------------------------------------------------
-module(infra_secrets).
-export([mode/0, is_dev/0, validate_startup/0,
require_admin_seed_password/1, require_admin_seed_email/1]).
-define(MIN_SECRET_LEN, 32).
%%%-------------------------------------------------------------------
%%% API
%%%-------------------------------------------------------------------
-spec mode() -> dev | stage | prod.
mode() ->
case os:getenv("EVENTHUB_ENV") of
false -> dev;
Env ->
case string:lowercase(Env) of
"dev" -> dev;
"stage" -> stage;
"prod" -> prod;
"production" -> prod;
_ -> stage
end
end.
-spec is_dev() -> boolean().
is_dev() -> mode() =:= dev.
%% @doc Проверка JWT-секретов до поднятия Mnesia/HTTP.
-spec validate_startup() -> ok | {error, {missing_secret, atom()} | {weak_secret, atom()}}.
validate_startup() ->
case mode() of
dev ->
dev_warn_if_weak(jwt_secret, effective_jwt_secret()),
dev_warn_if_weak(admin_jwt_secret, effective_admin_jwt_secret()),
ok;
_ ->
case require_strong_secret(jwt_secret, effective_jwt_secret()) of
ok -> require_strong_secret(admin_jwt_secret, effective_admin_jwt_secret());
Err -> Err
end
end.
-spec require_admin_seed_email(string()) ->
{ok, binary()} | {error, {missing_secret, atom()}}.
require_admin_seed_email(EnvKey) ->
case os:getenv(EnvKey) of
false -> {error, {missing_secret, list_to_atom(EnvKey)}};
"" -> {error, {missing_secret, list_to_atom(EnvKey)}};
Email -> {ok, list_to_binary(Email)}
end.
-spec require_admin_seed_password(string()) ->
{ok, binary()} | {error, {missing_secret, atom()} | {weak_secret, atom()}}.
require_admin_seed_password(EnvKey) ->
case os:getenv(EnvKey) of
false -> {error, {missing_secret, list_to_atom(EnvKey)}};
"" -> {error, {missing_secret, list_to_atom(EnvKey)}};
Pass ->
Bin = list_to_binary(Pass),
case is_strong_secret(Bin) of
true -> {ok, Bin};
false -> {error, {weak_secret, list_to_atom(EnvKey)}}
end
end.
%%%-------------------------------------------------------------------
%%% Effective values (порядок как в eventhub_auth)
%%%-------------------------------------------------------------------
effective_jwt_secret() ->
effective_secret(jwt_secret, "JWT_SECRET").
effective_admin_jwt_secret() ->
effective_secret(admin_jwt_secret, "ADMIN_JWT_SECRET").
effective_secret(AppKey, OsKey) ->
case application:get_env(eventhub, AppKey) of
{ok, Secret} when is_binary(Secret), byte_size(Secret) > 0 ->
case is_placeholder(Secret) of
true -> os_getenv_binary(OsKey);
false -> Secret
end;
_ ->
os_getenv_binary(OsKey)
end.
%%%-------------------------------------------------------------------
%%% Internal
%%%-------------------------------------------------------------------
require_strong_secret(Name, undefined) ->
{error, {missing_secret, Name}};
require_strong_secret(Name, Secret) ->
case is_strong_secret(Secret) of
true -> ok;
false -> {error, {weak_secret, Name}}
end.
is_strong_secret(Secret) when is_binary(Secret) ->
byte_size(Secret) >= ?MIN_SECRET_LEN andalso not lists:member(Secret, denylist()).
is_placeholder(Secret) ->
lists:member(Secret, [
<<"change_me_in_production">>,
<<"${JWT_SECRET:-change_me_in_production}">>,
<<"${ADMIN_JWT_SECRET:-change_me_in_production}">>
]).
denylist() -> [
<<"123456">>,
<<"change_me_in_production">>,
<<"eventhub_top_secret">>,
<<"user-secret-key-32-bytes-minimum!">>,
<<"admin-secret-key-32-bytes-minimum!">>,
<<"botpass123">>
].
os_getenv_binary(Key) ->
case os:getenv(Key) of
false -> undefined;
"" -> undefined;
Val -> list_to_binary(Val)
end.
dev_warn_if_weak(_Name, undefined) -> ok;
dev_warn_if_weak(Name, Secret) ->
case is_strong_secret(Secret) of
true -> ok;
false ->
io:format("~n[infra_secrets] WARNING: ~p is weak or missing in dev mode; "
"runtime fallbacks may apply.~n", [Name])
end.
+62
View File
@@ -0,0 +1,62 @@
-module(infra_secrets_tests).
-include_lib("eunit/include/eunit.hrl").
-define(STRONG_JWT, <<"this-is-a-strong-user-jwt-secret-32b">>).
-define(STRONG_ADMIN, <<"this-is-a-strong-admin-jwt-secret32">>).
setup() ->
application:load(eventhub),
ok.
cleanup(_) ->
application:unset_env(eventhub, jwt_secret),
application:unset_env(eventhub, admin_jwt_secret),
os:unsetenv("EVENTHUB_ENV"),
os:unsetenv("JWT_SECRET"),
os:unsetenv("ADMIN_JWT_SECRET"),
ok.
with_env(Key, Val, Fun) ->
os:putenv(Key, Val),
try Fun() after os:unsetenv(Key) end.
infra_secrets_test_() ->
{foreach, fun setup/0, fun cleanup/1, [
{"dev mode allows missing secrets", fun test_dev_missing_ok/0},
{"stage rejects missing JWT_SECRET", fun test_stage_missing_jwt/0},
{"stage rejects weak JWT_SECRET", fun test_stage_weak_jwt/0},
{"stage accepts strong secrets", fun test_stage_strong_ok/0},
{"require_admin_seed_password rejects 123456", fun test_weak_admin_password/0}
]}.
test_dev_missing_ok() ->
with_env("EVENTHUB_ENV", "dev", fun() ->
?assertEqual(dev, infra_secrets:mode()),
?assertEqual(ok, infra_secrets:validate_startup())
end).
test_stage_missing_jwt() ->
with_env("EVENTHUB_ENV", "stage", fun() ->
?assertMatch({error, {missing_secret, jwt_secret}},
infra_secrets:validate_startup())
end).
test_stage_weak_jwt() ->
with_env("EVENTHUB_ENV", "stage", fun() ->
os:putenv("JWT_SECRET", "123456"),
os:putenv("ADMIN_JWT_SECRET", binary_to_list(?STRONG_ADMIN)),
?assertMatch({error, {weak_secret, jwt_secret}},
infra_secrets:validate_startup())
end).
test_stage_strong_ok() ->
with_env("EVENTHUB_ENV", "stage", fun() ->
os:putenv("JWT_SECRET", binary_to_list(?STRONG_JWT)),
os:putenv("ADMIN_JWT_SECRET", binary_to_list(?STRONG_ADMIN)),
?assertEqual(ok, infra_secrets:validate_startup())
end).
test_weak_admin_password() ->
os:putenv("ADMIN_PASSWORD", "123456"),
?assertMatch({error, {weak_secret, 'ADMIN_PASSWORD'}},
infra_secrets:require_admin_seed_password("ADMIN_PASSWORD")).