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.
This commit is contained in:
2026-07-25 22:59:34 +03:00
parent d593add0f3
commit 5e42f6d11b
9 changed files with 77 additions and 15 deletions

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

@@ -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 готов"

View File

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

View File

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

View File

@@ -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 (лучше сразу, но необязательный шаг — образы, которых нет в реестре под текущим тегом, пропускаются)"