gendesign/caddy/sites/infra.caddy
lekss361 1bd5283aa6
Some checks failed
Deploy Infra Host / sync-infra-host (push) Successful in 8s
Deploy / changes (push) Successful in 14s
Deploy / build-backend (push) Has been skipped
Deploy / build-worker (push) Has been skipped
Deploy / build-frontend (push) Has been skipped
Deploy / deploy (push) Has been skipped
Deploy / deploy-caddy (push) Successful in 5s
Deploy / deploy-status (push) Successful in 1s
Deploy Metrics / server (push) Successful in 46s
Deploy Metrics / agent-apps (push) Failing after 14s
Deploy Metrics / agent-infra (push) Successful in 24s
Deploy / perimeter-smoke (push) Successful in 1m41s
fix(caddy): закрыть генерацию архивов Forgejo — она съедала диск (#3534)
2026-09-16 14:22:18 +00:00

186 lines
11 KiB
Text
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ═══════════════════════════════════════════════════════════════════════════
# caddy/sites/infra.caddy — инфраструктурные домены, остающиеся на Beget
#
# Переезд GenDesign с Beget (46.173.16.127) на Selectel Poincare
# (188.124.37.140), окно 30.08.2026. Три домена этого файла ОСТАЮТСЯ на
# Beget — переезжают только Site Finder/МЕРА-домены (см.
# caddy/sites/apps.caddy). Общий механизм переключения (переменная
# CADDY_SITES, `import caddy/sites/{$CADDY_SITES:*}.caddy` в корневом
# Caddyfile, мотив с ACME rate-limit) описан в шапке apps.caddy — не
# дублируем здесь, читай там.
#
# Домены в этом файле (после переезда — на Beget, CADDY_SITES=infra):
# - obsidian.gendsgn.ru — Obsidian Self-hosted LiveSync (CouchDB backend)
# - errors.gendsgn.ru — GlitchTip, self-hosted error tracking
# - git.gendsgn.ru — Forgejo, self-hosted git + Actions CI
#
# На Selectel (CADDY_SITES=apps) этот файл не импортируется вообще — Caddy
# там даже не видит эти хосты и не пытается выпускать для них сертификаты.
# ═══════════════════════════════════════════════════════════════════════════
# Obsidian Self-hosted LiveSync (CouchDB backend).
# Auto-TLS Let's Encrypt. CORS уже включён на стороне CouchDB через bootstrap
# (см. scripts/setup-couchdb.sh). Basic-auth — на стороне CouchDB (admin user).
#
# DNS: A-record obsidian.gendsgn.ru → IP VPS.
# Клиенты Obsidian + Self-hosted LiveSync plugin указывают на этот URL.
obsidian.gendsgn.ru {
encode zstd gzip
reverse_proxy couchdb:5984 {
# Большие документы (vault attachments / images) — увеличиваем timeout
transport http {
response_header_timeout 120s
}
}
}
# GlitchTip — self-hosted error tracking (Sentry-compatible).
# DNS: A-record errors.gendsgn.ru → IP VPS.
errors.gendsgn.ru {
encode zstd gzip
reverse_proxy glitchtip-web:8080
log {
output file /var/log/caddy/errors.gendsgn.ru.log
}
}
# Forgejo — self-hosted git (migration 2026-05-16).
# DNS: A-record git.gendsgn.ru → IP VPS.
# Forgejo container из forgejo-migration/docker-compose.yml на shared
# gendesign_default network. HTTP port 3000 (default Forgejo).
# Был добавлен вручную при migration, потерян при первом auto-deploy после
# изменения Caddyfile (deploy.yml делает git reset --hard). См. fix issue.
git.gendsgn.ru {
encode zstd gzip
# Генерация архивов закрыта — она съедала диск (16.09.2026).
#
# Forgejo на КАЖДЫЙ GET /<owner>/<repo>/archive/<ref>.{zip,tar.gz,bundle}
# собирает снапшот репозитория заново (20-30 МБ, несколько секунд CPU) и
# кладёт результат в кэш repo-archive. Штатная уборка archive_cleanup
# работает, но раз в сутки — а внешний обходчик (73 из 78 обращений за 6ч
# с одного адреса 216.73.216.17) дёргал архив для каждого коммита и держал
# генерацию на ~100 файлах в час. Кэш вырос до 55 ГБ при диске 145 ГБ и ел
# 2 ГБ/ч: до нуля оставалось ~12 часов.
#
# Ограничение скорости тут не лечит: даже один запрос в минуту — это
# 30 МБ/мин новых файлов при суточной уборке. Нужен именно отказ.
# На `git clone`, `git fetch` и работу CI это НЕ влияет: смарт-протокол
# ходит по /<owner>/<repo>/info/refs и /git-upload-pack, другие пути.
# Цена: анонимная кнопка «скачать архив» в вебе больше не работает —
# сознательный размен, вернуть можно снятием этих двух правил.
@repo_archive path_regexp repo_archive ^/[^/]+/[^/]+/archive/.+$
respond @repo_archive "Архивы отключены: используйте git clone" 403
@api_archive path_regexp api_archive ^/api/v1/repos/[^/]+/[^/]+/archive/.+$
respond @api_archive "Архивы отключены: используйте git clone" 403
reverse_proxy forgejo:3000
log {
output file /var/log/caddy/git.gendsgn.ru.log
}
}
# Мониторинг — Grafana + приёмник метрик и логов (задача #3078).
# DNS: A-record metrics.gendsgn.ru → 46.173.16.127 (заведена 2026-08-26).
#
# ПОЧЕМУ ЗДЕСЬ, А НЕ В apps.caddy. Наблюдатель сознательно поставлен у ДРУГОГО
# провайдера, чем наблюдаемое: продукт живёт на Selectel Poincare, и если ляжет
# он, мониторинг обязан выжить и сказать об этом. Стек поднимается отдельным
# compose-проектом gendesign-metrics и виден Caddy через сеть gendesign_shared.
#
# ДВА КОНТУРА С РАЗНЫМИ УЧЁТКАМИ:
# /ingest/* — машинный. Агент Alloy с Poincare шлёт сюда remote_write и логи
# исходящим HTTPS. Благодаря этому на Poincare не открыт НИ ОДИН
# входящий порт сверх 22/80/443.
# всё прочее — витрина Grafana под отдельным паролем.
# Пароль приёмника по построению лежит в открытом виде на продуктовом хосте;
# разделив учётки, мы не отдаём вместе с ним доступ к дашбордам.
metrics.gendsgn.ru {
encode zstd gzip
# handle_path срезает префикс: Prometheus получает /api/v1/write,
# как если бы обращались к нему напрямую.
handle_path /ingest/prometheus/* {
import ../metrics-ingest.caddy.snippet
reverse_proxy prometheus:9090
}
# Loki ожидает путь /loki/api/v1/push — он и остаётся после среза /ingest/loki.
handle_path /ingest/loki/* {
import ../metrics-ingest.caddy.snippet
reverse_proxy loki:3100
}
# Кнопка «Принял в работу» под клиентским инцидентом (#3078).
#
# БЕЗ АВТОРИЗАЦИИ НАМЕРЕННО. Кнопку жмут с телефона, ночью, когда лежит
# прод. Требовать в этот момент ввод пароля — значит получить ноль нажатий
# и вернуться к вопросу «а кто-нибудь вообще это читает».
#
# Защита — неугадываемость и одноразовость: путь содержит 128-битный токен,
# выданный ровно под это сообщение и живущий сутки. Худшее, чего добьётся
# угадавший, — ложная отметка «принято» в чате, где сразу видно, что её
# поставил не человек. Прав в системе токен не даёт никаких.
#
# Сервис отвечает только на /ack/*, /glitchtip и /healthz; всё прочее — 404.
handle /ack/* {
reverse_proxy alert-ack:8080
}
# Резервный приёмник алертов GlitchTip (#3471). Основной получатель —
# продуктовый бэкенд на Selectel, то есть тот самый сервис, за которым эти
# алерты и следят: пока он лежит, его собственные ошибки доставлять некому.
# Этот путь живёт у другого провайдера и с чистой сетью до Telegram, поэтому
# переживает падение Selectel целиком.
#
# Секрет — в значении query-параметра, а не в пути: путь сам по себе не
# секрет, и его попадание в access-лог безопасно. Само значение вырезает
# scrub_credentials в Alloy до записи в Loki (#3154).
handle /glitchtip* {
reverse_proxy alert-ack:8080
}
# Ретранслятор Bot API продукта (#3471). Путь от Selectel до
# api.telegram.org теряет примерно каждый четвёртый короткий запрос, тот же
# замер с Beget в те же минуты — чистый. Поэтому продуктовый бот ходит в
# Telegram отсюда, а не напрямую.
#
# Путь содержит токен бота (/tg-relay/bot<TOKEN>/<method>) — он не должен
# осесть ни в файловом логе сайта, ни в stdout-копии (#3154), поэтому
# запрос помечен log_skip.
@tg_relay path /tg-relay/*
log_skip @tg_relay
handle_path /tg-relay/* {
reverse_proxy tg-relay:8080 {
# getUpdates — long-poll до ~40с, дефолтный таймаут ответа короче.
transport http {
response_header_timeout 80s
}
}
}
# Вход ОДИН — собственный вход Grafana (#3078). Внешний basic_auth снят по
# решению владельца: два запроса пароля подряд мешали работе, а Grafana имеет
# собственную аутентификацию с ролями и `GF_USERS_ALLOW_SIGN_UP=false`.
#
# Что при этом теряется, чтобы решение было осознанным: basic_auth отсекал
# сканеры до Grafana и прикрывал бы её собственную будущую уязвимость. Теперь
# страница входа Grafana видна из интернета напрямую.
#
# Приём метрик НАРОЧНО остаётся под basic_auth (см. handle_path выше): туда
# ходит агент по паролю, который лежит в открытом виде в окружении
# продуктового хоста, и отдельная учётка там ограничивает ущерб записью.
handle {
reverse_proxy grafana:3000
}
log {
output file /var/log/caddy/metrics.gendsgn.ru.log
}
}