SSL-сертификаты от Let's Encrypt с cert-manager в Kubernetes
В этой статье я расскажу о том, как автоматизировать заказ и продление сертификатов от Let«s Encrypt (и не только) для Ingress«а в Kubernetes с помощью дополнения cert-manager. Но начну с краткого введения в суть проблемы.
Немного ликбеза
Протокол HTTP, разработанный в начале 90-х годов прошлого века, вошёл в нашу повседневную жизнь настолько плотно, что тяжело представить хотя бы день без его использования. Однако сам по себе он не обеспечивает даже минимальный уровень безопасности при обмене информации между пользователем и веб-сервером. На помощь приходит HTTPS («S» — secure): используя упаковку передаваемых данных в SSL/TLS, этот протокол неплохо себя зарекомендовал в защите информации от перехвата и активно пропагандируется индустрией.
Например, Google с 2014 года придерживается позиции «HTTPS везде» и даже понижает приоритет сайтам без него в поисковой выдаче. Не обходит стороной эта «пропаганда» и рядовых потребителей: современные браузеры предупреждают своих пользователей о наличии и корректности SSL-сертификатов у посещаемых сайтов.
Стоимость сертификата для личного сайта начинается с десятков долларов. Не всегда покупка его оправдана и целесообразна. Благо, с конца 2015 года доступна бесплатная альтернатива в виде сертификатов Let«s Encrypt (LE). Этот некоммерческий проект был создан энтузиастами из Mozilla для того, чтобы покрыть большую часть интернет-сайтов шифрованием.
Центр сертификации выписывает сертификаты типа domain-validated (самые простые среди имеющихся на рынке) сроком действия в 90 дней, и уже не первый год возможен выпуск так называемого wildcard-сертификата на несколько поддоменов.
Для получения сайтом сертификата используются алгоритмы, описанные в протоколе Automated Certificate Management Environment (ACME), созданного специально для Let’s Encrypt. При его использовании подтверждение владения доменом осуществляется запросами через размещение определенного HTTP-кода (называется HTTP-01) или установку DNS-записей (DNS-01) — подробнее о них будет ниже.
Cert-manager
Cert-manager — специальный проект для Kubernetes, представляющий собой набор CustomResourceDefinitions (отсюда ограничение на минимально поддерживаемую версию K8s — v1.12) для конфигурации CA (удостоверяющих центров) и непосредственного заказа сертификатов. Установка CRD в кластер тривиальна и сводится к применению одного YAML-файла:
kubectl create ns cert-manager
kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v0.13.0/cert-manager.yaml
(Существует также и возможность установки с помощью Helm.)
Для инициирования процедуры заказа в кластере должны быть объявлены ресурсы центров сертификации (CA): Issuer
или ClusterIssuer
, — которые используются для подписи CSR (запросов на выпуск сертификата). Отличие первого ресурса от второго — в области видимости:
-
Issuer
может использоваться в рамках одного пространства имен, -
ClusterIssuer
является глобальным объектом кластера.
Практика с cert-manager
№1. Самоподписанный сертификат
Начнем с простейшего случая — заказа самоподписанного сертификата. Такой вариант довольно распространен, например, для динамических тестовых окружений для разработчиков или же в случае использования внешнего балансировщика, терминирующего SSL-трафик.
Ресурс Issuer
будет выглядеть так:
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: selfsigned
spec:
selfSigned: {}
А чтобы выпустить сертификат, необходимо описать ресурс Certificate
, где указывается, как произвести выпуск (см. раздел issuerRef
ниже) и где размещен приватный ключ (поле secretName
). После этого в Ingress потребуется сослаться на этот ключ (см. раздел tls
в spec
):
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: selfsigned-crt
spec:
secretName: tls-secret
issuerRef:
kind: Issuer
name: selfsigned
commonName: "yet-another.website"
dnsNames:
- "yet-another.website"
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: app
spec:
tls:
- hosts:
- "yet-another.website"
secretName: tls-secret
rules:
- host: "yet-another.website"
http:
paths:
- path: /
backend:
serviceName: app
servicePort: 8080
Через несколько секунд после добавления этих ресурсов в кластер сертификат будет выписан. Увидеть соответствующий отчёт можно в выводе команды:
kubectl -n app describe certificate selfsigned-crt
...
Normal GeneratedKey 5s cert-manager Generated a new private key
Normal Requested 5s cert-manager Created new CertificateRequest resource "selfsigned-crt-4198958557"
Normal Issued 5s cert-manager Certificate issued successfully
Если посмотреть на сам ресурс секрета, то в нём лежат:
- приватный ключ
tls.key
, - корневой сертификат
ca.crt
, - наш самоподписанный сертификат
tls.crt
.
Содержимое этих файлов можно увидеть с помощью утилиты openssl
, например, так:
kubectl -n app get secret tls-secret -ojson | jq -r '.data."tls.crt"' | base64 -d | openssl x509 -dates -noout -issuer
notBefore=Feb 10 21:01:59 2020 GMT
notAfter=May 10 21:01:59 2020 GMT
issuer=O = cert-manager, CN = yet-another.website
Стоит отметить, что в общем случае сертификату, выписанному с помощью такого Issuer
, подключаемые клиенты доверять не будут. Причина проста: он не имеет CA (см. примечание на сайте проекта). Чтобы этого избежать, нужно указать в Certificate
путь до файла секрета, где содержится ca.crt
. Таковым может быть и корпоративный CA организации — чтобы подписать выпускаемые для Ingress сертификаты ключом, уже используемым для нужд других серверных служб/информационных систем.
№2. Сертификат Let«s Encrypt с HTTP-валидацией
Для выпуска сертификатов LE, как упоминалось ранее, доступны два типа подтверждения владения доменом: HTTP-01 и DNS-01.
Первый подход (HTTP-01) заключается в запуске небольшого веб-сервера отдельным deployment«ом, который будет отдавать в интернет по ссылке
Второй вариант проверки выпускаемого сертификата (DNS-01) исходит из наличия API к DNS-серверу, где размещены записи домена. Issuer с помощью указанных токенов создает TXT-записи на домене, которые потом получает в ходе подтверждения ACME-сервер. Среди официально поддерживаемых DNS-провайдеров — CloudFlare, AWS Route53, Google CloudDNS и другие, в том числе и собственная реализация (acme-dns).
Примечание: у Let«s Encrypt существуют довольно строгие лимиты на запросы к ACME-серверам. Чтобы не попасть в длительный бан, для отладки рекомендуется использовать тип сертификата letsencrypt-staging (отличие только в ACME-сервере).
Итак, опишем ресурсы:
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt
solvers:
- http01:
ingress:
class: nginx
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: le-crt
spec:
secretName: tls-secret
issuerRef:
kind: Issuer
name: letsencrypt
commonName: yet-another.website
dnsNames:
- yet-another.website
Обратите внимание, что в качестве server
у acme
(в Issuer
) указан адрес staging-сервера. Заменить его на боевой можно будет позже.
Применив эту конфигурацию, проследим весь путь заказа:
- Создание
Certificate
породило новый ресурсCertificateRequest
:kubectl -n app describe certificate le-crt ... Created new CertificateRequest resource "le-crt-1127528680"
- В его описании — отметка о создании
Order
:kubectl -n app describe certificaterequests le-crt-1127528680 … Created Order resource app/le-crt-1127528680-1805948596
- В
Order
описано, с какими параметрами проходит проверка и какой у неё текущий статус. Такая проверка осуществляется ресурсомChallenge
:kubectl -n app describe order le-crt-1127528680-1805948596 … Created Challenge resource "le-crt-1127528680-1805948596-1231544594" for domain "yet-another.website"
- Наконец, в подробностях этого ресурса содержится информация о статусе самой проверки:
kubectl -n app describe challenges le-crt-1127528680-1805948596-1231544594 ... Reason: Successfully authorized domain ... Normal Started 2m45s cert-manager Challenge scheduled for processing Normal Presented 2m45s cert-manager Presented challenge using http-01 challenge mechanism Normal DomainVerified 2m22s cert-manager Domain "yet-another.website" verified with "http-01" validation
Если все условия были соблюдены (т.е. домен доступен снаружи, нет бана со стороны LE…) — меньше, чем через минуту, сертификат будет выпущен. В случае успеха в выводе describe certificate le-tls
появится запись Certificate issued successfully
.
Теперь можно смело менять адрес сервера на боевой (https://acme-v02.api.letsencrypt.org/directory
) и перезаказывать уже настоящие сертификаты, подписанные не Fake LE Intermediate X1
, а Let's Encrypt Authority X3
.
Для этого сначала потребуется удалить ресурс Certificate
: иначе никакие процедуры заказа не активируются, потому что сертификат уже есть и он актуален. Удаление секрета приведет к его немедленному возврату с сообщением в describe certificate
:
Normal PrivateKeyLost 44s cert-manager Lost private key for CertificateRequest "le-crt-613810377", deleting old resource
Остается применить «боевой» манифест для Issuer
с уже описанным выше Certificate
(он не изменился):
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt
solvers:
- http01:
ingress:
class: nginx
После получения сообщения Certificate issued successfully
в describe
проверим его:
kubectl -n app get secret tls-secret -ojson | jq -r '.data."tls.crt"' | base64 -d | openssl x509 -dates -noout -issuer
notBefore=Feb 10 21:11:48 2020 GMT
notAfter=May 10 21:11:48 2020 GMT
issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3
№3. Wildcard LE с валидацией через DNS
Усложним задачу, выписав сертификат сразу на все поддомены сайта и воспользовавшись на этот раз DNS-проверкой (через CloudFlare).
Для начала получим в панели управления CloudFlare токен для работы через API:
- Profile → API Tokens → Create Token.
- Выставляем права доступа следующим образом:
- Permissions:
- Zone — DNS — Edit
- Zone — Zone — Read
- Zone Resources:
- Include — All Zones
- Permissions:
- Копируем полученный после сохранения токен (например:
y_JNkgQwkroIsflbbYqYmBooyspN6BskXZpsiH4M
).
Создадим Secret, в котором будет храниться этот токен, и сошлемся на него в Issuer:
apiVersion: v1
kind: Secret
metadata:
name: cloudflare-api-token
type: Opaque
stringData:
api-token: y_JNkgQwkroIsflbbYqYmBooyspN6BskXZpsiH4M
---
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt
solvers:
- dns01:
cloudflare:
email: my-cloudflare-acc@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
apiVersion: cert-manager.io/v1alpha2
kind: Certificate
metadata:
name: le-crt
spec:
secretName: tls-secret
issuerRef:
kind: Issuer
name: letsencrypt
commonName: yet-another.website
dnsNames:
- "yet-another.website"
- "*.yet-another.website"
(Не забудьте про использование staging, если экспериментируете!)
Пройдем процедуру подтверждения владения доменом:
kubectl -n app describe challenges.acme.cert-manager.io le-crt-613810377-1285319347-3806582233
...
Status:
Presented: true
Processing: true
Reason: Waiting for dns-01 challenge propagation: DNS record for "yet-another.website" not yet propagated
State: pending
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Started 54s cert-manager Challenge scheduled for processing
Normal Presented 53s cert-manager Presented challenge using dns-01 challenge mechanism
В панели появится TXT-запись:
…, а через некоторое время статус сменится на:
Domain "yet-another.website" verified with "dns-01" validation
Убедимся в том, что сертификат валиден для любого поддомена:
kubectl -n app get secret tls-secret -ojson | jq -r '.data."tls.crt"' | base64 -d | openssl x509 -dates -noout -text |grep DNS:
DNS:*.yet-another.website, DNS:yet-another.website
Валидация по DNS, как правило, происходит не быстро, так как для большинства DNS-провайдеров характерен период обновления данных, показывающий, сколько времени пройдёт с момента изменения DNS-записи до фактического обновления всех DNS-серверов провайдера. Однако стандарт ACME предусматривает и комбинацию двух вариантов проверок, что можно использовать для ускорения получения сертификата на основной домен. В этом случае описание Issuer
будет следующим:
apiVersion: cert-manager.io/v1alpha2
kind: Issuer
metadata:
name: letsencrypt
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt
solvers:
- selector:
dnsNames:
- "*.yet-another.website"
dns01:
cloudflare:
email: my-cloudflare-acc@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
- selector:
dnsNames:
- "yet-another.website"
http01:
ingress:
class: nginx
Если применить эту конфигурацию, будут созданы два ресурса Challenge
:
kubectl -n app describe orders le-crt-613810377-1285319347
…
Normal Created 3m29s cert-manager Created Challenge resource "le-crt-613810377-1285319347-3996324737" for domain "yet-another.website"
Normal Created 3m29s cert-manager Created Challenge resource "le-crt-613810377-1285319347-1443470517" for domain "yet-another.website"
№4. Использование специальных аннотаций Ingress
Помимо прямого пути по созданию сертификатов в cert-manager есть возможность воспользоваться компонентом под названием ingress-shim и явно не создавать ресурсы Certificate
. Идея заключается в том, что с помощью специальных аннотаций Ingress«а сертификат будет автоматически заказан с помощью указанного в них Issuer
. В результате получается примерно следующий ресурс Ingress«а:
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt
spec:
tls:
- hosts:
- "yet-another.website"
secretName: tls-secret
rules:
- host: "yet-another.website"
http:
paths:
- path: /
backend:
serviceName: app
servicePort: 8080
Для корректной работы тут достаточно только наличия Issuer«а, то есть создавать на одну сущность меньше.
Кроме того, существует устаревшая аннотация kube-lego — kubernetes.io/tls-acme: "true"
, — которая требует задания Issuer
по умолчанию при установке cert-manager с помощью параметров Helm (или в параметрах запуска контейнера менеджера).
Мы в компании не пользуемся этими вариантами и не можем их посоветовать ввиду непрозрачности используемых подходов к заказу SSL-сертификатов (а заодно — и к возникающих разного рода проблем), но все же решили упомянуть в статье для более полной картины.
Вместо заключения
Путём несложных манипуляций с CRD мы научились выписывать автопродляемые, самоподписанные и бесплатные SSL-сертификаты от проекта Let«s Encrypt для доменов сайтов, запущенных в рамках Ingress«ов в Kubernetes-кластерах.
В статье приведены примеры решения наиболее частых в нашей практике задач. Однако функции cert-manager не ограничиваются описанными выше возможностями. На сайте утилиты можно найти примеры работы с другими сервисами — например, связка с Vault или же использование внешних выпускающих центров (issuers).
P.S.
Читайте также в нашем блоге: