Files
ayurishchevandClaude Opus 5.5 8384c2c311 Задачи 034-035: публикация через Caddy, последний вход пользователя
034 Публикация стенда как https://rxipam.rxmsk.ru через общий Caddy хоста (Caddyfile вне репозитория):
    UI и /api/v1 доступны из интернета, /docs, /redoc, /openapi.json — только из частных сетей
    (RFC 1918/4193, loopback). Caddy проксирует на опубликованный порт приложения — общая Docker-сеть
    отклонена из-за коллизии имён app/db с Nextcloud. TRUSTED_PROXIES=172.16.0.0/12: реальный IP
    клиента из X-Forwarded-For. Интеграционный тест журнала больше не проверяет подделку XFF с хоста
    (хост за Caddy доверенный), логику покрывает unit-тест.
035 Дата и IP последнего входа в разделе «Пользователи»: users.last_login_at / last_login_ip
    (миграция 0014 с заполнением из журнала), запись при успешном входе, UserOut, колонка в UI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 15:45:30 +03:00

118 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Публикация IPAM через Caddy: rxipam.rxmsk.ru (изменение 034)
> Отклонение при выполнении: схема с общей сетью `app_internal` (раздел 1) отменена из-за коллизии имён `app`/`db` — см. `SUMMARY.md`.
## Context
IPAM сейчас доступен только из LAN по HTTP (`192.168.5.9:8088`). Его нужно опубликовать в интернет через общий Caddy
(`/opt/lvraid/apps/caddy`) под `rxipam.rxmsk.ru` с TLS.
**Решения пользователя:**
- из интернета доступны UI и `/api/v1`;
- Swagger и OpenAPI (`/docs`, `/redoc`, `/openapi.json`) доступны только из частных сетей (RFC 1918 / RFC 4193, loopback);
- CNAME `rxipam` → `mkt.rxmsk.ru` в Cloudflare пользователь создаёт сам.
Отличить вызов API из UI от прямого вызова на уровне прокси нельзя: браузер SPA сам ходит в `/api/v1`.
Поэтому API в интернете защищают JWT, роли и лимиты входа приложения.
**Факты разведки:**
- Caddy работает в контейнере `caddy`, сети `app_external` (172.18/16) и `app_internal` (172.19/16).
Сертификаты — ACME DNS-01 через Cloudflare, работает и без открытого 80.
- Caddy видит настоящие публичные IP клиентов (в логах `93.123.x`, `13.210.x` …), поэтому `remote_ip` для разграничения надёжен.
LAN-клиенты, заходящие по публичному имени, приходят с `192.168.5.253`: это шлюз MikroTik (hairpin NAT), он в частном диапазоне.
- Приложение (`ipam_control_006-app-1`) — в своей сети `ipam_control_006_default`, Caddy его сейчас не видит.
- `X-Forwarded-For` приложение учитывает только от `TRUSTED_PROXIES` (`app/request_context.py::resolve_client_ip`, справа налево).
Сейчас список пуст. Без него после публикации все интернет-клиенты получат один IP — IP Caddy, тогда:
- лимит 20 неудач на IP блокирует вход всем сразу;
- в журнале исчезнут реальные адреса.
## Изменения
### 1. Сеть: приложение в `app_internal` (репозиторий ipam_control)
- Новый `docker-compose.caddy.yml`: сервису `app` добавить сеть `app_internal` (external) с алиасом `ipam-app`, сеть `default` сохранить.
Отдельный файл нужен, чтобы стенд на машине без Caddy (без сети `app_internal`) продолжал подниматься базовым `docker-compose.yml`.
- `.env` стенда:
- `COMPOSE_FILE=docker-compose.yml:docker-compose.caddy.yml` — команды `docker compose -p ipam_control_006 …` из README и тестов не меняются;
- `TRUSTED_PROXIES=172.19.0.0/16` — подсеть `app_internal`. IP контейнера Caddy при пересоздании может смениться, поэтому /32 не подходит.
Остальные контейнеры этой сети — свои сервисы, риск подделки XFF от них приемлем.
- `.env.example`: закомментированные `COMPOSE_FILE` и `TRUSTED_PROXIES` с пояснением.
- `APP_BIND` оставить `0.0.0.0`: прямой LAN-доступ `:8088` сохраняется, порт на шлюзе наружу не проброшен.
### 2. Caddyfile (`/opt/lvraid/apps/caddy/Caddyfile`)
- Перед правкой — копия `Caddyfile.bak-2026-09-27`.
- Править **на месте**: файл смонтирован в контейнер одиночным bind-mount `:ro`. Замена inode, как делают редакторы с atomic-rename,
оставит контейнер со старым файлом — после правки сверить `docker exec caddy cat /etc/caddy/Caddyfile`.
- Новый блок в стиле соседних (`vault`, `artstore`):
```
# IPAM Manager
rxipam.rxmsk.ru {
log {
output file /var/log/caddy/rxipam.log {
roll_size 100mb
roll_keep 5
roll_keep_for 720h
}
format json
level INFO
}
# Swagger/OpenAPI — только из частных сетей (RFC 1918/4193, loopback); из интернета — как будто их нет
@docs_external {
path /docs /docs/* /redoc /redoc/* /openapi.json
not remote_ip private_ranges
}
respond @docs_external 404
reverse_proxy ipam-app:8000
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
-Server
}
}
```
- `remote_ip` (TCP-пир), а не `client_ip`: у Caddy нет `trusted_proxies`, поэтому заголовки клиента на решение не влияют.
- XFF к приложению Caddy формирует сам и не доверяет входящему. Приложение берёт из него реальный IP (п. 1).
- `level INFO` — access-лог для разбора инцидентов (у соседей `ERROR`). CSP и прочие заголовки ставит приложение.
- Применение:
- `docker exec caddy caddy validate --config /etc/caddy/Caddyfile`;
- `docker exec caddy caddy reload --config /etc/caddy/Caddyfile`, без рестарта: остальные сайты не прерываются.
### 3. Документация (правила проекта)
- `docs/changes/034-caddy-publication/PLAN.md` — этот план; по завершении — `SUMMARY.md`.
- `README.md`, раздел «Публикация и эксплуатация»:
- пример Caddy заменить фактической схемой: `docker-compose.caddy.yml`, `COMPOSE_FILE`, `TRUSTED_PROXIES`, ограничение Swagger;
- добавить строку 034 в историю изменений.
- План 031 частично перекрыт. Часть 2 (свой Caddy-профиль) заменяется общим Caddy. Часть 1 (`DOCS_ENABLED=false` по умолчанию)
остаётся как вторая линия защиты — отдельной задачей, в этот объём не входит.
## Замечания (без действий в этом объёме)
- LAN-пользователи через публичное имя идут через hairpin и для приложения выглядят одним IP `192.168.5.253`. Они делят лимит
«20 неудач на IP» и неразличимы в журнале. Лечится split DNS на MikroTik (`rxipam.rxmsk.ru` → `192.168.5.9` внутри LAN):
трафик пойдёт на Caddy напрямую с реальным LAN-IP. Рекомендация пользователю.
- Если шлюз когда-либо начнёт делать SNAT входящего интернет-трафика (masquerade на WAN→LAN), интернет-клиенты
станут «частными» и получат Swagger. Это проверяется пунктом «из интернета» ниже; при смене конфигурации шлюза — перепроверять.
## Порядок выполнения
1. Пользователь: CNAME `rxipam` → `mkt.rxmsk.ru` (proxy в Cloudflare выключен, как у соседних: иначе `remote_ip` = IP Cloudflare).
2. ipam_control: `docker-compose.caddy.yml`, `.env` (`COMPOSE_FILE`, `TRUSTED_PROXIES`), `docker compose -p ipam_control_006 up -d`;
проверить `docker exec caddy wget -qO- http://ipam-app:8000/healthz`.
3. Caddy: бэкап, правка блока, validate, reload.
4. Проверки (ниже), затем документация и коммит в ipam_control (Caddyfile вне репозитория).
## Проверка
- DNS: `getent hosts rxipam.rxmsk.ru` → `62.176.10.113`. Сертификат выпущен: `docker logs caddy` без ошибок ACME,
`curl -sI https://rxipam.rxmsk.ru/` → 200, HSTS присутствует.
- Из LAN (частный IP): `/` 200, `/api/v1/auth/me` без токена 401, `/docs` и `/openapi.json` 200.
- Из интернета (мобильный интернет пользователя или внешний хост):
- `/` 200 и вход в UI работает;
- `/docs`, `/docs/`, `/redoc`, `/openapi.json`, `/openapi.json?x=1`, `//docs`, `/%64ocs` → 404;
- в `rxipam.log` у этих запросов `remote_ip` публичный, `status` 404.
- Реальный IP в приложении: войти из интернета → в журнале (событие входа) `client_ip` — публичный адрес, не `172.19.x`.
Вход из LAN через FQDN даёт `192.168.5.253`.
- Регрессия: `venv/bin/python -m pytest -q` (15 passed) — тесты идут на `127.0.0.1:8088`, мимо Caddy.
- Остальные сайты Caddy после reload отвечают: `curl -sI https://vault.rxmsk.ru`, `https://artstore.rxmsk.ru` → не 5xx.
## Откат
- Caddy: вернуть `Caddyfile.bak-2026-09-27` (на месте) и `caddy reload`.
- ipam_control: убрать `COMPOSE_FILE`/`TRUSTED_PROXIES` из `.env`, `docker compose -p ipam_control_006 up -d`.