Задачи 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>
This commit is contained in:
1 parent
744a025960
commit
8384c2c311
13 files changed
+330
-11
No files matched your search
@@ -0,0 +1,117 @@
|
||||
# Публикация 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`.
|
||||
Reference in new issue
Block a user