Перейти к основному содержимому

Прокси Caddy для пользовательских доменов

Пользовательские церковные домены (mychurch.org → веб-сайт церкви на B1) завершаются на единственном Windows-сервере EC2, работающем под управлением Caddy. Этот сервер владеет TLS-сертификатами, разрешает каждый домен в его сайт {sub}.b1.church и выполняет реверс-проксирование с переписанным заголовком Host. Вся его конфигурация — это два файла: статический Caddyfile и hosts.map, обновляемый из Membership API, — так что он переживает перезапуски с нулевым состоянием времени выполнения. На этой странице описано, как этот сервер строится с нуля, как он работает, и подводные камни, проверенные на практике, о которые споткнётся любой, кто будет пересобирать его.

О том, как запрос разрешается в церковь/сайт после того, как он достигает B1App, см. Маршрутизация веб-сайтов и мульти-сайт.

Компоненты

ЧастьЧто это такое
Экземпляр EC2Windows 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/hostmaptext/plain, отсортированные, дедуплицированные строки {domain} {sub}.b1.church (с учётом сайта: домен, назначенный дополнительному сайту, набирает субдомен этого сайта). Источник для map

Поток запроса

  1. Браузер разрешает mychurch.org3.23.251.61 (запись apex A, либо CNAME proxy.b1.church).
  2. Caddy завершает TLS. Сертификат под рукой в S3 → обслуживание; неизвестный SNI → запрашивается authorize; 200 → выдача по требованию через Let's Encrypt; 404 → рукопожатие отклоняется (нет сертификата, нет ответа — неизвестный хост получает отказ на уровне TLS, а не HTTP-ошибку).
  3. map разрешает Host в {sub}.b1.church; www.{apex} получает 302 на апекс; авторизованный, но не сопоставленный хост (совсем новый домен внутри окна синхронизации ≤5 минут) получает чистую 404.
  4. reverse_proxy набирает {sub}.b1.church:443 с переписанными SNI и Host на вышестоящий сервер, так что граница Vercel обслуживает сайт B1App.
  5. Порт 80 пропускает вызовы ACME HTTP-01 и делает редирект 308 на https для всего остального.

Распространение нового домена: домен, сохранённый в B1Admin, становится маршрутизируемым в течение ~5 минут (задача синхронизации); его сертификат выпускается при первом обращении по HTTPS.

Сборка сервера с нуля

Сокращённо из проверенной на практике процедуры (полная пошаговая инструкция с командами для копирования-вставки живёт в рабочем пространстве эксплуатации, не в этом репозитории). Сначала предпосылки — без них сборка мертва:

  1. IAM: прикрепите CaddyRole (доступ S3 к бакету сертификатов) к экземпляру. Проверьте через IMDSv2 с сервера — учтите, что голый GET к IMDS, возвращающий 401, просто означает, что принудительно включён IMDSv2, а не «нет роли».
  2. Здоровье API: authorize должен вернуть 404 для фиктивного домена, а hostmap должен вернуть 200, прежде чем делать что-либо ещё.

Затем:

  1. Бинарный файл: скачайте пользовательскую сборку через сервис сборки 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 и не найдёт существующие сертификаты.
  2. Файлы: Caddyfile + sync-hostmap.ps1 в C:\caddy\; заполните карту один раз через sync-hostmap.ps1 -NoReload.
  3. Проверки перед первым запуском: caddy list-modules должен показывать модуль хранилища s3; caddy adapt должен выдавать "module":"s3","bucket":"churchapps-caddy-certs","region":"us-east-2","prefix":"certs" в своём блоке хранилища; caddy validate должен проходить успешно.
  4. Служба: установите через WinSW (id службы caddy, автоперезапуск при сбое, вращающиеся логи). Запускается от LocalSystem, что даёт доступ к IMDS для учётных данных роли.
  5. Задача синхронизации: зарегистрируйте CaddyHostmapSync (SYSTEM, каждые 5 минут + при запуске, лимит выполнения 4 минуты).
  6. Проверка перед переключением: принудительно разрешите домены на 127.0.0.1 через curl --resolve (у сервера нет реального трафика, пока EIP не переехал): существующий домен должен обслуживаться с валидным перенесённым сертификатом; www. должен давать 302; неизвестный хост должен получать отказ на уровне TLS; и Restart-Service caddy должна вернуться к обслуживанию без ручной повторной инициализации — этот тест перезапуска и есть весь смысл статического дизайна.
  7. Запуск в продакшн: переассоциируйте 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.

Устаревший runtime-пуш

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.

Связанные страницы