Pull to refresh

Caddy и 100к доменов: автоматический SSL при одностраничном конфиге

Level of difficultyHard
Reading time7 min
Views2.8K
Nginx config vs Caddy TLS on Demand
Nginx config vs Caddy TLS on Demand

Я начал использовать Nginx более 20 лет назад, и как-то привык к тому что это решение по умолчанию при выборе веб сервера. В своем пути в IT я начинал с linux администрирования, потом был мелкий онлайн бизнес, работал бизнес аналитиком, продактом, временами что-то программировал для себя. Обстоятельства опять поменялись и год назад я устроился работать девопсом в маркетплейс доменов, по сути такой возврат к истокам. Первая задача которую мне выдали - перевести паркинг с 100к доменами с nginx на caddy. На тот момент я не слышал про Caddy, но был очень хорошего мнения о nginx. 

Я был удивлен, зачем?!
Что такого может быть в каком-то другом веб сервере, чего не умеет nginx? 

Я изучил нюансы, перевел паркинг на Caddy, и теперь могу уверенно заявить: да, у Caddy действительно есть очень сильные стороны. 

В этой статье я изложу кейс, нюансы, которые становятся важными когда у вас 100к клиентских доменов, на которых должен работать https. И какие тут есть преимущества у Caddy перед Nginx. На хабре есть всего несколько статей по Caddy, и это незаслужено мало для него. Поэтому я надеюсь из этого кейса вы сможете узнать что-то интересное.

Проблема 100k ssl сертификатов 

Есть ли какая-то сложность в том, чтобы настроить паркинг на 100к доменов с https?
На первый взгляд может показаться, а в чём сложность-то?
Для одного домена проблем с https вообще нет, можно на Let's Encrypt получить бесплатный сертификат и обновлять его certbot регулярно. 

Если задача решена для одного домена, повторяй это нужное количество раз и задача решена, за это мы и любим автоматизацию. Так и поступили ребята, проектировавшие паркинг. Клиенты добавляют новые домены в систему, прописывают выданные им NS, система добавляет новый домен в nginx и выпускает сертификат от Let's Encrypt.
Просто, понятно, отлично работает... когда у вас мало доменов. 

Когда доменов стали десятки тысяч, обнаружились нюансы. Суммарный вес конфига nginx на все домены превысил 400 мегабайт. С таким конфигом nginx перезапускается долго, минуты даунтайма. Перезапускать его каждый раз при добавлении нового домена стало совершенно невозможно, ведь новые домены добавляются постоянно.

TLS on Demand

В Caddy есть фича "TLS on Demand", позволяет сделать один конфиг на все домены, полный вайлдкард. Правда, тогда надо дать ему еще api, где бы Caddy проверял валидный ли домен, и стоит ли пробовать для него выпустить SSL сертификат. 

И уже одного этого достаточно, чтобы попробовать. 

Вот минимальная версия конфига, Caddyfile для того, чтобы обрабатывать произвольное количество доменов, выпуская на них сертификаты через Let's Encrypt при первом обращении. Продлевает истекающие сертификаты он также автоматически.

{
on_demand_tls {
ask http://api.example.com/check
}
}
:443 {
tls no-reply-caddy1-1@example.com {
on_demand
}
handle @valid_paths {
reverse_proxy parking.example.com:3000 {
header_up Host {host}
header_up X-SSL-Redirect "true"
}
}
}

Конфиг маленький, перезагрузка Caddy происходит практически мгновенно. А при добавлении новых доменов перезагрузка и вообще не требовалась. 

Перед тем как выпустить сертификат, Caddy стучится в указанный ему API, http://api.example.com/check?domain=client_domain.com 

И если получает ответ с кодом 200, пытается выпустить сертификат через Let's Encrypt. Это нужно в качестве защиты от DOS, чтобы атакующий, подставляя левые домены, не мог заставить Caddy обращаться в Let's Encrypt в попытках выпустить сертификаты на домены, владение которыми он не может подтвердить. 

Если же не получилось выпустить через Let's Encrypt, например, уперлись в лимит, или сервис временно недоступен, то Caddy выпустит сертификат через ZeroSSL.

Отказоустойчивость

Мы сочли, что для паркинга достаточно надежной будет система, которая продолжает работать при падении любой одной ноды веб сервера. Эта модель не специфична для Caddy, а может быть использована с любым веб сервером. Идея в том, что для самого домена и www создается 2 А записи, на разные IP, принадлежащие разным нодам. Браузер при попытке открыть сайт использует случайным образом один из этих двух IP и попадет на одну из двух нод. Если же одна из нод не отвечает, то браузер через примерно 30 секунд использует другой IP и сайт откроется.

Redis Sentinel для сертификатов

Так как у нас каждый домен должен обслуживаться на двух нодах, использование Redis хранилища гораздо удобнее чем файлов, для хранения сертификатов. Так как при использовании Redis любая нода выпускает сертификат, сохраняет его в Redis, и всем остальным нодам сертификат становится доступен, и им не придется его выпускать, расходуя ресурс процессора и лимит Let's Encrypt. 

Проще всего было бы использовать 1 Redis сервер. Но для надежности я использовал Redis Sentinel в минимальной конфигурации из трех нод. В Caddyfile ноды Redis Sentinel вписываются в основной секции

storage redis failover {
address {
1.2.3.4:26379
5.6.7.8:26379
9.1.2.3:26379
}
master_name caddy1
}

Про Redis чаще всего думают как про кеш базы данных, однако его можно использовать и как постоянное первичное хранилище. Caddy создает записи без срока истечения. При удалении клиентских доменов самописный модуль из маркета просто удаляет записи в Redis, находя их по имени домена.

Сборка с модулями

В стандартной сборке Caddy поддержки Redis кластера нет, чтобы ее добавить надо собрать вручную, добавив нужный модуль. Caddy написан на Go, компилируется как один бинарник без зависимостей, и собрав его на одном сервере, можно просто скопировать на все остальные. 

Первым делом надо поставить Go, если не стоит. 
Для сборки есть утилита xcaddy, которая все очень упрощает.

#go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
#cp /root/go/bin/xcaddy /usr/bin/

Собрать Caddy с модулями для хранения сертификатов в Redis, и модулем работы с PowerDNS

#xcaddy build \
--with github.com/pberkel/caddy-storage-redis \
--with
github.com/caddy-dns/powerdns \

#mv caddy /usr/bin/caddy
#chmod 755 /usr/bin/caddy

При ручной сборке у нас появляется только бинарник, и дополнительно надо сделать то, что обычно делает пакетный менеджер.

Добавить пользователя

# groupadd --system caddy
# useradd --system \
--gid caddy \
--create-home \
--home-dir /var/lib/caddy \
--shell /usr/sbin/nologin \
--comment "Caddy web server" \
caddy

Cоздать файл /etc/systemd/system/caddy.service с текстом 

[Unit]
Description=Caddy
Documentation=
https://caddyserver.com/docs/
After=network.target network-online.target
Requires=
network-online.target

[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
Environment="POWERDNS_API_TOKEN=secret_token"
Environment="POWERDNS_SERVER_URL=
http://1.2.3.3:8081"

[Install]
WantedBy=
multi-user.target

Перезагрузить конфигурацию

#systemctl daemon-reload
#systemctl enable --now caddy

DNS для подтверждения владения доменом

Подтверждение владения доменом для Let's Encrypt у нас делается через DNS, у нас уже использовался PowerDNS, и было достаточно добавить в Caddy модуль, прописать доступы к API в /etc/systemd/system/caddy.service 

И добавить использование PowerDNS в Caddyfile в секцию tls

tls no-reply-caddy1-1@example.com {
on_demand
dns powerdns {env.POWERDNS_SERVER_URL} {env.POWERDNS_API_TOKEN}
}

Это было бизнес требованием, чтобы если у нас поступит разово, например, 20к доменов, они не висели с ошибкой SSL, ожидая когда обновятся лимиты Let's Encrypt для получения сертификата, а временно редиректили на страницу паркинга example.com/domain/domain.com

Лимиты Let’s Encrypt 

У Let's Encrypt есть лимит на количество выпускаемых сертификатов, 300 сертификатов за 3 часа с одного аккаунта. Также есть лимит на 10 аккунтов с одного IP Аккаунт регистрировать не требуется, это просто емейл который передается Let's Encrypt вместе с именем домена, при запросе на выпуск сертификата. Подтверждать этот емейл не требуется, будет работать даже с несуществующей почтой. Суть этой почты в том, что туда Let's Encrypt будет слать письма в случае каких-то проблем с сертификатом. 

Если мы будем регулярно менять почту указываемую при выпуске сертификата, то с одного IP мы сможем выпускать 3000 сертификатов за 3 часа. Для Nginx у нас использовалась схема, когда один сертификат выпускался одновременно для самого домена и www. Однако, у Caddy политика выпуска сертификатов строго 1 домен 1 сертификат, то есть на один домен нам приходится выпускать 2 сертификата, для самого домена и для www. Конечно, Caddy все это делает сам, но нам надо знать про это чтобы понимать сколько доменов сможет обработать нода, до того как упрется в лимит выпуска сертификатов. 

Максимум мы сможем выпускать 3000 сертификатов за 3 часа, и обрабатывать 1500 новых доменов на одной ноде. Усредненно максимум 500 доменов в час, или 12к в сутки. Цифра внушительная, но у нас уже были случаи когда 1 клиент за раз заливал 20к доменов на паркинг. 

Если же равномерно делать выпуск сертификатов с 3х нод, то можно достичь скорости выпуска сертификатов для новых доменов 36к в сутки, что уже закрывало бизнес требование "желательно чтоб все новые домены получали сертификат за 24 часа". 

Для того чтобы регулярно менять аккаунт, я воспользовался возможностью перезаписывать любую часть активного конфига Caddy прямо в памяти по API. Достаточно простого скрипта в cron который считывает значение текущего емейла и меняет на "следующий".
Мой скрипт на python https://github.com/akamitch/caddy_change_email/blob/main/caddy_change_email.py 

Вначале я поставил cron-скрипт раз в 5 минут, но потом обнаружил неприятный момент, если первый клик на домен приходил как раз перед переключением акка, то выпуск сертификата заканчивался ошибкой. Да, уже при следующем клике сертификат выпускается, но все равно неприятно. Поэтому в итоге поставил раз в 20 минут.

*/20 * * * * /usr/bin/python3 /usr/local/bin/caddy_change_email.py

Производительность

Паркинг на caddy стабильно работает уже более полугода.
Всего там сейчас держит примерно 120к доменов, 4 ноды caddy.

Вот как в Grafana выглядит нагрузка на одной из нод.
Вот как в Grafana выглядит нагрузка на одной из нод.

Виртуалка на облачном хостинге за 50$/мес: 1 vCPU, 8 гиг памяти, NVMe

Кроме Caddy там еще одна из нод Redis, синхронизация у него на диск стоит раз в час, что видно в пиках на IO и CPU

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+8
Comments6

Articles