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>
9.6 KiB
9.6 KiB
Публикация 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 в историю изменений.
- пример Caddy заменить фактической схемой:
- План 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. Это проверяется пунктом «из интернета» ниже; при смене конфигурации шлюза — перепроверять.
Порядок выполнения
- Пользователь: CNAME
rxipam→mkt.rxmsk.ru(proxy в Cloudflare выключен, как у соседних: иначеremote_ip= IP Cloudflare). - 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. - Caddy: бэкап, правка блока, validate, reload.
- Проверки (ниже), затем документация и коммит в 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.json200. - Из интернета (мобильный интернет пользователя или внешний хост):
/200 и вход в UI работает;/docs,/docs/,/redoc,/openapi.json,/openapi.json?x=1,//docs,/%64ocs→ 404;- в
rxipam.logу этих запросовremote_ipпубличный,status404.
- Реальный 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.