Пост

Wildcard SSL-сертификат: один для всех поддоменов

🔐 Wildcard SSL-сертификат: один для всех поддоменов

Зачем он нужен

Если у вас десятки сервисов на поддоменах (grafana.example.ru, vpn.example.ru, ha.example.ru, npm.example.ru), управлять отдельными сертификатами для каждого — боль. Wildcard-сертификат покрывает все поддомены первого уровня:
*.example.rugrafana.example.ru, vpn.example.ru, ha.example.ru и т.д.

Важно: wildcard не покрывает вложенные уровни вида a.b.example.ru, и не покрывает сам apex-домен example.ru. Поэтому почти всегда выпускают два SAN: *.example.ru и example.ru.


Как это работает (коротко)

Wildcard можно выпустить только через DNS-01 challenge (ACME-протокол). CA (напр., Let’s Encrypt) проверяет, что вы управляете доменом, ища TXT-запись _acme-challenge.example.ru с нужным токеном.
Варианты валидации:

  • Ручная: разово добавить TXT и подтвердить.
  • Автоматическая: через API провайдера DNS (Cloudflare, DO, Yandex, Route53 и др.).
  • acme-dns: отдельный поддомен, делегированный для автоматизации TXT-записей.

Что выбрать: Certbot или acme.sh?

Оба отличные. Кратко:

  • Certbot — «дефолт» от Let’s Encrypt, много плагинов DNS.
  • acme.sh — очень лёгкий shell-скрипт, куча DNS-интеграций, просто ставится в cron, удобен в Docker.

Я покажу оба пути + кейсы для Nginx, Traefik, Nginx Proxy Manager и OPNsense.


Подготовка (чек-лист)

  1. Домен и доступ к DNS-зоне.
  2. Определитесь, что выпускаете: example.ru + *.example.ru.
  3. Решите, где хранить/использовать ключи: Nginx/Traefik/NPM/OPNsense/HAProxy.
  4. Выберите тип ключа:
    • ECDSA P-256 (короче и быстрее) — мой фаворит.
    • (Опционально) параллельно RSA 2048 для старых клиентов.
  5. Решите способ автоматизации DNS-валидации (API/плагин/Delegated acme-dns).

Вариант A: Certbot + DNS API (пример с Cloudflare)

1) Устанавливаем Certbot и плагин

1
2
3
# Debian/Ubuntu (пример)
sudo apt-get update
sudo apt-get install -y certbot python3-certbot-dns-cloudflare

2) Токен Cloudflare (минимальные права)

  • Zone → DNS: Edit
  • Zone → Zone: Read

Сохраните токен в файле, доступном только root:

1
2
3
4
5
sudo mkdir -p /etc/letsencrypt
sudo bash -c 'cat > /etc/letsencrypt/cloudflare.ini <<EOF
dns_cloudflare_api_token = CF_TOKEN_HERE
EOF'
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

3) Выпускаем wildcard + apex

1
sudo certbot certonly   --dns-cloudflare   --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini   -d "example.ru" -d "*.example.ru"   --preferred-chain "ISRG Root X1"   --key-type ecdsa   --agree-tos -m you@example.ru --no-eff-email

Где искать результаты:

1
2
3
4
/etc/letsencrypt/live/example.ru/
  ├─ fullchain.pem  # цепочка+сертификат
  ├─ privkey.pem    # приватный ключ
  └─ chain.pem

4) Автопродление + перезапуск сервисов

Let’s Encrypt выдаёт на 90 дней, обновляйте каждые ~60.

1
2
# Cron (два раза в день пробует, обновит только когда нужно)
echo '0 3,15 * * * root certbot renew --deploy-hook "systemctl reload nginx"'  | sudo tee /etc/cron.d/certbot

Если Nginx Proxy Manager, Traefik или Docker — см. ниже разделы интеграции.


Вариант B: Certbot + ручной DNS (разово)

Подходит, если нет API у провайдера и нужно «просто один раз».

1
sudo certbot certonly --manual --preferred-challenges dns   -d "example.ru" -d "*.example.ru"   --key-type ecdsa --agree-tos -m you@example.ru

Certbot попросит добавить TXT в _acme-challenge.example.ru. Добавьте две TXT (одна для apex, одна для wildcard), подождите 1–2 минуты (TTL/propagation), далее Enter.
Минусы: обновление вручную (неудобно).


Вариант C: acme.sh (ультралёгкий и гибкий)

1) Установка

1
2
curl https://get.acme.sh | sh -s email=you@example.ru
# После установки перезайдите в shell или source ~/.bashrc

2) Cloudflare токен и выпуск

1
2
3
export CF_Token="CF_TOKEN_HERE"
export CF_Account_ID="(не обяз.)"
acme.sh --issue --dns dns_cf   -d example.ru -d '*.example.ru'   --keylength ec-256

3) Установка (deploy) в вашу систему

Пример — развернуть в каталог Nginx и перезагрузить:

1
2
sudo mkdir -p /etc/nginx/ssl/example.ru
acme.sh --install-cert -d example.ru   --ecc   --fullchain-file /etc/nginx/ssl/example.ru/fullchain.pem   --key-file      /etc/nginx/ssl/example.ru/privkey.pem   --reloadcmd     "systemctl reload nginx"

acme.sh сам ставит cron на продление.


Интеграция: Nginx / Nginx Proxy Manager / Traefik / OPNsense

🔸 Nginx (стендэлон)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
server {
  listen 443 ssl http2;
  server_name grafana.example.ru;

  ssl_certificate     /etc/nginx/ssl/example.ru/fullchain.pem;
  ssl_certificate_key /etc/nginx/ssl/example.ru/privkey.pem;

  # (опционально) добавить RSA и ECDSA вместе:
  # ssl_certificate     /etc/nginx/ssl/example.ru/fullchain-ecdsa.pem;
  # ssl_certificate_key /etc/nginx/ssl/example.ru/privkey-ecdsa.pem;
  # ssl_certificate     /etc/nginx/ssl/example.ru/fullchain-rsa.pem;
  # ssl_certificate_key /etc/nginx/ssl/example.ru/privkey-rsa.pem;

  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

После обновления сертификата — systemctl reload nginx.

🔸 Nginx Proxy Manager (NPM)

  1. В NPM → SSL CertificatesAddCustom.
  2. Вставьте fullchain.pem и privkey.pem (из Certbot/acme.sh).
  3. Назначьте сертификат нужным Proxy Hosts.
  4. Для автопродления:
    • Либо используйте DNS-Challenge прямо в NPM (если DNS-провайдер поддерживается).
    • Либо обновляйте сертификат вне NPM (Certbot/acme.sh) и скриптом через NPM API переимпортируйте (hook --deploy-hook).

🔸 Traefik (Docker/K8s)

Проще всего — встроенный ACME (dnsChallenge). Пример traefik.yml:

1
2
3
4
5
6
7
8
9
10
11
certificatesResolvers:
  le-dns:
    acme:
      email: you@example.ru
      storage: /letsencrypt/acme.json
      dnsChallenge:
        provider: cloudflare
        resolvers:
          - "1.1.1.1:53"
          - "8.8.8.8:53"
      keyType: EC256

И роутер с SAN:

1
2
3
4
5
6
tls:
  certResolver: le-dns
  domains:
    - main: "example.ru"
      sans:
        - "*.example.ru"

Передайте токены провайдера как env CF_DNS_API_TOKEN и т.п.

🔸 OPNsense (ACME Client + HAProxy/напрямую)

  • Установите плагин ACME Client.
  • Добавьте Account (Let’s Encrypt / ZeroSSL).
  • Создайте Certificate:
    • FQDN: example.ru
    • Alt Names: *.example.ru
    • Validation Method: DNS-01 через ваш DNS-провайдер (введите API токен).
  • В Actions привяжите команды: перезапустить HAProxy/Nginx на OPNsense (если используете).
  • В HAProxy/Frontend укажите путь к сертификату, который ACME Client положит в систему сертификатов OPNsense.

    Плюс OPNsense: всё обновляется автоматически, включая DNS-валидацию.


Лучшие практики (важно!)

  • ECDSA по умолчанию, RSA как fallback при необходимости (старые клиенты).
  • Выпускайте SAN: example.ru + *.example.ru.
  • Держите автопродление: cron/systemd timers/Traefik auto.
  • Перезагрузка/перечтение сервисов post-renewal: --deploy-hook/--reloadcmd.
  • Разделяйте права: токен DNS с минимальными правами (только DNS Edit).
  • Проверьте CAA-записи в зоне: разрешите выбранный CA (например, Let’s Encrypt).
  • Уменьшите TTL TXT на время валидации до 60-120 секунд, потом верните обратно.
  • Не коммитьте ключи в Git. Закрывайте права 600 на приватные ключи.
  • Для массового хостинга подумайте о обособлении ключей на каждом узле (секрет-менеджеры/Ansible Vault/HashiCorp Vault).

Проверка сертификата

Быстрая проверка из CLI

1
echo | openssl s_client -connect grafana.example.ru:443 -servername grafana.example.ru 2>/dev/null   | openssl x509 -noout -issuer -subject -dates

Проверка цепочки

1
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /etc/nginx/ssl/example.ru/fullchain.pem

Проверка HTTP/2/TLS

1
curl -Iv https://grafana.example.ru

(Опционально) Прогон через SSL Labs (браузером) для внешней оценки.


Траблшутинг

  • No TXT record found for _acme-challenge
    Подождите propagation 1–3 минуты (или дольше у некоторых DNS), убедитесь, что добавили в нужную зону, без лишних кавычек.

  • CAA record prevents issuance
    В зоне стоят CAA, разрешающие другой CA. Добавьте CAA, разрешающий вашего CA (например, Let’s Encrypt).

  • Too many certificates already issued (rate limit)
    У Let’s Encrypt есть недельные лимиты на домен. Разворачивайтесь на staging во время тестов:
    1
    2
    
    --test-cert          # certbot
    acme.sh --staging    # acme.sh
    
  • Обновление не импортируется в NPM
    Используйте --deploy-hook для автоматического реимпорта через API NPM или включите native DNS-challenge NPM.

  • Traefik не берёт wildcard
    Проверьте, что включён dnsChallenge, а не httpChallenge. Для wildcard только DNS.

  • OPNsense ACME не проходит DNS-валидацию
    Проверьте корректность DNS API токена/прав и что выбран верный DNS-провайдер в плагине.

Примеры автоматизации (на сладкое)

Certbot: systemd-таймер вместо cron

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot Renew
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --deploy-hook "systemctl reload nginx"

# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Twice daily renew of Let's Encrypt certificates
[Timer]
OnCalendar=*-*-* 03,15:00:00
Persistent=true
[Install]
WantedBy=timers.target
1
sudo systemctl enable --now certbot-renew.timer

acme.sh: dual-stack (ECDSA + RSA) и деплой

1
2
3
4
5
6
7
8
9
# ECDSA
acme.sh --issue --dns dns_cf -d example.ru -d '*.example.ru' --keylength ec-256
# RSA
acme.sh --issue --dns dns_cf -d example.ru -d '*.example.ru' --keylength 2048

# Deploy обеих
acme.sh --install-cert -d example.ru --ecc   --fullchain-file /etc/nginx/ssl/example.ru/fullchain-ecdsa.pem   --key-file      /etc/nginx/ssl/example.ru/privkey-ecdsa.pem   --reloadcmd     "systemctl reload nginx"

acme.sh --install-cert -d example.ru   --fullchain-file /etc/nginx/ssl/example.ru/fullchain-rsa.pem   --key-file      /etc/nginx/ssl/example.ru/privkey-rsa.pem   --reloadcmd     "systemctl reload nginx"

И в Nginx:

1
2
3
4
ssl_certificate     /etc/nginx/ssl/example.ru/fullchain-ecdsa.pem;
ssl_certificate_key /etc/nginx/ssl/example.ru/privkey-ecdsa.pem;
ssl_certificate     /etc/nginx/ssl/example.ru/fullchain-rsa.pem;
ssl_certificate_key /etc/nginx/ssl/example.ru/privkey-rsa.pem;

Итоги

  • Wildcard даёт один сертификат для всех поддоменов первого уровня — меньше рутины, проще масштабирование.
  • Для wildcard обязателен DNS-01 challenge → используйте DNS API или Traefik/NPM/OPNsense с поддержкой DNS-валидации.
  • Настройте автопродление и перезагрузку сервисов hook’ами.
  • Выбирайте ECDSA как основной, добавляйте RSA для максимальной совместимости при необходимости.
Авторский пост защищен лицензией CC BY 4.0 .