Files
ros_control/docs/changes/025-trusted-proxies-port/plan.md
T

58 lines
6.4 KiB
Markdown
Raw Normal View History

# План: 025 — доверенные прокси (реальный IP клиента) и параметризация порта
## Context
Ревью `docs/reviews/2026-09-28-1735-codebase-review.md`:
- **п. 14** — блокировка входа по IP (021) и IP в событиях `auth.*` берут `request.client.host`. За reverse-proxy все клиенты видны
с адреса прокси: одна блокировка на всех и журнал без реальных адресов. Разбора `X-Forwarded-For` и доверенных прокси нет.
- **п. 16** — `docker-compose.yml` публикует жёстко `"8000:8000"`; порт 8000 хоста занят посторонним процессом, стенд работает на 8001
через override-файл вне репозитория.
Решения пользователя:
- п. 16 — переменные **`APP_PORT`** (порт на хосте, по умолчанию 8000) и **`APP_BIND`** (адрес публикации, по умолчанию `0.0.0.0`),
как в `ipam_control`. В `.env` стенда оркестратор добавил `APP_PORT=8001` — стенд поднимается обычным `docker compose up -d`, override не нужен.
- п. 14 — **`TRUSTED_PROXIES` пустой по умолчанию и на стенде**: `X-Forwarded-For` игнорируется, пока прокси явно не указаны.
## Изменения
### п. 16 — порт (`docker-compose.yml`, `.env.example`, README)
- `ports: - "${APP_BIND:-0.0.0.0}:${APP_PORT:-8000}:8000"`. Внутренний порт контейнера не меняется (8000).
- `.env.example`: `APP_PORT=8000`, `APP_BIND=0.0.0.0` с комментарием (`127.0.0.1` — только за reverse-proxy на том же хосте).
- Приложение эти переменные не читает (только compose); `Settings` — `extra="ignore"`, конфликтов нет.
### п. 14 — реальный IP клиента (`app/config.py`, `app/security.py` или новый `app/client_ip.py`, `app/ui/routes.py`)
- `Settings.trusted_proxies: str = ""` — CIDR через запятую (`10.0.0.0/8,172.16.0.0/12`); одиночный адрес — как `/32`/`/128`.
Невалидный CIDR — проблема в `insecure_settings` (отказ старта, как у секретов): «TRUSTED_PROXIES: неверная сеть <значение>» (это не секрет — значение можно показать).
- `client_ip(request) -> str`: `peer = request.client.host` (или `"unknown"`).
- Если `peer` не входит в доверенные сети (или список пуст) — вернуть `peer`, **`X-Forwarded-For` игнорируется**.
- Иначе разобрать `X-Forwarded-For` (все заголовки, через запятую) **справа налево**: пропускать адреса из доверенных сетей;
первый недоверенный — IP клиента. Невалидная запись в цепочке — остановиться и вернуть последний валидный разобранный адрес
(или `peer`, если таких нет). Все адреса доверенные / заголовка нет — `peer`.
- Сети разбираются один раз (кэш по строке настройки).
- `login` (`app/ui/routes.py`): `ip = client_ip(request)` вместо `request.client.host` — ключ блокировки `login:<ip>` и `data.ip` в `auth.*`.
Другие места с `request.client` — найти grep'ом и перевести на `client_ip`.
- uvicorn `--proxy-headers` **не** включать (иначе uvicorn сам подменит `request.client` по своей логике `--forwarded-allow-ips`).
## Тесты (минимально, `tests/test_app.py`)
- `client_ip`: пустой `TRUSTED_PROXIES` + XFF → peer (подделка игнорируется); доверенный peer + `XFF: 1.2.3.4, 10.0.0.2` при `10.0.0.0/8` → `1.2.3.4`;
недоверенный peer + XFF → peer; мусор в XFF → последний валидный / peer; IPv6.
- Блокировка входа за прокси: `TestClient(client=("10.0.0.5", …))`, `TRUSTED_PROXIES=10.0.0.0/8`, 5 неверных с `XFF: 1.1.1.1` → 429 для `1.1.1.1`,
с `XFF: 2.2.2.2` вход проходит.
- `insecure_settings`: невалидный `TRUSTED_PROXIES` → проблема.
## Документация
README: «Конфигурация» (`APP_PORT`, `APP_BIND`, `TRUSTED_PROXIES`), «Быстрый старт» (адрес с `APP_PORT`), «Безопасность» (вход за прокси —
заменить «не поддерживается» на описание `TRUSTED_PROXIES` и предупреждение: доверие к сети Docker-моста делает доверенными и процессы хоста),
«Эксплуатация» (стенд на 8001 — через `APP_PORT` в `.env`, override не нужен), число тестов, строка 025 в истории. `summary.md` — оркестратор.
## Исполнение
Исполнитель (Sonnet): код, compose, `.env.example`, тесты, README; пересборка стенда **обычной командой**
`docker compose up -d --build --force-recreate` (без override — порт 8001 берётся из `.env`). Тесты не запускает, не коммитит, `.env` не читает.
## Проверка
- `pytest` — все зелёные.
- `docker compose config` → опубликован `8001` (из `.env`); контейнер Up на `0.0.0.0:8001->8000`; `/login` 200; новый код.
- Подделка: с хоста `curl -H "X-Forwarded-For: 9.9.9.9"` — 5 неверных входов → 6-й 429 уже **без** заголовка (блокируется реальный адрес, XFF проигнорирован);
событие `auth.failed` содержит реальный IP (адрес шлюза Docker), не `9.9.9.9`. Блокировка снимается перезапуском контейнера.
- Боевые данные — сверка по ID.
- Ручная проверка — пользователь: UI на `http://<хост>:8001`.