step-ca на сервере: частые ошибки и решения
Внутренний CA на step-ca выглядит просто ровно до первого продакшен-инцидента: лист-сертификат не обновился по таймеру, клиент вдруг перестал доверять промежуточному CA после переустановки сервера, ACME-провайдер отдаёт 400 на обычный запрос. Ниже — ошибки, которые реально всплывают при эксплуатации step-ca для внутренних сервисов на VPS, и что с ними делать, без пересказа официального quickstart.
Содержание
- Инициализация CA: где теряются пароли и не туда смотрит --address
- Клиенты не доверяют CA: где реально теряется доверие
- ACME-провайдер step-ca: 400 Bad Request и rate-лимиты, о которых не пишут в логе по умолчанию
- Авторотация: почему `step ca renew` не подхватывается сервисом
- Сетевой доступ к CA: firewall, порт 9000 и SNI-роутинг через reverse proxy
- mTLS между внутренними сервисами: где именно рвётся цепочка доверия
- Мониторинг и диагностика: как поймать проблему до отказа сервиса
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →Инициализация CA: где теряются пароли и не туда смотрит --address
Первая же команда step ca init создаёт два ключа — root и intermediate — и просит пароль для каждого. Здесь основная ошибка новичков: пароль root-ключа записывают куда попало или, хуже, используют один и тот же пароль для root и intermediate, а потом не могут понять, какой файл какому соответствует при ручном восстановлении.
Правильная схема — сразу разносить пароли по разным secrets-файлам и root-ключ убирать из сети вообще:
step ca init \
--name "Internal CA" \
--dns ca.internal.example \
--address :9000 \
--provisioner admin@example \
--password-file /root/.step/root-pass \
--deployment-type standalone
После инициализации в /root/.step/certs/root_ca.crt лежит корневой сертификат, а /root/.step/secrets/root_ca_key — единственный ключ, который не должен постоянно жить на сервере с работающим демоном. Частая ошибка эксплуатации: root-ключ остаётся смонтированным рядом с intermediate, и любой, кто получил доступ к серверу, может переподписать саму CA. Практика, которая снимает риск: после инициализации скопировать root_ca_key в офлайн-хранилище (зашифрованный архив, отдельный volume, монтируемый вручную раз в год для ре-подписи intermediate) и удалить его с боевого хоста. Демон step-ca для повседневной работы использует только intermediate-ключ — это задаётся в ca.json полями crt/key в секции intermediateCA.
Ещё одна ошибка — --address :9000 слушает только IPv4 при отсутствии явного биндинга на dual-stack системах, и клиенты по IPv6 получают connection refused. Проверяется командой ss -tlnp | grep 9000: если видите только 0.0.0.0:9000, а сервисам нужен IPv6-доступ — биндите --address [::]:9000 или явно поднимайте reverse proxy (см. ниже).
Клиенты не доверяют CA: где реально теряется доверие
Самая частая жалоба — x509: certificate signed by unknown authority на клиенте при том, что сертификат выпущен корректно. Причина почти всегда одна из трёх:
- Клиент доверяет root, а сертификат подписан intermediate, но in-chain intermediate не отдаётся. step-ca по умолчанию отдаёт fullchain (leaf + intermediate) через
step ca certificate, но если сертификат генерировался вручную через ACME-клиент с неправильным--chain-флагом или сервис берёт только.crtбез промежуточного звена — проверка у клиента падает. Проверить фактическую цепочку:openssl s_client -connect service.internal:443 -showcerts </dev/null 2>/dev/null | grep -c "BEGIN CERTIFICATE". Если единица вместо двух — сертификат отдаётся без intermediate, нужно поправить конфигурацию сервиса (nginxssl_certificateдолжен указывать на fullchain.pem, а не на изолированный leaf).
- Root CA не завезли в trust store клиента. step-ca — приватный CA, публичные trust store (браузеры, ОС) о нём ничего не знают по определению. Root сертификат нужно раздать на все машины, которые обращаются к внутренним сервисам:
# Debian/Ubuntu
cp root_ca.crt /usr/local/share/ca-certificates/internal-ca.crt
update-ca-certificates
# RHEL/Alma/Rocky
cp root_ca.crt /etc/pki/ca-trust/source/anchors/internal-ca.crt
update-ca-trust extract
Для Docker-контейнеров это отдельный шаг: base-образ не наследует trust store хоста, root сертификат нужно монтировать и прогонять update-ca-certificates внутри самого образа при сборке — иначе внутренние HTTPS-запросы между контейнерами будут падать даже при рабочем CA на хосте.
- Пересоздали CA — старый root удалили, новый не завезли. Ловушка при переносе или пересоздании сервера: если
step ca initзапустили заново вместо восстановления из бэкапа, генерируется новая пара ключей с новым отпечатком, и все выданные ранее сертификаты вместе с записями в trust store клиентов становятся бесполезны разом. Бэкапить нужно весь/root/.stepцеликом, а не толькоca.json.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверACME-провайдер step-ca: 400 Bad Request и rate-лимиты, о которых не пишут в логе по умолчанию
step-ca умеет выступать как полноценный ACME-сервер — это удобно, когда сервисы (Caddy, cert-manager, certbot с --server) должны получать сертификаты автоматически, как с Let's Encrypt, но внутри периметра. Добавляется провайдер так:
step ca provisioner add acme --type ACME
Типичная ошибка после этого — ACME-клиент получает 400 malformed на newOrder, потому что домен не проходит DNS- или HTTP-01 challenge внутри закрытой сети: у step-ca нет собственного публичного резолвера, challenge должен реально резолвиться именно с того хоста, где крутится step-ca. Для внутренних доменов без публичного DNS почти всегда проще принудительно использовать DNS-01 через внутренний DNS-провайдер, либо переключиться на JWK/X5C-провизионер вместо ACME, если challenge физически негде подтвердить.
Второй источник тишины в логах — по умолчанию ca.json не логирует отклонённые ACME-запросы на уровне info. Включайте "logger": { "format": "json" } в конфиге и смотрите journalctl -u step-ca -f — без этого отладка ACME превращается в гадание по кодам ответа клиента.
Третье: у ACME-провайдера step-ca есть встроенные лимиты на количество заказов (claims.maxTLSCertDuration и неявные ограничения по частоте при большом числе провизионеров). При массовом выпуске сертификатов для десятков контейнеров разом клиенты начинают получать 429. Решение — не гонять bootstrap параллельно, а сериализовать выпуск через небольшую задержку в скрипте разворачивания.
Авторотация: почему `step ca renew` не подхватывается сервисом
Ключевая ценность step-ca для внутренних сервисов — короткоживущие сертификаты (по умолчанию 24 часа) с автоматической ротацией вместо ручного продления раз в год. Именно здесь чаще всего и ломается автоматизация.
Базовый вариант — systemd timer, который вызывает step ca renew и рестартует сервис только если сертификат реально обновился:
# /etc/systemd/system/step-renew.service
[Unit]
Description=Renew internal service certificate
[Service]
Type=oneshot
ExecStart=/usr/bin/step ca renew \
--force \
/etc/service/certs/tls.crt /etc/service/certs/tls.key
ExecStartPost=/usr/bin/systemctl reload service.service
# /etc/systemd/system/step-renew.timer
[Unit]
Description=Run cert renewal every 12 hours
[Timer]
OnCalendar=*-*-* 00,12:00:00
Persistent=true
[Install]
WantedBy=timers.target
Частые ошибки в этой связке:
--forceбез понимания логики продления. Флаг перезаписывает файл, даже если сертификат ещё свежий — при коротком TTL (24 часа) это нормально, но при более долгих сертификатах создаёт лишнюю нагрузку на CA. Без--forcestep-cli продлит сертификат, только если до истечения осталась примерно треть срока — это нужно понимать при отладке "почему сертификат не обновился".- Права на приватный ключ. step-ca пишет обновлённый
tls.keyот пользователя, из-под которого запущенstep— если сервис работает отwww-data, а renew отroot, сервис не сможет прочитать новый ключ и упадёт без TLS на следующем запуске. Фикс — запускать renew от того же пользователя либо явно выставлятьchownвExecStartPost. - Нет
ExecStartPostс reload. Файл сертификата обновится, но nginx/Caddy держат сертификат в памяти процесса и не перечитывают файл на диске безreload/SIGHUP. Без этого шага соединения продолжают идти по старому сертификату вплоть до полного перезапуска процесса. step ca renew --daemonв контейнере без healthcheck. Встроенный демон-режим удобен внутри Docker, но если контейнер убивают по OOM мимо штатногоSIGTERM, ротация прерывается посреди записи файла, и сервис получает битый сертификат. Надёжнее — sidecar-контейнер под ротацию с volume, разделяемым с основным сервисом, иhealthcheck, который проверяет валидность сертификата, а не просто "процесс жив".
Сетевой доступ к CA: firewall, порт 9000 и SNI-роутинг через reverse proxy
step-ca слушает свой собственный HTTPS-порт (по умолчанию 9000) — это отдельная точка, которую забывают открыть во внутреннем firewall при разворачивании новых нод. Симптом — клиент внутри VPN видит "connection timed out" при step ca bootstrap, хотя сам CA работает и локально отвечает: ufw allow from 10.0.0.0/8 to any port 9000 proto tcp.
Второй случай — CA спрятан за общим reverse proxy вместе с другими внутренними сервисами (см. nginx как reverse proxy на сервере: частые ошибки и решения). step-ca сам по себе — HTTPS-сервер с собственным TLS-терминированием, поэтому классический nginx-проксинг с proxy_pass https:// без правильной настройки SNI и proxy_ssl_verify либо рвёт handshake, либо незаметно проксирует без проверки цепочки:
location / {
proxy_pass https://127.0.0.1:9000;
proxy_ssl_server_name on;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/step-ca/certs/root_ca.crt;
}
Без proxy_ssl_server_name on step-ca может не находить нужный сертификат по SNI и падать в дефолтный ответ, что выглядит как "CA недоступен", хотя порт открыт и процесс жив.
mTLS между внутренними сервисами: где именно рвётся цепочка доверия
step-ca особенно оправдан, когда цель — не просто HTTPS, а взаимная аутентификация сервисов (mTLS) без внешнего Vault или полноценного service mesh. Здесь ошибки почти всегда одни и те же:
- Сервер проверяет клиентский сертификат по root, а клиент отдаёт leaf без intermediate в цепочке — сервер не может выстроить путь доверия и рвёт handshake на этапе
client verify. Решение то же, что и в разделе про доверие: клиент должен предъявлять fullchain, а не изолированный leaf. - CN/SAN сертификата не совпадает с именем, которое сервис ожидает при проверке клиента. step-ca генерирует SAN на основе имени, переданного при выпуске (
step ca certificate service-a.internal ...); если сервис проверяет клиента по CN, а не по SAN (встречается в legacy-библиотеках), возможны отказы уже после успешного TLS-хендшейка на уровне бизнес-логики. - Рассинхронизация времени между нодами. Короткоживущие сертификаты (24 часа и меньше) чувствительны к дрейфу системных часов — расхождение в несколько минут при TTL в час-два может привести к отклонению ещё не "начавшего действовать" сертификата. NTP-синхронизация (
chronyd/systemd-timesyncd) для инфраструктуры со step-ca — обязательное условие, а не опция.
Мониторинг и диагностика: как поймать проблему до отказа сервиса
Отдельная категория инцидентов — CA работал месяцами, ротация настроена один раз и забыта, а через полгода что-то в цепочке (диск, systemd timer, права после апдейта пакета) незаметно перестало работать, и сертификаты синхронно истекли одной ночью.
Минимальный набор проверок для мониторинга (см. также Grafana и Prometheus на сервере):
#!/bin/bash
CERT=/etc/service/certs/tls.crt
EXPIRY_EPOCH=$(date -d "$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)" +%s)
HOURS_LEFT=$(( (EXPIRY_EPOCH - $(date +%s)) / 3600 ))
if [ "$HOURS_LEFT" -lt 4 ]; then
echo "ALERT: cert expires in ${HOURS_LEFT}h" | logger -t cert-monitor
# здесь — curl вебхук в вашу систему алертов
fi
Помимо истечения сертификатов стоит проверять доступность самого демона (curl -s https://ca.internal:9000/health) и следить за диском под /root/.step/db — step-ca по умолчанию использует BadgerDB для хранения выданных сертификатов, и при агрессивной ротации база растёт быстрее, чем кажется на старте проекта.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли восстановить step-ca, если потерян только intermediate-ключ, а root цел?
Да — именно для этого root и хранится отдельно. Новый intermediate подписывается заново тем же root-ключом, и вся ранее выданная цепочка у клиентов остаётся валидной.
Нужен ли отдельный сервер под step-ca?
Для небольшой инфраструктуры держать step-ca на общем сервере — нормальная практика, если root-ключ вынесен в офлайн-хранилище. Для крупных или требовательных к комплаенсу инсталляций CA стоит выносить на отдельный узел с ограниченным сетевым доступом.
Чем step-ca отличается от certbot + Let's Encrypt для внутренних доменов?
Let's Encrypt не выпускает сертификаты для приватных, не резолвящихся публично доменов вида service.internal. step-ca закрывает этот случай — приватный CA, который вы полностью контролируете, с произвольным TTL и без внешних rate-лимитов.
Как понять, что сертификат обновился, не заходя в систему вручную?
Добавить проверку отпечатка (openssl x509 -fingerprint -sha256) в тот же cron-скрипт и сравнивать с предыдущим прогоном — изменение отпечатка при неизменном сроке действия подтверждает, что ротация прошла.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →