В этой статье я расскажу о том, как автоматизировать заказ и продление сертификатов от 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 у acmeIssuer) указан адрес staging-сервера. Заменить его на боевой можно будет позже.

Применив эту конфигурацию, проследим весь путь заказа:

  1. Создание Certificate породило новый ресурс CertificateRequest:

    kubectl -n app describe certificate le-crt
    ...
    Created new CertificateRequest resource "le-crt-1127528680"
  2. В его описании — отметка о создании Order:

    kubectl -n app describe certificaterequests le-crt-1127528680
    …
    Created Order resource app/le-crt-1127528680-1805948596
  3. В Order описано, с какими параметрами проходит проверка и какой у неё текущий статус. Такая проверка осуществляется ресурсом Challenge:

    kubectl -n app describe order le-crt-1127528680-1805948596
    …
    Created Challenge resource "le-crt-1127528680-1805948596-1231544594" for domain "yet-another.website"
  4. Наконец, в подробностях этого ресурса содержится информация о статусе самой проверки:

    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-crt появится запись 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:

  1. Profile > API Tokens > Create Token.
  2. Выставляем права доступа следующим образом:
    • Permissions:
      • Zone — DNS — Edit
      • Zone — Zone — Read
    • Zone Resources:
      • Include — All Zones
  3. Копируем полученный после сохранения токен (например: 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-legokubernetes.io/tls-acme: "true", — которая требует задания Issuer по умолчанию при установке cert-manager с помощью параметров Helm (или в параметрах запуска контейнера менеджера).

Мы в компании не пользуемся этими вариантами и не можем их посоветовать ввиду непрозрачности используемых подходов к заказу SSL-сертификатов (а заодно — и к возникающих разного рода проблем), но все же решили упомянуть в статье для более полной картины.

Вместо заключения


Путём несложных манипуляций с CRD мы научились выписывать автопродляемые, самоподписанные и бесплатные SSL-сертификаты от проекта Let’s Encrypt для доменов сайтов, запущенных в рамках Ingress’ов в Kubernetes-кластерах.

В статье приведены примеры решения наиболее частых в нашей практике задач. Однако функции cert-manager не ограничиваются описанными выше возможностями. На сайте утилиты можно найти примеры работы с другими сервисами — например, связка с Vault или же использование внешних выпускающих центров (issuers).

P.S.


Читайте также в нашем блоге: