All checks were successful
CI Trade-In / backend-tests (pull_request) Has been skipped
CI Trade-In / browser-tests (pull_request) Has been skipped
CI / frontend-tests (pull_request) Has been skipped
CI Trade-In / frontend-checks (pull_request) Successful in 1m3s
CI Trade-In / changes (pull_request) Successful in 10s
CI / changes (pull_request) Successful in 18s
CI / backend-tests (pull_request) Has been skipped
CI / openapi-codegen-check (pull_request) Has been skipped
Аудит 02.09: curl http://188.124.37.140/api/v1/me отвечал 401 с Www-Authenticate: Basic realm=GenDesign Pilot. Браузер, открывший голый IP, отправил бы пароль пилота plaintext-ом любому слушающему канал. Замысел блока был верный — «закрыть обход через голый IP тем же auth-гейтом». Исполнение дырявое: basic_auth на plain HTTP это и есть канал утечки пароля. Редирект на канонический HTTPS строже гейта: по HTTP не отдаётся ни контент, ни сам запрос пароля — обход закрыт тем, что отвечать нечему. /health остаётся публичным (liveness). Потребителей у IP:80 нет: все deploy-смоки ходят docker exec → localhost внутри контейнеров (grep по .forgejo/workflows и ops/). Конфиг провалидирован БОЕВЫМ бинарём: docker exec gendesign-caddy-1 caddy validate → Valid configuration. Гейт snippet-mounts зелёный.
93 lines
6.6 KiB
Caddyfile
93 lines
6.6 KiB
Caddyfile
# Caddy config.
|
||
#
|
||
# - gendsgn.ru — main production site, auto-TLS via Let's Encrypt.
|
||
# - www.gendsgn.ru — 301 redirect to apex (canonical URL).
|
||
# - :80 — fallback for raw IP access. Closed by same auth gate (pilot phase).
|
||
#
|
||
# `handle /api/*` (NOT `handle_path`) — we keep the /api prefix because
|
||
# the FastAPI router is mounted at /api/v1/*.
|
||
#
|
||
# Auth gate: basic_auth for pilot phase (2026-05-23).
|
||
# Users managed via caddy/users.caddy.snippet (git history = audit trail).
|
||
# Public exclusions: /health (liveness probe), /preview/* (static mockups).
|
||
#
|
||
# #2558: с 2026-07 basic_auth гейтит ТОЛЬКО Site Finder (`/`, `/api/*`,
|
||
# `/analytics` и т.д.). `/trade-in/*` (+ `/sale-share` redirect) вынесены ВЫШЕ
|
||
# import'а — у trade-in своя авторизация (форма входа + opaque session-cookie,
|
||
# см. #2552) поверх RBAC (`tradein-mvp/backend/app/core/rbac.py`). Site Finder
|
||
# всё ещё легаси-пилотный basic_auth (roles.yaml dual-mode остаётся живым для
|
||
# него — НЕ трогать caddy/users.caddy.snippet).
|
||
#
|
||
# IMPORTANT: route { } block is required to preserve directive order.
|
||
# Without route { }, Caddy executes directives in hard-coded default order
|
||
# (basic_auth runs before handle), making /health and /preview/* exclusions
|
||
# ineffective. With route { }, handlers are matched top-to-bottom as written.
|
||
|
||
# #2213: global `log_credentials` УБРАН. Он заставлял Caddy писать raw
|
||
# Authorization header (Base64 `user:password`, тривиально обратимый) в ОБА лог-файла
|
||
# (gendsgn.ru.log + auth_audit.log). Даже при root-only доступе и коротком retention
|
||
# это plaintext-пароли пилотов на диске — недопустимый риск против скромной выгоды.
|
||
# Trade-off: glitchtip-auth-forwarder больше не извлекает `attempted_username` из 401
|
||
# (тег деградирует в "(none)" — forwarder это уже обрабатывает gracefully, не падает).
|
||
# Событие basic_auth 401 (remote_ip / uri / method) по-прежнему уходит в GlitchTip.
|
||
|
||
|
||
# ── Site-блоки вынесены по хостам (#3059, переезд 30.08) ────────────────────
|
||
# Раньше все восемь доменов жили прямо здесь. После разделения продуктов между
|
||
# двумя хостами это стало опасно: деплой синхронизирует рабочее дерево с
|
||
# origin/main и перечитывает конфиг, поэтому на Selectel приезжал бы файл
|
||
# целиком — и Caddy начинал бы выпускать сертификаты для obsidian/errors/git,
|
||
# чей DNS указывает на Beget. ACME падал бы на HTTP-01, с риском упереться в
|
||
# rate limit Let's Encrypt.
|
||
#
|
||
# caddy/sites/apps.caddy gendsgn.ru, www, meraocenka, merahome, meraotsenka
|
||
# -> уезжают на Selectel
|
||
# caddy/sites/infra.caddy obsidian, errors, git
|
||
# -> остаются на Beget (Forgejo, GlitchTip, CouchDB)
|
||
#
|
||
# CADDY_SITES выбирает подмножество. Дефолт `*` = оба файла = ТЕКУЩЕЕ поведение
|
||
# Beget, где сейчас обслуживаются все восемь доменов — то есть до переезда
|
||
# ничего не меняется. В окне: на Selectel CADDY_SITES=apps, на Beget=infra.
|
||
import caddy/sites/{$CADDY_SITES:*}.caddy
|
||
|
||
# Plain HTTP by IP. /health остаётся публичным (liveness). Всё остальное —
|
||
# РЕДИРЕКТ на канонический HTTPS, а не проксирование под basic_auth.
|
||
#
|
||
# ЗДЕСЬ СТОЯЛ auth-гейт с проксированием приложения — «закрыть обход через
|
||
# голый IP тем же гейтом». Замысел верный, исполнение — дыра: Basic-challenge
|
||
# на plain HTTP означает, что браузер отправит пароль пилота ОТКРЫТЫМ ТЕКСТОМ
|
||
# любому, кто слушает канал (аудит 02.09.2026: curl http://<IP>/api/v1/me →
|
||
# 401 + Www-Authenticate: Basic realm="GenDesign Pilot"). Редирект строже
|
||
# гейта: по HTTP не отдаётся ни контент, ни сам запрос пароля, обход через
|
||
# IP закрыт тем, что отвечать нечему. Потребителей у IP:80 нет: все
|
||
# deploy-смоки ходят docker exec → localhost внутри контейнеров (проверено
|
||
# grep-ом по .forgejo/workflows и ops/ 02.09.2026).
|
||
:80 {
|
||
route {
|
||
# /health — public, без auth (liveness probe).
|
||
handle /health {
|
||
reverse_proxy backend:8000
|
||
}
|
||
|
||
handle {
|
||
redir https://gendsgn.ru{uri} permanent
|
||
}
|
||
}
|
||
}
|
||
# Test deploy flow 2026-05-15T21:43:32Z
|
||
|
||
# ─────────────────────────────────────────────────────────────────────────────
|
||
# Локальные site-блоки, которых не может быть в git.
|
||
#
|
||
# Мотив: garmin.gendsgn.ru (личный remote MCP на этом же VPS). Апстрим-сервер
|
||
# аутентификации не имеет вовсе, а claude.ai custom connector ходит на голый URL
|
||
# без кастомных заголовков — единственный доступный рубеж это секрет в пути.
|
||
# Секрет в git класть нельзя, а блок, вписанный руками прямо сюда, сносится
|
||
# первым же деплоем (`git reset --hard origin/main`; 2026-08-16 так и вышло —
|
||
# контейнер остался жив, но хост пропал вместе со своим сертификатом).
|
||
#
|
||
# Поэтому: сам блок лежит на VPS как untracked `caddy/local/*.caddy` (reset
|
||
# --hard untracked не трогает), а в репозитории живёт только этот import.
|
||
# Пустой glob для Caddy не ошибка — `caddy validate` проходит, на машинах без
|
||
# локальных блоков строка просто ничего не делает.
|
||
import caddy/local/*.caddy
|