Прокси Caddy для пользовательских доменов
Пользовательские церковные домены (mychurch.org → веб-сайт церкви на B1) завершаются на единственном Windows-сервере EC2, работающем под управлением Caddy. Этот сервер владеет TLS-сертификатами, разрешает каждый домен в его сайт {sub}.b1.church и выполняет реверс-проксирование с переписанным заголовком Host. Вся его конфигурация — это два файла: статический Caddyfile и hosts.map, обновляемый из Membership API, — так что он переживает перезапуски с нулевым состоянием времени выполнения. На этой странице описано, как этот сервер строится с нуля, как он работает, и подводные камни, проверенные на практике, о которые споткнётся любой, кто будет пересобирать его.
О том, как запрос разрешается в церковь/сайт после того, как он достигает B1App, см. Маршрутизация веб-сайтов и мульти-сайт.
Компоненты
| Часть | Что это такое |
|---|---|
| Экземпляр EC2 | Windows Server; Elastic IP 3.23.251.61 (вшит в DNS церквей по всему миру — IP постоянен, экземпляры одноразовые) |
C:\caddy\caddy.exe | Пользовательская сборка Caddy с модулем хранилища techknowlogick/certmagic-s3 — стандартный Caddy не может читать это хранилище сертификатов |
C:\caddy\Caddyfile | Вся конфигурация прокси: on-demand TLS, map хост→вышестоящий сервер, редиректы www→apex, :80→https |
C:\caddy\hosts.map | Строка {domain} {sub}.b1.church на каждый маршрутизируемый домен, импортируется в блок map Caddyfile |
sync-hostmap.ps1 + задача CaddyHostmapSync | Запланированная задача (каждые 5 минут + при загрузке, от имени SYSTEM) обновляет hosts.map из API и плавно перезагружает Caddy только при изменении |
Служба Windows caddy (обёртка WinSW) | Запускает caddy.exe run --config C:\caddy\Caddyfile --adapter caddyfile; автоперезапуск при сбое. Caddy не осведомлён об SCM, поэтому нужна обёртка |
Бакет S3 churchapps-caddy-certs | Общее хранилище сертификатов (region us-east-2, префикс certs) — сертификаты переживают пересборку экземпляра |
Роль IAM CaddyRole | Даёт экземпляру доступ к S3; Caddy использует стандартную цепочку учётных данных AWS (без ключей в конфиге) |
Две конечные точки API, от которых зависит сервер
Обе анонимны, в Membership API:
| Конечная точка | Роль |
|---|---|
GET /membership/domains/authorize?domain={host} | Шлюз ask on-demand TLS Caddy: 200 {"authorized":true}, когда хост (или, для хоста www., его апекс) является строкой в domains; иначе 404. Это контроль злоупотреблений — Caddy не выдаст сертификат для хоста, который отклоняет эта конечная точка |
GET /membership/domains/hostmap | text/plain, отсортированные, дедуплицированные строки {domain} {sub}.b1.church (с учётом сайта: домен, назначенный дополнительному сайту, набирает субдомен этого сайта). Источник для map |
Поток запроса
- Браузер разрешает
mychurch.org→3.23.251.61(запись apexA, либоCNAME proxy.b1.church). - Caddy завершает TLS. Сертификат под рукой в S3 → обслуживание; неизвестный SNI → запрашивается
authorize; 200 → выдача по требованию через Let's Encrypt; 404 → рукопожатие отклоняется (нет сертификата, нет ответа — неизвестный хост получает отказ на уровне TLS, а не HTTP-ошибку). mapразрешает Host в{sub}.b1.church;www.{apex}получает 302 на апекс; авторизованный, но не сопоставленный хост (совсем новый домен внутри окна синхронизации ≤5 минут) получает чистую 404.reverse_proxyнабирает{sub}.b1.church:443с переписанными SNI и Host на вышестоящий сервер, так что граница Vercel обслуживает сайт B1App.- Порт 80 пропускает вызовы ACME HTTP-01 и делает редирект 308 на https для всего остального.
Распространение нового домена: домен, сохранённый в B1Admin, становится маршрутизируемым в течение ~5 минут (задача синхронизации); его сертификат выпускается при первом обращении по HTTPS.
Сборка сервера с нуля
Сокращённо из проверенной на практике процедуры (полная пошаговая инструкция с командами для копирования-вставки живёт в рабочем пространстве эксплуатации, не в этом репозитории). Сначала предпосылки — без них сборка мертва:
- IAM: прикрепите
CaddyRole(доступ S3 к бакету сертификатов) к экземпляру. Проверьте через IMDSv2 с сервера — учтите, что голый GET к IMDS, возвращающий 401, просто означает, что принудительно включён IMDSv2, а не «нет роли». - Здоровье API:
authorizeдолжен вернуть 404 для фиктивного домена, аhostmapдолжен вернуть 200, прежде чем делать что-либо ещё.
Затем:
- Бинарный файл: скачайте пользовательскую сборку через сервис сборки Caddy —
https://caddyserver.com/api/download?os=windows&arch=amd64&p=github.com/techknowlogick/certmagic-s3(~57 МБ против ~45 МБ у стандартной; v2.11.4 на момент написания). Выбор модуля важен:techknowlogick/certmagic-s3использует ключиbucket/region/prefix, соответствующие существующей раскладке сертификатов; форкss098используетhost/endpointи не найдёт существующие сертификаты. - Файлы:
Caddyfile+sync-hostmap.ps1вC:\caddy\; заполните карту один раз черезsync-hostmap.ps1 -NoReload. - Проверки перед первым запуском:
caddy list-modulesдолжен показывать модуль хранилища s3;caddy adaptдолжен выдавать"module":"s3","bucket":"churchapps-caddy-certs","region":"us-east-2","prefix":"certs"в своём блоке хранилища;caddy validateдолжен проходить успешно. - Служба: установите через WinSW (id службы
caddy, автоперезапуск при сбое, вращающиеся логи). Запускается от LocalSystem, что даёт доступ к IMDS для учётных данных роли. - Задача синхронизации: зарегистрируйте
CaddyHostmapSync(SYSTEM, каждые 5 минут + при запуске, лимит выполнения 4 минуты). - Проверка перед переключением: принудительно разрешите домены на
127.0.0.1черезcurl --resolve(у сервера нет реального трафика, пока EIP не переехал): существующий домен должен обслуживаться с валидным перенесённым сертификатом;www.должен давать 302; неизвестный хост должен получать отказ на уровне TLS; иRestart-Service caddyдолжна вернуться к обслуживанию без ручной повторной инициализации — этот тест перезапуска и есть весь смысл статического дизайна. - Запуск в продакшн: переассоциируйте Elastic IP
3.23.251.61с новым экземпляром. DNS церквей никогда не меняется.
Подводные камни, проверенные на практике (усвоено тяжёлым путём — не допускать регресса)
| Подводный камень | Симптом | Исправление |
|---|---|---|
tls_server_name {vars.upstream} в транспорте reverse_proxy | Каждый проксируемый домен выдаёт 502: плейсхолдеры карты разрешаются пустыми в момент TLS-набора («либо ServerName, либо InsecureSkipVerify должны быть указаны») | Используйте нативный для транспорта плейсхолдер: tls_server_name {http.reverse_proxy.upstream.host} |
Дублирующиеся ключи или мусорные строки в hosts.map | Обработчик map Caddy выдаёт жёсткую ошибку при дублирующемся входном ключе — одна плохая строка может уронить всю конфигурацию | Скрипт синхронизации нормализует пробелы, отбрасывает некорректные строки (отклоняя всё целиком только если плохих строк >20%), дедуплицирует по принципу «первый побеждает» и записывает UTF-8 без BOM (BOM портит первый ключ карты). API также фильтрует пустые/содержащие пробелы строки доменов на источнике |
Register-ScheduledTask -RepetitionDuration ([TimeSpan]::MaxValue) | Регистрация задачи молча завершается неудачей (XML вне диапазона, нетерминирующая ошибка) | Постройте повторение как CIM-экземпляр MSFT_TaskRepetitionPattern с Interval = "PT5M" и без длительности; добавьте ExecutionTimeLimit в 4 минуты (первый запуск от SYSTEM может зависнуть на холодном поиске TLS/CRL) |
Admin API привязан только к localhost:2019. Устаревший runtime-режим предоставлял его удалённо, чтобы Membership API мог отправлять конфигурации маршрутов; статический дизайн не требует удалённых пушей, и меньшая поверхность — намеренное решение. caddy reload (запускаемый локально скриптом синхронизации) — единственный потребитель admin API.
CaddyHelper в API (и конечные точки /membership/domains/caddy + /caddy/init) всё ещё существуют как путь отката к старому runtime-режиму конфигурации. Они запланированы к удалению, как только статический сервер докажет стабильность в течение пары недель — после этого authorize + hostmap останутся единственными точками интеграции.
Эксплуатация
- Логи: вращающиеся логи WinSW в
C:\caddy\(stdout/err службы — ошибки реверс-прокси попадают вcaddy-service.err.log); история синхронизации вC:\caddy\sync-hostmap.log. - Принудительное обновление карты:
Start-ScheduledTask -TaskName CaddyHostmapSync. - Изменение конфигурации: отредактируйте
C:\caddy\Caddyfile, затемcaddy validate+caddy reload(либоRestart-Service caddy— перезапуски безопасны по замыслу). - Массовое удаление доменов намеренно срабатывает защиту скрипта синхронизации от резкого сокращения; отложите старый
hosts.mapв сторону и перезапустите задачу, чтобы принять намеренное большое сокращение. - Инструкции по DNS для церквей неизменны навсегда: apex
A 3.23.251.61либоCNAME proxy.b1.church.
Связанные страницы
- Маршрутизация веб-сайтов и мульти-сайт — как проксируемый запрос разрешается в церковь/сайт в B1App
- Развёртывание API — развёртывание Membership API, который обслуживает
authorize/hostmap