м. Київ, вул. Кирилівська 102

2 серпня 2026 р.

Сертифікати Let’s Encrypt через cert-manager у Kubernetes

Сертифікати Let’s Encrypt через cert-manager у Kubernetes

cert-manager автоматизує повний життєвий цикл TLS-сертифікатів у Kubernetes: створює запит, проходить ACME-перевірку домену, зберігає ключ і сертифікат у Secret, а потім поновлює їх до завершення строку дії. Разом із Let’s Encrypt це прибирає ручне копіювання PEM-файлів і нагадування в календарі, але лише за умови, що DNS, Ingress Controller та зовнішня маршрутизація вже працюють правильно.

У цьому матеріалі встановимо cert-manager з офіційного OCI Helm chart, створимо окремі ClusterIssuer для тестового й бойового ACME endpoint, випустимо сертифікат через HTTP-01 та підключимо його до Ingress. Для прикладу використовується ingress-nginx і поле `ingressClassName: nginx`. Якщо контролер входу ще не готовий, спочатку налаштуйте його за практичною інструкцією встановлення Nginx Ingress Controller.

Спершу завжди використовуйте staging endpoint Let’s Encrypt. Він видає недовірений браузерами тестовий сертифікат, зате дозволяє безпечно виправити DNS, firewall і solver, не витрачаючи production rate limits. Перехід на production issuer має бути окремою усвідомленою зміною після успішної перевірки всього ACME-маршруту.

Передумови для HTTP-01 і перевірка кластера

HTTP-01 підтверджує контроль над доменом через тимчасовий URL `/.well-known/acme-challenge/...`. Let’s Encrypt має дістатися до нього з інтернету через порт 80. cert-manager створює тимчасові Pod, Service та Ingress, а ingress-nginx спрямовує запит до solver. Якщо DNS вказує не на ту адресу або зовнішній firewall закриває порт 80, видача не завершиться.

Перевіримо контекст, IngressClass і зовнішню адресу контролера:

bash
kubectl config current-context
kubectl get nodes
kubectl get ingressclass
kubectl get service ingress-nginx-controller \
  -n ingress-nginx -o wide

DNS-запис домену повинен уже вести на зовнішню адресу ingress-nginx. Перевірте його з кількох resolver, а не лише з локального кешу:

bash
dig +short app.example.com A
dig +short app.example.com AAAA
curl -I http://app.example.com/

Якщо існує AAAA-запис, але IPv6-маршрут не працює, ACME-перевірка може піти через IPv6 і завершитися помилкою. Видаліть некоректний запис або налаштуйте повноцінну IPv6-доступність. Для доменів за CDN чи reverse proxy переконайтеся, що шлях challenge не блокується редиректами, WAF або власними правилами кешування.

HTTP-01 зручний для звичайних публічних host. Wildcard-сертифікат `*.example.com` через нього отримати не можна — для wildcard потрібен DNS-01 і API-доступ до DNS-провайдера. У цій статті використовуємо простіший HTTP-01 сценарій.

Встановлення cert-manager через офіційний OCI chart

cert-manager встановлюють один раз на кластер, а не як dependency кожного застосунку. Він керує cluster-scoped CRD, webhook та контролерами, тому версію потрібно фіксувати й оновлювати окремим інфраструктурним процесом.

Перевіримо Helm і відсутність попереднього release:

bash
helm version
helm list --all-namespaces | grep cert-manager || true
kubectl get namespace cert-manager 2>/dev/null || true

На момент підготовки прикладу офіційна документація використовує OCI chart `v1.21.0`. Перед виконанням звірте актуальну підтримувану версію та release notes. Встановимо chart разом із CRD:

bash
helm upgrade --install cert-manager \
  oci://quay.io/jetstack/charts/cert-manager \
  --version v1.21.0 \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true \
  --atomic \
  --wait \
  --timeout 10m

Перевіримо Deployment, webhook і CRD:

bash
kubectl get pods -n cert-manager
kubectl get deployment -n cert-manager
kubectl get crd | grep cert-manager.io
kubectl wait --namespace cert-manager \
  --for=condition=Available deployment/cert-manager-webhook \
  --timeout=180s

Не видаляйте CRD під час звичайного troubleshooting: видалення CustomResourceDefinition може видалити пов’язані Issuer, Certificate, Order і Challenge з усього кластера. Перед upgrade потрібно читати офіційні notes для переходу між конкретними версіями та мати резервну копію ресурсів.

Staging ClusterIssuer і перша ACME-перевірка

ClusterIssuer доступний для Ingress у всіх namespace. У спільному кластері це зручно, але потребує політики доступу: не кожна команда повинна мати можливість безконтрольно замовляти сертифікати на довільні домени.

Створимо `letsencrypt-staging.yaml`, замінивши email на справжню технічну адресу для повідомлень ACME:

yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:
    email: [email protected]
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-staging-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Застосуємо ресурс і дочекаємося стану Ready:

bash
kubectl apply -f letsencrypt-staging.yaml
kubectl get clusterissuer letsencrypt-staging
kubectl describe clusterissuer letsencrypt-staging
kubectl wait --for=condition=Ready \
  clusterissuer/letsencrypt-staging \
  --timeout=180s

Поле `privateKeySecretRef` стосується ACME-акаунта, а не сертифіката сайту. Secret створюється cert-manager і не повинен копіюватися між незалежними середовищами без продуманого плану. Для production endpoint використовуйте окремий issuer і окремий account key.

Якщо ClusterIssuer не стає Ready, перевірте доступ pod до ACME endpoint, DNS resolver, час на вузлах і логи controller. Проблеми webhook зазвичай проявляються ще під час `kubectl apply`, а помилки мережі до ACME — у status та events issuer.

Ingress із автоматичним TLS-сертифікатом

Припустимо, застосунок уже має Service `web-app` у namespace `web`. Додамо до Ingress annotation, яка вказує ClusterIssuer, і секцію `tls`. Значення `secretName` визначає Secret, куди cert-manager запише приватний ключ і виданий сертифікат.

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: web-app
  namespace: web
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-staging
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-app
                port:
                  number: 80
  tls:
    - hosts:
        - app.example.com
      secretName: app-example-com-tls

Застосуємо Ingress і простежимо ланцюжок ресурсів:

bash
kubectl apply -f web-app-ingress.yaml
kubectl get ingress,certificate -n web
kubectl get certificaterequest,order,challenge -n web
kubectl describe certificate app-example-com-tls -n web

Компонент ingress-shim побачить annotation і `tls.secretName`, створить Certificate, а далі cert-manager сформує CertificateRequest, Order та Challenge. Під час HTTP-01 у namespace тимчасово з’являться solver Pod, Service та Ingress.

bash
kubectl get pods,services,ingress -n web \
  -l acme.cert-manager.io/http01-solver=true

Після успішної перевірки Certificate стане Ready, а Secret міститиме `tls.crt` і `tls.key`:

bash
kubectl wait --for=condition=Ready \
  certificate/app-example-com-tls \
  -n web --timeout=300s
kubectl get secret app-example-com-tls -n web
curl -vkI https://app.example.com/

Staging CA не є довіреним, тому `curl` покаже помилку довіри без `-k`. Це очікувано. Важливо перевірити, що host, SAN, маршрутизація та автоматично створений Secret відповідають конфігурації.

Перехід на production issuer та поновлення

Після успішного staging challenge створимо окремий `letsencrypt-production.yaml`. Не редагуйте staging issuer, змінюючи endpoint на місці: окремі ресурси чіткіше відділяють тестовий і бойовий ACME-акаунти.

yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-production
spec:
  acme:
    email: [email protected]
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-production-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx

Застосуйте issuer, дочекайтеся Ready, а потім змініть annotation Ingress на production name:

bash
kubectl apply -f letsencrypt-production.yaml
kubectl wait --for=condition=Ready \
  clusterissuer/letsencrypt-production \
  --timeout=180s
kubectl annotate ingress web-app -n web \
  cert-manager.io/cluster-issuer=letsencrypt-production \
  --overwrite

Щоб зміна була durable, annotation потрібно оновити у Git-маніфесті або Helm values, а не залишати лише імперативну команду. Автоматизоване застосування issuer та Ingress логічно включити у побудову й оптимізацію CI/CD, зберігаючи production approval і перевірку kube-context.

cert-manager сам визначає час поновлення та оновлює той самий Secret. ingress-nginx підхоплює зміну без ручного копіювання файлів. Стан усіх сертифікатів зручно контролювати так:

bash
kubectl get certificates --all-namespaces
kubectl get certificate app-example-com-tls \
  -n web -o wide
kubectl describe certificate app-example-com-tls -n web

Не запускайте ручне перевидавання без розуміння причини: багато повторних спроб можуть упертися в rate limits. Спочатку виправте DNS, solver або доступність, а вже потім ініціюйте новий запит.

Типові помилки та висновок

Найчастіша помилка HTTP-01 — домен не веде на ingress-nginx або порт 80 недоступний з інтернету. Локальний curl може працювати через `/etc/hosts`, але Let’s Encrypt використовує публічний DNS. Перевіряйте A та AAAA записи, зовнішню адресу Service і firewall.

Друга проблема — неправильний `ingressClassName`. Якщо solver Ingress обробляє не той controller або жоден controller, challenge залишиться pending. Третя — редиректи, authentication чи WAF перехоплюють `/.well-known/acme-challenge/`. Четверта — порожні endpoints backend не блокують сам solver, але часто маскують загальну помилку ingress-маршруту.

Для діагностики починайте з Certificate і рухайтеся вниз до Challenge та solver:

bash
kubectl describe certificate app-example-com-tls -n web
kubectl get certificaterequest,order,challenge -n web
kubectl describe challenge -n web
kubectl logs -n cert-manager \
  deployment/cert-manager --tail=200

П’ята помилка — відразу використовувати production issuer під час налагодження. Staging endpoint існує саме для безпечних повторних тестів. Шоста — зберігати приватний `tls.key` у репозиторії. Ключ повинен залишатися в Kubernetes Secret або керованому сховищі з обмеженим доступом.

Надійна схема складається з чітких кроків: робочий Ingress Controller, правильний публічний DNS, доступний порт 80, встановлений cert-manager із CRD, Ready staging ClusterIssuer, успішний HTTP-01 challenge і лише потім production issuer. Після видачі потрібно контролювати Ready status та строки поновлення, а не просто перевірити зелений замок один раз.

Коли cert-manager, ingress-nginx і deployment pipeline описані декларативно, TLS стає повторюваною частиною платформи. Команда отримує автоматичне поновлення, однаковий процес для namespace та зрозумілий ланцюжок діагностики без ручного розкладання сертифікатів по серверах. Такий контур зазвичай підтримується разом з іншими DevOps-послугами, щоб безпека зовнішнього трафіку не залежала від пам’яті окремого адміністратора.

📞