Files

117 lines
9.6 KiB
Markdown
Raw Permalink Normal View History

# Публикация 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`.