MAATRIX / Блог / step-ca в Docker Compose: готовый файл

step-ca в Docker Compose: готовый файл

MAATRIX

Когда внутренних сервисов на сервере больше пяти — Grafana, Portainer, pgAdmin, внутренние API — самоподписанные сертификаты и ручное подтверждение "Продолжить, я знаю о риске" в браузере превращаются в рутину, а Let's Encrypt для сервисов без белого домена и вовсе не подходит. Решение — поднять свой центр сертификации на базе step-ca от Smallstep: он выдаёт сертификаты по протоколу ACME, как Let's Encrypt, но для внутренней сети, и умеет ротировать их автоматически. Ниже — рабочий docker-compose.yml, инициализация и подключение к Traefik.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что такое step-ca и когда он нужен на своём сервере

step-ca — это открытый (Apache 2.0) центр сертификации от команды Smallstep, тот же движок, что используется в Netflix и в ряде корпоративных PKI. По сути это ACME-сервер, который вы полностью контролируете: он подписывает сертификаты для доменов и IP-адресов, которые не видны из интернета, и делает это без ручного участия — клиенты запрашивают сертификат по тому же протоколу, что используют certbot или acme.sh для Let's Encrypt, только точка входа — не acme-v02.api.letsencrypt.org, а ваш собственный сервер.

Типичные сценарии, где это оправдано:

  • mTLS между микросервисами — каждый контейнер получает клиентский сертификат, и сервисы проверяют друг друга, а не просто доверяют сетевой изоляции Docker.
  • Внутренние панели без белого домена — Grafana, Portainer, Kibana доступны только по VPN или из локальной сети, но браузер всё равно должен показывать закрытый замок, а не предупреждение.
  • Короткоживущие сертификаты вместо длинных паролей — вместо API-ключа сервис предъявляет сертификат со сроком жизни 24 часа, украденный ключ бесполезен уже на следующий день.

Если у вас один-два внутренних сервиса и есть возможность прокинуть DNS через Cloudflare с DNS-01 challenge — проще обойтись обычным Let's Encrypt (см. статью про certbot и acme.sh). step-ca нужен, когда сервисов много, домены внутренние, а сертификаты должны обновляться без вашего участия.

Готовый docker-compose.yml для step-ca

Ниже минимальная, но production-пригодная конфигурация. Она использует встроенный в официальный образ механизм автоинициализации через переменные DOCKER_STEPCA_INIT_* — при первом запуске контейнер сам создаст root- и intermediate-сертификаты и провижионеры, без интерактивного диалога.

services:
  step-ca:
    image: smallstep/step-ca:latest
    container_name: step-ca
    restart: unless-stopped
    ports:
      - "9000:9000"
    volumes:
      - step-data:/home/step
    environment:
      DOCKER_STEPCA_INIT_NAME: "Internal CA"
      DOCKER_STEPCA_INIT_DNS_NAMES: "step-ca,step-ca.internal,10.0.0.5"
      DOCKER_STEPCA_INIT_PROVISIONER_NAME: "admin"
      DOCKER_STEPCA_INIT_ACME: "true"
      DOCKER_STEPCA_INIT_PASSWORD_FILE: /home/step/secrets/password
    secrets:
      - source: stepca_password
        target: /home/step/secrets/password
    networks:
      - internal
    healthcheck:
      test: ["CMD", "step", "ca", "health"]
      interval: 30s
      timeout: 5s
      retries: 3

secrets:
  stepca_password:
    file: ./secrets/stepca_password.txt

volumes:
  step-data:

networks:
  internal:
    driver: bridge

Что здесь важно:

  • DOCKER_STEPCA_INIT_DNS_NAMES — все имена и IP, по которым к CA будут обращаться клиенты: DNS-имя контейнера в сети Compose (step-ca), внутреннее доменное имя, при необходимости — IP сервера. Этот список попадёт в SAN сертификата самого API step-ca.
  • DOCKER_STEPCA_INIT_ACME: "true" — сразу поднимает ACME-провижионер с именем acme, через него и будут выдаваться сертификаты клиентам (Traefik, certbot, acme.sh).
  • Пароль через Docker secret, а не голым текстом в environment — это тот же принцип, что разобран в статье про Docker secrets: пароль шифрования приватных ключей CA не должен светиться в docker inspect или логах.
  • Отдельная сеть internal — step-ca не нужно публиковать наружу вообще, если все клиенты — контейнеры на этом же сервере; порт 9000 можно даже убрать из ports, оставив сервис доступным только внутри Docker-сети (тип bridge разобран в статье про сети Docker).

Файл с паролем создайте заранее и закройте права:

mkdir -p secrets
openssl rand -base64 32 > secrets/stepca_password.txt
chmod 600 secrets/stepca_password.txt

Дальше — docker compose up -d. При первом старте в логах (docker compose logs step-ca) появится вывод инициализации с отпечатком (fingerprint) корневого сертификата — сохраните его, он понадобится для bootstrap-а клиентов.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Инициализация: root CA, intermediate CA и провижионеры

step-ca по умолчанию создаёт двухуровневую иерархию: root CA подписывает intermediate CA, а уже intermediate подписывает конечные (leaf) сертификаты сервисов. Это стандартная практика PKI — приватный ключ root можно после инициализации вынести в офлайн-хранилище, а онлайн-сервис step-ca работает только с ключом intermediate. Если он скомпрометирован — вы пересоздаёте только его, не трогая root и не переустанавливая доверие на всех клиентах.

После автоинициализации внутри volume step-data появится структура:

/home/step/
├── certs/
│   ├── root_ca.crt
│   └── intermediate_ca.crt
├── secrets/
│   ├── root_ca_key
│   ├── intermediate_ca_key
│   └── password
└── config/
    └── ca.json

Отпечаток root-сертификата, если нужно получить его повторно:

docker compose exec step-ca step certificate fingerprint /home/step/certs/root_ca.crt

Провижионеры — это способы аутентификации клиента перед выдачей сертификата. По умолчанию создаются два: admin (JWK-провижионер, вход по логину/паролю через step ca certificate) и acme (для автоматических клиентов). Посмотреть список:

docker compose exec step-ca step ca provisioner list

Добавить ещё один ACME-провижионер под конкретную группу сервисов (например, отдельно для mTLS между бэкендами) можно так:

docker compose exec step-ca step ca provisioner add acme-internal --type ACME

ACME-провижионер: автоматическая выдача и ротация

Главное отличие step-ca от самоподписанного сертификата, сгенерированного openssl один раз — сертификаты здесь короткоживущие по умолчанию (обычно 24 часа для leaf-сертификатов, если не менять ca.json) и ротируются автоматически.

Вручную, для проверки, что всё работает — выпуск сертификата провижионером admin:

docker compose exec step-ca step ca bootstrap \
  --ca-url https://step-ca:9000 \
  --fingerprint <FINGERPRINT_ИЗ_ЛОГОВ>

docker compose exec step-ca step ca certificate \
  service.internal service.crt service.key \
  --provisioner admin

Флаг bootstrap сохраняет корневой сертификат в локальный $STEPPATH клиента и добавляет его в доверенные — без этого шага TLS-клиент откажется доверять сертификатам вашего CA, как браузер ругается на самоподписанный сертификат.

Автоматическая ротация — через демон обновления, который следит за сроком жизни и перевыпускает сертификат заранее (обычно в последней трети срока жизни):

step ca renew --daemon \
  --exec "nginx -s reload" \
  service.crt service.key

Для сервисов, которые сами говорят по ACME (Traefik, Caddy, certbot, acme.sh), ротацию делает сам клиент — step-ca для них ничем не отличается от настоящего Let's Encrypt, кроме URL directory-эндпоинта:

https://step-ca:9000/acme/acme/directory

Подключение Traefik и других сервисов к step-ca

Самый частый кейс — отдать step-ca в качестве CA-сервера для Traefik, чтобы он сам получал и обновлял сертификаты для внутренних поддоменов. Если Traefik уже настроен как реверс-прокси (см. Traefik как реверс-прокси для Docker), добавление step-ca — это ещё один certificatesResolver:

# traefik.yml
certificatesResolvers:
  stepca:
    acme:
      caServer: "https://step-ca:9000/acme/acme/directory"
      email: "admin@internal.local"
      storage: "/letsencrypt/acme-stepca.json"
      httpChallenge:
        entryPoint: web

Ключевой нюанс: Traefik (как и большинство ACME-клиентов на базе библиотеки lego) должен доверять сертификату самого API step-ca, иначе TLS-хендшейк к caServer не пройдёт. Для этого в контейнер нужно смонтировать root_ca.crt и указать переменную окружения:

  traefik:
    environment:
      - LEGO_CA_CERTIFICATES=/certs/root_ca.crt
    volumes:
      - step-data:/certs:ro

где step-data — тот же volume, что у step-ca (можно смонтировать read-only, чтобы Traefik видел certs/root_ca.crt). После этого в лейблах роутера указываете traefik.http.routers.<имя>.tls.certresolver=stepca — и Traefik сам запросит, а потом будет обновлять сертификат.

Для клиентов вне Docker-сети (например, certbot на другом сервере) сначала нужно установить root-сертификат в системное хранилище доверия, иначе сама проверка TLS до directory-эндпоинта завершится ошибкой:

cp root_ca.crt /usr/local/share/ca-certificates/step-ca.crt
update-ca-certificates
certbot certonly --server https://ca.internal:9000/acme/acme/directory \
  --standalone -d service.internal

acme.sh умеет то же самое через флаг --ca-bundle, без установки в системное хранилище:

acme.sh --server https://ca.internal:9000/acme/acme/directory \
  --issue -d service.internal --standalone \
  --ca-bundle /path/to/root_ca.crt

Бэкап, безопасность ключей и типичные ошибки

step-ca — это не просто ещё один контейнер, это корень доверия для всей внутренней инфраструктуры: если потерян intermediate_ca_key, придётся переустанавливать доверие на всех клиентах заново, а это дольше, чем кажется на бумаге.

Что бэкапить обязательно:

ФайлКритичностьКомментарий
secrets/root_ca_keyмаксимальнаяЛучше вообще вынести в офлайн-хранилище после инициализации
secrets/intermediate_ca_keyвысокаяКомпрометация = переподписание всей цепочки
secrets/passwordвысокаяБез него ключи выше бесполезны, но хранить отдельно от ключей
config/ca.jsonсредняяКонфигурация провижионеров, легко восстановить руками, но проще с бэкапом

Простейший вариант — регулярный docker run --rm -v step-data:/data -v $(pwd):/backup alpine tar czf /backup/step-ca-backup.tar.gz -C /data . в cron, с последующей загрузкой архива в отдельное защищённое хранилище, а не рядом с самим сервером.

Частые ошибки:

  • "x509: certificate signed by unknown authority" — клиент не прошёл step ca bootstrap или не получил root_ca.crt в доверенные. Это самая частая жалоба новичков со step-ca.
  • Контейнер не стартует после перезапуска — если переменные DOCKER_STEPCA_INIT_* остались в файле, а volume уже проинициализирован, entrypoint видит существующий ca.json и пропускает повторную инициализацию. Проблема начинается, если volume удалили, а переменные — нет: CA пересоздастся с новым root-сертификатом, и все ранее выпущенные сертификаты перестанут быть доверенными.
  • Рассинхронизация времени — короткоживущие сертификаты (сутки) чувствительны к дрейфу часов на клиентах; NTP на всех узлах обязателен.
  • Один провижионер на все сервисы — практичнее сразу завести отдельные ACME-провижионеры под разные группы (внешние сервисы, mTLS, CI-раннеры), чтобы при компрометации одного отозвать доступ можно было точечно.

Нужен сервер под эту задачу?

Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Арендовать сервер

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.

Частые вопросы

Нужен ли step-ca, если сервисы и так закрыты VPN или firewall-ом?

Сетевая изоляция не даёт браузеру зелёного замка и не решает mTLS-задачи — step-ca закрывает уровень TLS-доверия, а не сетевой доступ, это дополняющие меры.

Можно ли использовать step-ca вместе с публичными сертификатами Let's Encrypt на том же сервере?

Да, это разные certificatesResolver в Traefik или отдельные вызовы certbot/acme.sh с разным --server — публичные домены идут через Let's Encrypt, внутренние — через step-ca, конфликта нет.

Что будет с уже выпущенными сертификатами, если контейнер step-ca упадёт?

Они продолжат работать до истечения срока (обычно 24 часа), но новые выдачи и продления прекратятся, поэтому мониторинг доступности step-ca важен не меньше, чем мониторинг самих сервисов.

Как отозвать скомпрометированный сертификат?

step ca revoke <serial> добавляет его в CRL/OCSP-ответ CA; для коротких сроков жизни это часто менее критично, чем для годовых сертификатов, но механизм работает так же.

Стоит ли выносить step-ca на отдельный сервер?

Для небольшой инфраструктуры не обязательно, достаточно отдельного volume с регулярным бэкапом; для крупной PKI root CA стоит держать офлайн, а online CA — на изолированном хосте.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →