В этой статье я расскажу о том, как автоматизировать заказ и продление сертификатов от 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’ом, который будет отдавать в интернет по ссылке <YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN> некие данные, запрашиваемые с сервера сертификации. Следовательно, этот метод подразумевает доступность Ingress’а извне по 80 порту и разрешение DNS-записи домена в публичные IP-адреса.
Второй вариант проверки выпускаемого сертификата (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.
Читайте также в нашем блоге:
- «Введение в сетевые политики Kubernetes для специалистов по безопасности»;
- «Kubernetes tips & tricks: персонализированные страницы ошибок в NGINX Ingress»;
- «Обзор и сравнение контроллеров Ingress для Kubernetes».