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