# TLS-сертификаты - X.509, цепочка доверия, Let's Encrypt _Безопасность · LinuxLab Knowledge Base_ **TL;DR:** TLS cert - X.509 объект с public key + identity (CN/SAN) + подписью CA. Цепочка: leaf → intermediate → root (доверенный OS). Let's Encrypt = бесплатный CA через ACME (HTTP-01/DNS-01 challenge). В k8s - cert-manager. ## Зачем 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 ``` Расшифровать локально: ```bash 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: ```nginx ssl_certificate /etc/ssl/fullchain.pem; # leaf + intermediates ssl_certificate_key /etc/ssl/privkey.pem; ``` Проверить цепочку: ```bash 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/ Сервер LE проверяет HTTP, видит token → выдаёт cert ``` - Работает только для одиночных доменов (не wildcard) - Требует open 80 на сервере с domain'ом **DNS-01:** ``` Запрос: создай TXT-запись _acme-challenge.example.com = Сервер LE проверяет DNS → выдаёт cert ``` - Работает для wildcard (`*.example.com`) - Требует API DNS-провайдера (Route53, Cloudflare, DigitalOcean) - Можно делать на любом сервере, не где работает домен ## certbot, стандартный клиент ```bash # 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//`: - `fullchain.pem`, leaf + intermediate (для nginx ssl_certificate) - `privkey.pem`, приватный ключ (chmod 600!) - `cert.pem`, только leaf - `chain.pem`, только intermediate Hooks для перезапуска сервиса: ```bash certbot renew --deploy-hook "systemctl reload nginx" ``` ## cert-manager, для Kubernetes Контроллер k8s, выдающий cert'ы как ресурсы. Поддерживает Let's Encrypt, HashiCorp Vault, Venafi, self-signed. ```yaml 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**: ```yaml 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 (`SelfSigned` issuer) Базовая последовательность вручную: ```bash # 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 <