From 5e42f6d11bb6e830705df913129f103ca8ac73c7 Mon Sep 17 00:00:00 2001 From: Max Ronzhin Date: Sat, 25 Jul 2026 22:59:34 +0300 Subject: [PATCH] deploy: require redis auth (--requirepass) across all consumers Redis without a password was the root cause of the security incident (cron miner via unauthenticated replication RCE, see .forcc/deploy/SESSION2-FINDINGS.md). Loopback binding alone doesn't protect against a compromised container inside the same compose network, so wire REDIS_PASSWORD as a required secret everywhere redis is used: backend/worker/worker-transcriber(-gpu), redis-exporter, livekit and egress (via rendered templates). docker compose now refuses to start without it instead of silently running unauthenticated. --- .env.example | 11 +++++++- .gitignore | 1 + deploy/docker-compose.yml | 18 ++++++++---- .../{egress.yaml => egress.yaml.template} | 5 +++- deploy/livekit/livekit.yaml.template | 5 ++++ deploy/render-templates.sh | 10 +++++-- docs/deploy/env.md | 28 +++++++++++++++++-- docs/deploy/scaling.md | 2 +- install.sh | 12 ++++++-- 9 files changed, 77 insertions(+), 15 deletions(-) rename deploy/egress/{egress.yaml => egress.yaml.template} (87%) diff --git a/.env.example b/.env.example index 07d2235..2ce4b78 100644 --- a/.env.example +++ b/.env.example @@ -5,7 +5,16 @@ POSTGRES_DB=vidconf DATABASE_URL=postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf # --- Redis --- -REDIS_URL=redis://localhost:6379/0 +# Обязателен (${REDIS_PASSWORD:?} в docker-compose.yml) — redis запускается +# с --requirepass, публикация без пароля = открытый redis в интернет (см. +# .forcc/deploy/SESSION2-FINDINGS.md — реальный инцидент на этом проекте). +# install.sh генерирует случайное значение при первой установке. +REDIS_PASSWORD=change-me-redis-secret +# REDIS_URL нужен ТОЛЬКО для запуска backend/worker на хосте вне docker-сети +# (см. docs/deploy/dev-setup.md) — внутри контейнеров docker-compose.yml +# всегда подставляет redis://:${REDIS_PASSWORD}@redis:6379/0 сам, это +# значение игнорируя. Держите пароль в этой строке синхронным с REDIS_PASSWORD. +REDIS_URL=redis://:change-me-redis-secret@localhost:6379/0 # --- Seed admin user --- SEED_ADMIN_EMAIL=admin@vidconf.example diff --git a/.gitignore b/.gitignore index 28e3109..f9fd688 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ deploy/**/*.local.yml # содержат реальные секреты/IP, рендерятся перед `docker compose up`. deploy/coturn/turnserver.conf deploy/livekit/livekit.yaml +deploy/egress/egress.yaml # Артефакты Celery beat celerybeat-schedule*.db diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index af2a61d..eb54f02 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -32,6 +32,13 @@ services: redis: image: redis:7 restart: unless-stopped + # requirepass обязателен: сам по себе loopback-биндинг порта (ниже) не + # защищает от взлома через скомпрометированный/зловредный контейнер в + # той же compose-сети — только внешнюю экспозицию. Инцидент безопасности + # (см. .forcc/deploy/SESSION2-FINDINGS.md) начался именно с redis без + # пароля. `${REDIS_PASSWORD:?...}` — без секрета в .env контейнер не + # стартует, а не тихо поднимается без аутентификации. + command: ["redis-server", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}"] # Публикуем ТОЛЬКО на loopback: redis используется исключительно внутри # compose-сети (backend/worker/livekit/egress по имени `redis:6379`). # Публикация на 0.0.0.0 без пароля = открытый redis в интернет — вектор @@ -41,7 +48,7 @@ services: volumes: - redis_data:/data healthcheck: - test: ["CMD", "redis-cli", "ping"] + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}", "--no-auth-warning", "ping"] interval: 5s timeout: 5s retries: 10 @@ -56,7 +63,7 @@ services: - ../.env environment: DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf} - REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}@redis:6379/0 PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml} LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env} LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:?LIVEKIT_API_SECRET не задан в .env} @@ -119,7 +126,7 @@ services: - ../.env environment: DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf} - REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}@redis:6379/0 PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml} PYTHONPATH: /app volumes: @@ -216,7 +223,7 @@ services: - ../.env environment: DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf} - REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}@redis:6379/0 PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml} RECORDINGS_DIR: ${RECORDINGS_DIR:-/recordings} PYTHONPATH: /app @@ -272,7 +279,7 @@ services: - ../.env environment: DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf} - REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}@redis:6379/0 PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml} RECORDINGS_DIR: ${RECORDINGS_DIR:-/recordings} PYTHONPATH: /app @@ -684,6 +691,7 @@ services: restart: unless-stopped environment: REDIS_ADDR: "redis://redis:6379" + REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env} depends_on: redis: condition: service_healthy diff --git a/deploy/egress/egress.yaml b/deploy/egress/egress.yaml.template similarity index 87% rename from deploy/egress/egress.yaml rename to deploy/egress/egress.yaml.template index 6a1e5b2..c19058a 100644 --- a/deploy/egress/egress.yaml +++ b/deploy/egress/egress.yaml.template @@ -13,9 +13,12 @@ log_level: info # Обязателен для egress (координация запущенных записей, см. комментарий -# в deploy/livekit/livekit.yaml). +# в deploy/livekit/livekit.yaml). password подставляется рендером +# (deploy/render-templates.sh) из REDIS_PASSWORD в .env — redis в +# docker-compose.yml запускается с --requirepass. redis: address: redis:6379 + password: ${REDIS_PASSWORD} # HTTP-эндпоинт /healthz для healthcheck контейнера. health_port: 8081 diff --git a/deploy/livekit/livekit.yaml.template b/deploy/livekit/livekit.yaml.template index f03b62b..eafd99e 100644 --- a/deploy/livekit/livekit.yaml.template +++ b/deploy/livekit/livekit.yaml.template @@ -38,8 +38,13 @@ rtc: # (проверено по официальной документации livekit/egress, раздел "Running # locally"). LiveKit сам по себе тоже использует redis для координации # между узлами кластера (здесь один узел, но сервис оставлен включённым). +# password подставляется рендером (deploy/render-templates.sh) из +# REDIS_PASSWORD в .env — redis в docker-compose.yml запускается с +# --requirepass, без пароля LiveKit не подключится (см. +# .forcc/deploy/SESSION2-FINDINGS.md про redis без пароля). redis: address: redis:6379 + password: ${REDIS_PASSWORD} # TURN обслуживает отдельный сервис coturn (профиль `media`, deploy/coturn). # При публичной экспозиции LiveKit разместите coturn/TURN-TLS на 443 и diff --git a/deploy/render-templates.sh b/deploy/render-templates.sh index 7540aea..0108b97 100755 --- a/deploy/render-templates.sh +++ b/deploy/render-templates.sh @@ -33,6 +33,7 @@ TURN_EXTERNAL_IP="$(env_var TURN_EXTERNAL_IP)" LIVEKIT_API_KEY="$(env_var LIVEKIT_API_KEY)" LIVEKIT_NODE_IP="$(env_var LIVEKIT_NODE_IP)" LIVEKIT_USE_EXTERNAL_IP="$(env_var LIVEKIT_USE_EXTERNAL_IP)" +REDIS_PASSWORD="$(env_var REDIS_PASSWORD)" : "${TURN_STATIC_AUTH_SECRET:?TURN_STATIC_AUTH_SECRET не задан в .env}" : "${TURN_REALM:?TURN_REALM не задан в .env}" @@ -40,13 +41,18 @@ LIVEKIT_USE_EXTERNAL_IP="$(env_var LIVEKIT_USE_EXTERNAL_IP)" : "${LIVEKIT_API_KEY:?LIVEKIT_API_KEY не задан в .env}" : "${LIVEKIT_NODE_IP:?LIVEKIT_NODE_IP не задан в .env (внешний IP/домен для ICE)}" : "${LIVEKIT_USE_EXTERNAL_IP:?LIVEKIT_USE_EXTERNAL_IP не задан в .env (true/false)}" +: "${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env (redis запускается с --requirepass, см. docker-compose.yml)}" -export TURN_STATIC_AUTH_SECRET TURN_REALM TURN_EXTERNAL_IP LIVEKIT_API_KEY LIVEKIT_NODE_IP LIVEKIT_USE_EXTERNAL_IP +export TURN_STATIC_AUTH_SECRET TURN_REALM TURN_EXTERNAL_IP LIVEKIT_API_KEY LIVEKIT_NODE_IP LIVEKIT_USE_EXTERNAL_IP REDIS_PASSWORD envsubst '${TURN_STATIC_AUTH_SECRET} ${TURN_REALM} ${TURN_EXTERNAL_IP}' \ < "$SCRIPT_DIR/coturn/turnserver.conf.template" > "$SCRIPT_DIR/coturn/turnserver.conf" echo "[render] deploy/coturn/turnserver.conf готов" -envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY}' \ +envsubst '${LIVEKIT_USE_EXTERNAL_IP} ${LIVEKIT_NODE_IP} ${LIVEKIT_API_KEY} ${REDIS_PASSWORD}' \ < "$SCRIPT_DIR/livekit/livekit.yaml.template" > "$SCRIPT_DIR/livekit/livekit.yaml" echo "[render] deploy/livekit/livekit.yaml готов" + +envsubst '${REDIS_PASSWORD}' \ + < "$SCRIPT_DIR/egress/egress.yaml.template" > "$SCRIPT_DIR/egress/egress.yaml" +echo "[render] deploy/egress/egress.yaml готов" diff --git a/docs/deploy/env.md b/docs/deploy/env.md index d4ed6e2..2688328 100644 --- a/docs/deploy/env.md +++ b/docs/deploy/env.md @@ -35,18 +35,38 @@ POSTGRES_DB=vidconf ## Redis (очередь Celery) +### REDIS_PASSWORD +**Тип:** `str` | **Обязателен** (нет дефолта — `${REDIS_PASSWORD:?}` в `deploy/docker-compose.yml`) + +**Описание:** Пароль Redis (`--requirepass`). Без него `docker compose up` не +стартует — намеренно: инцидент безопасности на этом проекте начался именно +с redis без пароля, опубликованного наружу (см. `.forcc/deploy/SESSION2-FINDINGS.md` +во внутренних заметках сессии деплоя). `install.sh` генерирует случайное +значение при первой установке. Используется backend/worker/livekit/egress — +все читают его через `REDIS_URL`/секцию `redis:` своих конфигов, генерируемых +из `.env` (см. `deploy/render-templates.sh`). + +**Пример:** +```bash +REDIS_PASSWORD=a1b2c3... +``` + ### REDIS_URL **Тип:** `str` | **Default:** `redis://localhost:6379/0` **Описание:** URL Redis для брокера Celery (очередь задач) и кэша сеансов. +Значим только при запуске backend/worker НА ХОСТЕ вне docker-сети — внутри +контейнеров `docker-compose.yml` всегда подставляет +`redis://:${REDIS_PASSWORD}@redis:6379/0` сам, это значение игнорируя. **Пример:** ```bash -REDIS_URL=redis://localhost:6379/0 +REDIS_URL=redis://:password@localhost:6379/0 REDIS_URL=redis://:password@redis.example.com:6379/1 # с паролем, БД 1 ``` -**Проде:** Используйте Redis с паролем и настройте мониторинг/резервные копии. +**Проде:** Redis публикуется только на `127.0.0.1` (loopback) — снаружи +недоступен даже с паролем; удалённый доступ — через ssh-туннель. --- @@ -440,7 +460,8 @@ SMTP_TIMEOUT_S=30 ```bash # Database & Redis DATABASE_URL=postgresql+asyncpg://vidconf:vidconf@localhost:5432/vidconf -REDIS_URL=redis://localhost:6379/0 +REDIS_PASSWORD=change-me-redis-secret +REDIS_URL=redis://:change-me-redis-secret@localhost:6379/0 # App PLUGINS_CONFIG_PATH=config/plugins.yaml @@ -468,6 +489,7 @@ MEDIA_ROOT=/app/media ```bash # Database & Redis DATABASE_URL=postgresql+asyncpg://vidconf:secure-pass@db.example.com:5432/vidconf +REDIS_PASSWORD=redis-password REDIS_URL=redis://:redis-password@redis.example.com:6379/0 # App diff --git a/docs/deploy/scaling.md b/docs/deploy/scaling.md index 12ae386..f03e9f3 100644 --- a/docs/deploy/scaling.md +++ b/docs/deploy/scaling.md @@ -69,7 +69,7 @@ services: - ../.env environment: DATABASE_URL: ${DATABASE_URL:-postgresql+asyncpg://vidconf:vidconf@postgres:5432/vidconf} - REDIS_URL: ${REDIS_URL:-redis://redis:6379/0} + REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD не задан в .env}@redis:6379/0 PLUGINS_CONFIG_PATH: ${PLUGINS_CONFIG_PATH:-config/plugins.yaml} PYTHONPATH: /app volumes: diff --git a/install.sh b/install.sh index 9f3b2d9..02b319f 100755 --- a/install.sh +++ b/install.sh @@ -248,7 +248,7 @@ fi ensure_env_file if [ "$FRESH_ENV" = "1" ]; then - for key in JWT_SECRET POSTGRES_PASSWORD TURN_STATIC_AUTH_SECRET \ + for key in JWT_SECRET POSTGRES_PASSWORD REDIS_PASSWORD TURN_STATIC_AUTH_SECRET \ LIVEKIT_API_SECRET GRAFANA_ADMIN_PASSWORD SEED_ADMIN_PASSWORD; do set_env_var "$key" "" done @@ -264,6 +264,14 @@ ensure_secret POSTGRES_PASSWORD 16 # compose), не `localhost` (тот годится только для запуска backend на хосте # вне контейнера, см. docs/deploy/dev-setup.md). set_env_var DATABASE_URL "postgresql+asyncpg://$(get_env_var POSTGRES_USER):$(get_env_var POSTGRES_PASSWORD)@postgres:5432/$(get_env_var POSTGRES_DB)" +ensure_secret REDIS_PASSWORD 24 +# REDIS_URL здесь — только для запуска backend/worker НА ХОСТЕ вне +# docker-сети (см. docs/deploy/dev-setup.md): внутри контейнеров +# docker-compose.yml всегда подставляет redis://:${REDIS_PASSWORD}@redis:6379/0 +# сам, независимо от этого значения. Пересобираем на каждом запуске по +# тому же принципу, что и DATABASE_URL выше — иначе после регенерации +# REDIS_PASSWORD это значение осталось бы со старым паролем. +set_env_var REDIS_URL "redis://:$(get_env_var REDIS_PASSWORD)@localhost:6379/0" ensure_secret TURN_STATIC_AUTH_SECRET 24 ensure_secret LIVEKIT_API_SECRET 32 ensure_secret GRAFANA_ADMIN_PASSWORD 16 @@ -372,7 +380,7 @@ for _p in "${_profiles[@]}"; do [ -n "$_p" ] && PROFILE_ARGS+=(--profile "$_p") done -echo "[install] Рендерю coturn/livekit из шаблонов (deploy/render-templates.sh)" +echo "[install] Рендерю coturn/livekit/egress из шаблонов (deploy/render-templates.sh)" "$SCRIPT_DIR/deploy/render-templates.sh" echo "[install] docker compose pull (лучше сразу, но необязательный шаг — образы, которых нет в реестре под текущим тегом, пропускаются)"