Пункты 14 и 16 ревью 2026-09-28 17:35 (docs/changes/025):
- TRUSTED_PROXIES (CIDR, по умолчанию пусто): X-Forwarded-For учитывается
только от доверенного peer, цепочка разбирается справа налево; иначе
заголовок игнорируется — подделать IP нельзя. Реальный IP — в блокировке
входа и событиях auth.*; неверный CIDR — отказ старта;
- docker-compose: "${APP_BIND:-0.0.0.0}:${APP_PORT:-8000}:8000"; стенд на
8001 через APP_PORT в .env, override-файл больше не нужен.
Тесты: 36 из 36. Стенд: поддельный X-Forwarded-For проигнорирован,
заблокирован реальный адрес; боевые данные не изменены. Ручная проверка
UI пользователем на момент коммита не подтверждена.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
6.4 KiB
Markdown
59 lines
6.4 KiB
Markdown
# План: 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`.
|