Зачем TLS-сертификаты
[[tls-handshake|TLS]] нужен для двух целей: encryption трафика и server authentication (вы общаетесь с настоящим bank.com, не с MITM). Аутентификация, на сертификатах.
Сертификат, это публичный ключ + identity (доменное имя)
- подпись доверенного третьего лица (CA, Certificate Authority). Браузер доверяет CA → доверяет подписанному CA сертификату → доверяет серверу с этим сертификатом.
Без сертификатов TLS-encryption ещё работает (anonymous DH), но вы не знаете, с кем шифруетесь.
X.509, формат
Стандарт ITU-T от 1988-го, в 2026-м всё ещё актуален. Сертификат ASN.1 DER blob, обычно в Base64-обёртке (PEM).
Структура поля:
Certificate
├── tbsCertificate (to-be-signed)
│ ├── version (v3 = 0x02)
│ ├── serialNumber
│ ├── signatureAlgorithm
│ ├── issuer (CN, O, C, кто подписал)
│ ├── validity (notBefore, notAfter)
│ ├── subject (CN, O, C, кому выдан)
│ ├── subjectPublicKeyInfo (publicKey + algorithm)
│ └── extensions
│ ├── SubjectAlternativeName (DNS:*.example.com, IP:1.2.3.4)
│ ├── KeyUsage (digitalSignature, keyEncipherment)
│ ├── ExtendedKeyUsage (serverAuth, clientAuth)
│ ├── BasicConstraints (CA:TRUE/FALSE)
│ └── AuthorityInfoAccess (OCSP, caIssuers URL)
├── signatureAlgorithm
└── signature
Расшифровать локально:
openssl x509 -in cert.pem -text -noout
# из живого сервера
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -text -noout
Где живёт identity, CN vs SAN
Раньше identity сервера была в CN (Common Name). С 2017 (RFC 6125, Chrome 58+) CN игнорируется, identity берётся только из SAN (Subject Alternative Name).
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com, DNS:*.api.example.com
Wildcard *.example.com матчит foo.example.com но не
bar.foo.example.com (только один уровень). Wildcard на root
(*.com) запрещён.
Если cert без SAN, браузер скажет NET::ERR_CERT_COMMON_NAME_INVALID
даже если CN правильный.
Цепочка доверия
Root CA (self-signed, в OS truststore)
│ подписывает
▼
Intermediate CA (в bundle сервера)
│ подписывает
▼
Leaf certificate (для example.com)
Сервер должен отдать leaf + intermediate (root уже у клиента).
Если intermediate не отдаётся, unable to get local issuer certificate на старых клиентах. nginx:
ssl_certificate /etc/ssl/fullchain.pem; # leaf + intermediates
ssl_certificate_key /etc/ssl/privkey.pem;
Проверить цепочку:
openssl s_client -connect example.com:443 -showcerts
curl https://example.com -v # покажет subject и issuer
Truststore OS, в /etc/ssl/certs/ca-certificates.crt (Debian),
/etc/pki/tls/certs/ca-bundle.crt (RHEL). Браузеры, свой
truststore (Mozilla NSS).
Let's Encrypt, бесплатный публичный CA
С 2016 покрывает 80%+ всех публичных HTTPS. Доверен браузерами и OS.
Особенности:
- Бесплатно, без лимита на количество доменов
- Срок 90 дней (вместо 1-2 лет у платных), стимул автоматизировать
- ACME-протокол (RFC 8555), стандартный, не Let's-Encrypt-only
- Rate-limit 50 certs / domain / week (есть staging без лимита)
- Подписывает через intermediate
R10/R11(RSA) илиE5/E6(ECDSA)
ACME challenges, как доказать владение
Перед выпуском cert ACME-сервер хочет proof'а, что вы владеете доменом. Два основных challenge'а:
HTTP-01:
Запрос: положи "challenge-token" в http://example.com/.well-known/acme-challenge/<key>
Сервер LE проверяет HTTP, видит token → выдаёт cert
- Работает только для одиночных доменов (не wildcard)
- Требует open 80 на сервере с domain'ом
DNS-01:
Запрос: создай TXT-запись _acme-challenge.example.com = <token>
Сервер LE проверяет DNS → выдаёт cert
- Работает для wildcard (
*.example.com) - Требует API DNS-провайдера (Route53, Cloudflare, DigitalOcean)
- Можно делать на любом сервере, не где работает домен
certbot, стандартный клиент
# standalone (certbot сам поднимет :80)
sudo certbot certonly --standalone -d example.com -d www.example.com
# nginx-плагин (модифицирует конфиг nginx)
sudo certbot --nginx -d example.com
# webroot (если у вас уже есть webserver)
sudo certbot certonly --webroot -w /var/www/html -d example.com
# DNS-01 для wildcard через Cloudflare
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials ~/.cf-creds.ini \
-d example.com -d '*.example.com'
# автообновление (ставится в systemd timer)
systemctl list-timers | grep certbot
Cert хранится в /etc/letsencrypt/live/<domain>/:
fullchain.pem, leaf + intermediate (для nginx ssl_certificate)privkey.pem, приватный ключ (chmod 600!)cert.pem, только leafchain.pem, только intermediate
Hooks для перезапуска сервиса:
certbot renew --deploy-hook "systemctl reload nginx"
cert-manager, для Kubernetes
Контроллер k8s, выдающий cert'ы как ресурсы. Поддерживает Let's Encrypt, HashiCorp Vault, Venafi, self-signed.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata: { name: letsencrypt-prod }spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef: { name: letsencrypt-prod-account-key }solvers:
- http01:
ingress: { class: nginx }- dns01:
cloudflare:
apiTokenSecretRef: { name: cf-api-token, key: api-token }selector:
dnsZones: ["example.com"]
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata: { name: api-tls }spec:
secretName: api-tls # k8s Secret куда положить
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- api.example.com
- "*.api.example.com"
Но в 95% случаев используют annotation на Ingress:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts: [api.example.com]
secretName: api-tls
cert-manager сам создаст Certificate, дойдёт challenge, положит cert в Secret, который Ingress подцепит.
Частный (internal) CA
Для intra-org (microservices, mTLS) часто нужен свой CA. Опции:
- OpenSSL руками, для одноразовых случаев
- smallstep / step-ca, небольшая утилита, простой бутстрап
- HashiCorp Vault PKI engine, на больших масштабах
- cfssl (Cloudflare), быстрый CLI
- k8s самоподписные через cert-manager (
SelfSignedissuer)
Базовая последовательность вручную:
# Root CA
openssl genrsa -out ca.key 4096
openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.crt \
-subj "/CN=My Internal CA"
# Server cert
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/CN=internal.example.com"
cat > san.cnf <<EOF
subjectAltName = DNS:internal.example.com,DNS:internal.local
EOF
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 365 -sha256 -extfile san.cnf -out server.crt
ca.crt распространить по серверам в truststore (update-ca-certificates).
Certificate Transparency
Все публичные CA обязаны публиковать выдаваемые cert'ы в CT-логах.
Хорошо для безопасности (узнаешь о подделке), и для recon, поиск
поддоменов по crt.sh:
https://crt.sh/?q=%25.example.com
OCSP и revocation
Если cert украли, надо отозвать. Старая CRL (Certificate Revocation List), медленная, толстая. Современная: OCSP (Online Certificate Status Protocol), клиент спрашивает CA «жив ли cert?», или OCSP stapling, сервер прикладывает свежий OCSP-response к TLS-handshake'у, экономит RTT.
В Let's Encrypt OCSP-stapling включается на nginx:
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/.../chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
Когда что-то пошло не так
SSL_ERROR_BAD_CERT_DOMAIN/NET::ERR_CERT_COMMON_NAME_INVALIDнет SAN или SAN не матчит host. Регенерь cert с правильным SAN.unable to get local issuer certificate, server не отдаёт intermediate. Используйfullchain.pem, не простоcert.pem.certificate has expired, забыл renewal.certbot renew --dry-runпроверь cron/systemd timer.- renew падает с rate limit, Let's Encrypt 5 cert/week/domain
при
--cert-nameсовпадениях. Используй staging для тестов (--server https://acme-staging-v02.api.letsencrypt.org/directory). - DNS-01 fail, token TTL слишком высокий. Уменьшить TTL DNS записей до 60s, или использовать DNS provider plugin.
- cert-manager Certificate stuck,
kubectl describe cert→kubectl describe order→kubectl describe challenge. Чаще всего: ACME http-01 challenge не достижим извне, или DNS provider не настроен. x509: certificate signed by unknown authorityв Go-приложении intermediate не в truststore приложения. Распространиca.crtлибоInsecureSkipVerify(но плохо).- mTLS не работает, клиент не знает CA сервера или vice versa.
openssl s_client -CAfile ca.crt -cert client.crt -key client.keyдля пошаговой отладки.