Docker + TLS: Let’s Encrypt в контейнерах і на хості — обирайте безпечний підхід

Було корисно?

О 02:13 ваш телефон викликає знайомий страх: «клієнти не можуть увійти». Ви відкриваєте панель і бачите це: збої TLS-процесу рукостискання, каскад помилок 525/526 і сертифікат, що закінчився… учора. Сервіс працює. Ланцюжок сертифікації — ні. Саме так «прості HTTPS» перетворюються на інциденти в продукції.

Docker тут не винен прямо. Але Docker зробив легшим приховування гострих країв: приватні ключі в ефермерних файлових системах, оновлення, що виконуються в неправильному неймспейсі, ACME-челенджі, що застрягли за проксі, і підхід «просто змонтувати /etc/letsencrypt кудись», який перетворюється на цирк з правами доступу та ротацією.

Рішення: де повинна бути Let’s Encrypt

У вас є два загальні варіанти:

  1. ACME-клієнт запускається на хості (або на відокремленій «сертифікатній» VM/неймспейсі хоста) і записує сертифікати в контрольоване місце; контейнери читають їх тільки для читання.
  2. ACME-клієнт запускається всередині контейнера (Certbot, lego, Traefik, Caddy, nginx-proxy companion тощо) і записує сертифікати в том, який інші контейнери читають.

Якщо ви працюєте з продукційними системами і вам важливий радіус ураження, безпечний дефолт такий: завершуйте TLS на виділеному контейнері реверс-проксі, і виконуйте випуск/оновлення сертифікатів у тій самій межі (проксі з рідною ACME, наприклад Traefik/Caddy) або на хості з жорсткими правами доступу до файлів. Все інше може йти внутрішнім HTTP.

Чого слід уникати: «кожен контейнер додатка управляє своїм власним Let’s Encrypt». Це виглядає модульно. Насправді це генератор відмов обслуговування (rate limits), нічний кошмар для спостереження і подарунок тим, хто хоче ваші приватні ключі розсіяними по записуваних томах.

Ось правило з позицією: централізуйте сертифікати за точкою входу. Якщо публічний Інтернет потрапляє в одне місце — це місце володіє TLS. Контейнери додатків не повинні знати, що таке ACME.

Факти та історія, що змінюють операційну поведінку

  • Let’s Encrypt запустили в 2015 році і зробили автоматизацію очікуванням, а не розкішшю. Операційна планка змістилась: «ми оновлюємо за календарем» перестало бути прийнятним.
  • ACME став стандартом IETF (RFC 8555) у 2019 році. Це важливо, бо клієнти замінні; робочий процес — не якась прихована фішка постачальника.
  • Сертифікати короткочасні за дизайном (90 днів у Let’s Encrypt). Це не скупість; це зменшення вікна шкоди у випадку витоку приватного ключа.
  • Ліміти частоти — частина моделі безпеки. Вони також карають «retry-цикли», коли ваша розгортка постійно невдало проходить челенджі щохвилини.
  • HTTP-01 вимагає доступності порту 80 для домену. Якщо ви примушуєте HTTPS-тільки без продуманого винятку, ви зламаєте видачу сертифікатів у найгірший момент.
  • DNS-01 не вимагає вхідних портів. Це варіант для закритих середовищ, але він переносить ризик на облікові дані DNS API.
  • Wildcard-сертифікати вимагають DNS-01 у Let’s Encrypt. Якщо ваш план залежить від wildcard, ви вже обрали тип челенджу.
  • Термінація TLS — це також межа довіри. Саме там ви вирішуєте набори шифрів, HSTS, автентифікацію клієнтських сертифікатів і місце зберігання приватних ключів.
  • Перезавантаження сервера — не те ж саме, що рестарт контейнера. Деякі проксі можуть підвантажити сертифікати на гаряче; деякі потребують рестарту; деякі — сигналу; деякі — виклику API.

Одна перефразована ідея з команди книги Google SRE (Beyer, Jones, Petoff, Murphy): Сподіватися — не стратегія; надійність приходить від інженерних систем і зворотних зв’язків. Це болісно добре підходить до оновлення сертифікатів.

Три шаблони, ранжовані за безпекою

Шаблон A (найкращий дефолт): реверс-проксі володіє TLS + ACME, додатки залишаються HTTP-only

Це підхід «вхід — це продукт». Ви запускаєте Traefik або Caddy (або nginx з companion) як єдиний публічний контейнер. Він запитує сертифікати, зберігає їх, оновлює і обслуговує. Контейнери додатків ніколи не торкаються приватних ключів.

Чому це безпечно:

  • Одне місце для загартування і спостереження.
  • Одне місце для коректного перезавантаження сертифікатів.
  • Додатки можна масштабувати/переписувати без торкання стану TLS.
  • Простіше триматися в межах лімітів.

Де це б’є по вас: потрібно захистити ACME-сторедж проксі (приватні ключі й ключі облікового запису). Якщо контейнер проксі буде скомпрометовано, це ваша ідентичність сертифікаційної пари для даного набору доменів.

Шаблон B (дуже добре): ACME-клієнт на хості, сертифікати монтуються тільки для читання в проксі

Це нудний Unix-патерн. Certbot (або lego) працює на хості через таймери systemd, пише в /etc/letsencrypt, а ваш проксі читає сертифікати з bind-mount тільки для читання. Перезавантаження відбувається через контрольований хук.

Чому це безпечно:

  • Планування та логування на рівні хоста передбачувані.
  • Легше використовувати засоби безпеки ОС (права, SELinux/AppArmor).
  • Ваш проксі-контейнер не потребує облікових даних DNS API чи ключів облікового запису ACME.

Де це б’є по вас: HTTP-01 челенджі можуть бути незграбними, якщо ваш проксі теж контейнеризовано. Потрібен чистий шлях з Інтернету до відповідача челенджів.

Шаблон C (допустимий тільки за обмежень): ACME-клієнт в контейнері, що пише в спільний том

Це «контейнер Certbot + контейнер nginx» у Compose. Може працювати. Також схильний старіти погано: зсув прав, томи копіюються між хостами, і оновлення стають невидимими, поки не відмовлять.

Коли це виправдано:

  • Ви не можете встановлювати нічого на хості (керовані середовища, жорсткі образи).
  • Ви в обмеженнях на кшталт Kubernetes, але все ще на Docker.
  • У вас однопрофільний хост і сильні політики ізоляції контейнерів.

Що робити, якщо обираєте це: ставтеся до тому сертифікатів як до секретного сховища. Монтування тільки для читання скрізь, крім ACME-писаря. Жорстке володіння файлами. Ніяких «777, бо працює».

Жарт #1: Сертифікати як молоко. Вони в порядку, поки ви не забудете дату закінчення, а тоді запах доходить до керівництва.

Шаблон, який не слід випускати: кожен сервіс запускає свій Certbot

Кілька сервісів конкурують за порт 80, кожен пише в свій том, кожен оновлює в свій графік, кожен може використовувати staging vs production по-різному. Це прекрасний спосіб вивчити ліміти Let’s Encrypt в режимі реального часу.

Зберігання сертифікатів: томи, права та проблема приватного ключа

Більшість постмортемів щодо TLS насправді не про TLS. Вони про стан: де живуть ключі, хто може їх читати і чи переживає цей стан переправленням.

Що має бути персистентним

  • Приватні ключі (privkey.pem): якщо втрачені, можна перевипустити, але це призведе до простою і може заблокувати вас у сценаріях з pinning/HSTS.
  • Ланцюжок сертифікатів (fullchain.pem): потрібен серверу, щоб представити дійсний ланцюжок.
  • ACME-ключ облікового запису: використовується клієнтом для автентифікації в Let’s Encrypt. Втративши його, можна перереєструватись, але втрачається континуїтет і можливі операційні сюрпризи.

Bind mount vs named volume

Bind mount простий і аудитується: можна інспектувати файли на хості, робити бекапи і застосовувати права ОС. Для чутливого матеріалу bind mounts зазвичай легше пояснити.

Named volumes портативні в інструментарії Docker, але можуть стати чорною скринькою. Вони підходять, якщо ви ставитеся до них як до керованої бази даних і знаєте, як їх бекапити та відновлювати.

Права: найменша привілегія, не найменше зусиль

Вашому проксі потрібен доступ на читання до ключа. Ваш ACME-клієнт потребує права на запис. Ніхто інший — ні. Не монтуйте /etc/letsencrypt читано-записувано в півдесятка контейнерів лише тому, що так зручно.

Визначте модель довіри:

  • Один хост: зберігайте сертифікати в файловій системі хоста, root-owned, доступні групі, під якою запускається проксі-контейнер.
  • Кілька хостів: уникайте NFS для приватних ключів, якщо ви не впевнені у блокуванні файлів і безпеці. Надавайте перевагу per-host випуску (DNS-01) або механізму розповсюдження секретів з семантикою ротації.

Ключі в образах: просто ні

Запікання ключів в образи — кар’єрно обмежуючий крок. Образи пушаться в реєстри, кешуються на ноутбуках, скануються CI і іноді витікають. Тримайте ключі поза build-контекстом, шарами і історією.

Оновлення і перезавантаження: що означає «автоматизація»

Оновлення має три задачі:

  1. Отримати новий сертифікат до закінчення терміну.
  2. Помістити його туди, де сервер очікує.
  3. Змусити сервер використовувати його без втрати трафіку.

Гаряче підвантаження vs рестарт

Деякі проксі можуть підвантажити сертифікати без розриву з’єднань. Інші — ні. Потрібно знати, який у вас, і тестувати це. «Рестарт контейнера щотижня» — не стратегія; це рулетка з кращим брендуванням.

Хуки — ваші друзі

Якщо ви використовуєте Certbot на хості, використовуйте deploy hooks, щоб коректно перезавантажувати nginx/Traefik. Якщо у вас проксі з рідною ACME-реалізацією, підтвердіть, як воно зберігає ACME-стан і як поводиться при перезавантаженні.

Жарт #2: Нічого не додає впевненості так, як робота оновлення TLS, яка виконується лише тоді, коли хтось пам’ятає про її існування.

Практичні завдання: команди, виводи та рішення

Це не «копіюй-вставляй і молись» фрагменти. Кожне завдання включає, що означає вивід і яке рішення прийняти далі. Виконуйте їх на хості, якщо не зазначено інше.

Завдання 1: Підтвердіть, що слухає на портах 80/443

cr0x@server:~$ sudo ss -lntp | egrep ':80|:443'
LISTEN 0      4096         0.0.0.0:80        0.0.0.0:*    users:(("docker-proxy",pid=1123,fd=4))
LISTEN 0      4096         0.0.0.0:443       0.0.0.0:*    users:(("docker-proxy",pid=1144,fd=4))

Значення: Docker публікує обидва порти. Це означає, що контейнер — ваш вхід. Якщо ви очікували, що nginx на хості володіє 80/443 — ви вже знайшли конфлікт.

Рішення: Ідентифікуйте, який контейнер мапить ці порти і підтвердіть, що це єдиний точковий TLS-термінатор.

Завдання 2: Визначте контейнер, що публікує порти

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
NAMES        IMAGE             PORTS
edge-proxy   traefik:v3.1      0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
api          myco/api:1.9.2    127.0.0.1:9000->9000/tcp

Значення: edge-proxy — публічний вхід. Добре. API лише локальний.

Рішення: Забезпечте, щоб весь публічний TLS оброблявся в edge-proxy і видаліть будь-які прямі експозиції 443 в інших місцях.

Завдання 3: Перевірте сертифікат, що подається в Інтернет

cr0x@server:~$ echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R3
notBefore=Dec  1 03:12:10 2025 GMT
notAfter=Feb 29 03:12:09 2026 GMT

Значення: Живий сертифікат закінчується 29 лютого. Це ваш жорсткий дедлайн. Також підтверджує правильність SNI.

Рішення: Якщо термін закінчення в межах 14 днів і у вас немає перевіреної пайплайну оновлення — припиніть інші дії і виправте це в першу чергу.

Завдання 4: Перевірте повний ланцюжок та якість рукостискання

cr0x@server:~$ openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | egrep 'Verify return code|subject=|issuer='
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = R3
Verify return code: 0 (ok)

Значення: Ланцюжок нормальний і клієнти повинні його валідувати.

Рішення: Якщо код перевірки не 0, перевірте, чи ви подаєте fullchain.pem проти cert.pem і чи проксі налаштовано на правильний бандл.

Завдання 5: Якщо Certbot на хості — перелічіть сертифікати та терміни

cr0x@server:~$ sudo certbot certificates
Saving debug log to /var/log/letsencrypt/letsencrypt.log

Found the following certs:
  Certificate Name: example.com
    Domains: example.com www.example.com
    Expiry Date: 2026-02-29 03:12:09+00:00 (VALID: 57 days)
    Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/example.com/privkey.pem

Значення: Погляд Certbot на стан. Якщо це відрізняється від того, що показує openssl s_client, ваш проксі не читає очікувані файли.

Рішення: Вирівняйте конфігурацію проксі з живими шляхами під /etc/letsencrypt/live і переконайтеся, що ці симлінки доступні всередині контейнера.

Завдання 6: Сухий прогін оновлення (staging) для перевірки пайплайну

cr0x@server:~$ sudo certbot renew --dry-run
Saving debug log to /var/log/letsencrypt/letsencrypt.log

Processing /etc/letsencrypt/renewal/example.com.conf
Simulating renewal of an existing certificate for example.com and www.example.com

Congratulations, all simulated renewals succeeded:
  /etc/letsencrypt/live/example.com/fullchain.pem (success)

Значення: Шлях челенджів, облікові дані і хуки працюють у staging. Це найближче до unit-тесту, який ви отримаєте.

Рішення: Якщо це не вдається, не чекайте на продакшн-оновлення. Виправляйте помилку негайно.

Завдання 7: Перевірте досяжність челенджу для HTTP-01

cr0x@server:~$ curl -i http://example.com/.well-known/acme-challenge/ping
HTTP/1.1 404 Not Found
Server: traefik
Date: Sat, 03 Jan 2026 10:21:42 GMT
Content-Type: text/plain; charset=utf-8

Значення: Ви можете дістатися до хоста і проксі відповідає на порті 80. 404 — нормально для цього синтетичного URL; важливо, щоб він не редиректував на HTTPS таким чином, що ваш ACME-клієнт не може його обробити.

Рішення: Якщо ви отримуєте таймаут підключення — неправильно налаштований фаєрвол/NAT/публікація портів. Якщо ви отримуєте 301 на HTTPS — підтвердіть, що ваш ACME-клієнт/проксі це підтримує безпечно, або зробіть виняток для шляху челенджу.

Завдання 8: Перевірте монтування Docker-томів для сертифікатів

cr0x@server:~$ docker inspect edge-proxy --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/etc/letsencrypt","Destination":"/etc/letsencrypt","Mode":"ro","RW":false,"Propagation":"rprivate"}]

Значення: Проксі читає /etc/letsencrypt з хоста, тільки для читання. Це саме та форма, яку ви хочете.

Рішення: Якщо воно RW, а не повинно бути — закрийте доступ. Якщо джерело — named volume, підтвердіть, що ви можете його бекапити і відновлювати свідомо.

Завдання 9: Підтвердіть права та власність приватних ключів

cr0x@server:~$ sudo ls -l /etc/letsencrypt/live/example.com/privkey.pem
-rw------- 1 root root 1704 Dec  1 03:12 /etc/letsencrypt/live/example.com/privkey.pem

Значення: Читати може тільки root. Якщо ваш проксі працює не від root всередині контейнера, він може не встати з ключем.

Рішення: Або запускайте проксі під користувачем, що може читати ключ через групові права, або використовуйте контрольований механізм (спеціальна група і chmod 640) замість відкриття на всіх.

Завдання 10: Перевірте логи проксі на події ACME і перезавантаження сертифікатів

cr0x@server:~$ docker logs --since 2h edge-proxy | egrep -i 'acme|certificate|renew|challenge' | tail -n 20
time="2026-01-03T08:01:12Z" level=info msg="Renewing certificate from LE : {Main:example.com SANs:[www.example.com]}"
time="2026-01-03T08:01:15Z" level=info msg="Server responded with a certificate."
time="2026-01-03T08:01:15Z" level=info msg="Adding certificate for domain(s) example.com, www.example.com"

Значення: Оновлення відбулося і проксі вірить, що завантажив новий сертифікат.

Рішення: Якщо логи показують успішне оновлення, але клієнти все ще бачать старий сертифікат, ймовірно у вас кілька інстансів ingress або шар кешування/балансування, що подає інший сертифікат.

Завдання 11: Якщо Certbot через systemd timers — перевірте розклад і останній запуск

cr0x@server:~$ systemctl list-timers | grep -i certbot
Sun 2026-01-04 03:17:00 UTC  15h left  Sat 2026-01-03 03:17:02 UTC  5h ago  certbot.timer  certbot.service

Значення: Таймер існує і запустився недавно.

Рішення: Якщо його немає — немає автоматизації. Якщо він є, але не запускався — перевірте, чи хост не був вимкнений або чи таймер не помилково налаштований.

Завдання 12: Перевірте, чи deploy hooks справді перезавантажували проксі

cr0x@server:~$ sudo grep -R "deploy-hook" -n /etc/letsencrypt/renewal | head
/etc/letsencrypt/renewal/example.com.conf:12:deploy_hook = docker kill -s HUP edge-proxy

Значення: Після оновлення Certbot шле HUP в контейнер проксі. Це контрольований патерн перезавантаження.

Рішення: Підтвердіть, що проксі підтримує SIGHUP-перезавантаження. Якщо ні — замініть хук на правильну команду перезавантаження (або виклик API) і протестуйте це в робочий час.

Завдання 13: Підтвердіть, який файл сертифіката налаштований у проксі

cr0x@server:~$ docker exec -it edge-proxy sh -c 'grep -R "fullchain.pem\|privkey.pem" -n /etc/traefik /etc/nginx 2>/dev/null | head'
/etc/nginx/conf.d/https.conf:8:ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
/etc/nginx/conf.d/https.conf:9:ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

Значення: Ви подаєте повний ланцюжок і ключ зі стандартних live-шляхів.

Рішення: Якщо бачите шляхи під /tmp або app-специфічні директорії — очікуйте сюрпризів під час redeploy.

Завдання 14: Перевірте ризик лімітів, порахувавши недавні невдалі спроби

cr0x@server:~$ sudo awk '/urn:ietf:params:acme:error/ {count++} END {print count+0}' /var/log/letsencrypt/letsencrypt.log
0

Значення: Немає ACME-помилок у логах. Добре.

Рішення: Якщо це число зростає, зупиніть автоматичні повтори і виправте базову проблему до того, як ви вдарите по лімітам.

Завдання 15: Підтвердіть коректність часу в контейнері (так, це важливо)

cr0x@server:~$ docker exec -it edge-proxy date -u
Sat Jan  3 10:23:01 UTC 2026

Значення: Час правильний. Неправильний час може спричиняти помилки валідації сертифікатів, що виглядають як «випадкові TLS-ошибки».

Рішення: Якщо час неправильний — спочатку виправляйте NTP на хості. Контейнери успадковують час хоста; якщо він неправильний, усе буде неправильно.

Швидкий план діагностики

Коли TLS горить, ви не «досліджуєте». Ви триажите. Ось порядок, що швидко знаходить вузьке місце.

Перше: подається неправильний сертифікат чи його взагалі немає?

  • Запустіть openssl s_client проти публічного кінцевого пункту і перевірте notAfter, subject та issuer.
  • Якщо він прострочений: у вас збій поновлення або перезавантаження.
  • Якщо неправильний CN/SAN: ви потрапляєте на неправильний інстанс ingress, неправильний SNI-маршрут або дефолтний сертифікат.

Друге: чи ACME-клієнт вважає, що він оновився?

  • Перевірте логи Certbot/ACME на записи про успіх і часові мітки.
  • Перевірте часові мітки файлів на fullchain.pem та privkey.pem.
  • Якщо файли оновлені, але сервіс подає старий сертифікат: це проблема перезавантаження/розповсюдження.

Третє: чи можна зараз задовольнити челендж?

  • Для HTTP-01: підтвердіть досяжність порту 80 і що /.well-known/acme-challenge/ маршрутизований до правильного відповідача.
  • Для DNS-01: підтвердіть, що облікові дані DNS API є і дійсні, та перевірте затримки реплікації.

Четверте: підтвердіть, що є лише одне джерело істини

  • Шукайте кілька ingress-контейнерів або кілька хостів за балансувальником, які не ділять стан сертифікатів навмисно.
  • Переконайтеся, що staging і production endpoint не змішані.

П’яте: ліміти і шторм повторів

  • Якщо бачите повторювані відмови — тимчасово зупиніть роботу поновлення. Ліміти суворі і продовжать простій.
  • Виправте базову маршрутизацію/DNS, а потім виконайте контрольований повтор.

Типові помилки: симптом → корінь → виправлення

1) Симптом: «оновлення» пройшло успішно, але в браузерах все ще старий сертифікат

Корінь: Проксі ніколи не перезавантажив нові файли, або ви оновили сертифікати на одному хості, а трафік іде на інший.

Виправлення: Додайте deploy hook для перезавантаження проксі (сигнал/API) і перевірте, який інстанс подає трафік за допомогою openssl s_client з кількох точок.

2) Симптом: Certbot провалює HTTP-01 з «connection refused» або «timeout»

Корінь: Порт 80 недоступний (фаєрвол, NAT, неправильна публікація Docker) або інша служба його займає.

Виправлення: Переконайтеся, що порт 80 публікується ingress-контейнером і дозволений в security groups/фаєрволі. Запустіть ss -lntp для підтвердження.

3) Симптом: HTTP-01 повертає «unauthorized» і вміст токена неправильний

Корінь: Шлях челенджу редиректиться/маршрутизиться в додаток, а не до відповідача ACME. Часто викликано правилами «примусовий HTTPS» або надто жадібним правилом реверс-проксі.

Виправлення: Додайте спеціальний маршрут для /.well-known/acme-challenge/, який обминає редиректи і вказує на ACME-відповідача.

4) Симптом: Ви потрапили в ліміти Let’s Encrypt під час інциденту

Корінь: Автоматичні повтори виснажують продукційний випуск після повторних відмов челенджів.

Виправлення: Використовуйте --dry-run в staging для тестів; реалізуйте backoff; сповіщайте про збої. Під час інциденту зупиніть роботу і виправте досяжність перш ніж пробувати знову.

5) Симптом: Проксі не може прочитати privkey.pem всередині контейнера

Корінь: Права файлу доступні тільки root, а контейнер працює не від root, або SELinux маркування блокує доступ.

Виправлення: Використовуйте групово-читаємі права для спеціальної групи, запускайте проксі з цією групою, і якщо SELinux увімкнено — використайте правильні мітки на bind mount.

6) Симптом: Після redeploy сертифікати зникають і проксі подає дефолтний/самопідписаний сертифікат

Корінь: ACME-сторедж був в файловій системі контейнера (епемерний) або в тому, який не бекапиться і був створений заново.

Виправлення: Персистуйте ACME-сторедж у named volume або bind mount з бекапами. Ставтеся до нього як до stateful даних.

7) Симптом: Запити wildcard-сертифікатів падають, хоча HTTP-01 працює

Корінь: Wildcard вимагає DNS-01, а не HTTP-01.

Виправлення: Реалізуйте DNS-01 через API провайдера DNS, захистіть облікові дані і протестуйте поведінку реплікації.

8) Симптом: «Працює на одному хості, але не на іншому»

Корінь: Split-brain: кілька ingress-нoдів кожен видає незалежно, або несинхронний час, або невідповідна конфігурація.

Виправлення: Виберіть єдину модель володіння (per-host випуск з DNS-01 або централізована термінація з розподілом секретів) і забезпечте її через конфіг менеджмент.

Три корпоративні міні-історії з практики

Міні-історія 1: Інцидент через неправильне припущення

У них був акуратний стек Docker Compose: реверс-проксі, кілька API, фронтенд і сервіс «certbot». Припущення було просте: контейнер certbot оновлює сертифікати і проксі магічно починає їх використовувати. Ніхто не описав, що означає «магічно».

День оновлення настав. Certbot оновив. Файли на диску змінилися. Але процес проксі завантажив сертифікат при старті й ніколи більше не дивився на диск. Він щасливо обслуговував старий сертифікат з пам’яті, поки диск містив новий — як бібліотекар, що відмовляється приймати нові видання.

Команда ганялася за червоними геральдичними слідами: DNS, фаєрвол, Outage Let’s Encrypt. Тим часом браузери кричали «просрочений сертифікат» і клієнти підозрювали компроміс. Безпека втрутилася. Керівництво втрутилося. Сон покинув будівлю.

Виправлення зайняло хвилини, коли його побачили: deploy hook, що посилав правильний сигнал перезавантаження контейнеру проксі, плюс перевірка, яка порівнювала живий сертифікат з файловою системою після оновлення. Більше масштабне виправлення зайняло тиждень: вони додали алерт «сертифікат закінчується через 14 днів» і написали рукбук, що починається з openssl s_client.

Міні-історія 2: Оптимізація, що підвела

Інша компанія хотіла швидші деплої і менше вузлів. Хтось запропонував: «Нехай кожен сервіс контейнер просить свій сертифікат. Тоді масштабування просте, і команди автономні.» Це звучало як сучасна архітектура і також як те, що можна сказати на зустрічі без опору.

Працювало деякий час, як кухня, коли ніхто не готує. Потім відбувся міграційний хвиль: десятки сервісів перепрошилися за кілька годин. Кожен намагався видати сертифікат. Деякі використовували staging, деякі production, і кілька мали неправильно налаштовані домени, що падали і повторювалися агресивно.

Ліміти спрацьовували. Деякі сервіси не могли отримати сертифікати і подавали самопідписані. Клієнти з pinning’ом впали жорстко. Кількість заявок в підтримку злетіла. Інцидент було технічно «просто сертифікати», але операційно — розподілена система, що володіє собою.

Вони відкотилися до централізованого ingress, що видавав невеликий набір сертифікатів і маршрутизував всередину. Автономія повернулася в кращому вигляді: команди володіли маршрутами й заголовками, а не життєвим циклом приватних ключів. Оптимізація була «видалити вузьке місце». Те, що вони видалили — було єдиним місцем, куди хтось дивився.

Міні-історія 3: Нудна, але правильна практика, що виручила

Досить регульоване підприємство мало неефектне правило: весь інтернет-трафік TLS завершується на загартованому edge-проксі, а стан сертифікатів бекапиться як частина інфраструктурного стану. Інженери бурчали. Здавалося повільно. Здавалося як паперова робота.

Потім несподівано відбулася зміна ланцюга центрів сертифікації в екосистемі і частина старіших клієнтів поводилася погано. Команда не мусила бігати по 40 репозиторіях додатків у пошуках налаштувань TLS. Вони відкоригували конфігурацію edge, перевірили представлення ланцюжка і розгорнули контрольовану зміну з канаркою. Додатки не рухалися.

Пізніше того ж року помер хост. Заміну підняли, застосували конфіг, сертифікати відновили, трафік відновився. Жодного термінового випуску, жодної драми з лімітами, жодної «чому том порожній?» таємниці.

Рятівний крок не був героїчним. Це були межі відповідальності, бекапи і хуки перезавантаження, протестовані щоквартально. Це нудне, як ремені безпеки, нудне, але правильне.

Перевірочні списки / покроковий план

Оберіть шаблон (зробіть це перед написанням Compose файлів)

  1. Один хост, простий вхід: Шаблон A (проксі з рідною ACME) або Шаблон B (Certbot на хості + проксі читає тільки для читання).
  2. Кілька хостів за LB: Віддавайте перевагу DNS-01 і per-host видачі, або централізованому підходу розподілу сертифікатів. Уникайте «shared NFS /etc/letsencrypt», якщо ви по-справжньому не розумієте режими відмов.
  3. Сильно закриті хости: Шаблон A з продуманим ACME-стореджем, плюс бекапи й контроль доступу.

Чекліст загартування (те, про що шкодують, що пропустили)

  • Тільки один публічний ingress публікує порти 80/443.
  • Сертифікати й ключі персистуються і бекапляться.
  • Приватні ключі читає лише ingress (і поновлювач, якщо він окремий).
  • Тестування сухого прогону staging для оновлень заплановано.
  • Механізм перезавантаження реалізований і перевірений (сигнал/API/грейсфул перезавантаження).
  • Моніторинг: алерт на закінчення сертифікату, помилки поновлення і ACME-помилки.
  • Рунок: перша команда — openssl s_client, а не «перевірити Grafana».

Покроково: Certbot на хості + контейнерний nginx/Traefik читає тільки для читання

  1. Встановіть Certbot на хості та отримайте початковий сертифікат способом, сумісним з вашою маршрутизацією (standalone/webroot/DNS).
  2. Зберігайте сертифікати в /etc/letsencrypt на хості.
  3. Bind mount /etc/letsencrypt в контейнер проксі як тільки для читання.
  4. Налаштуйте проксі на використання fullchain.pem і privkey.pem.
  5. Додайте Certbot deploy hook для коректного перезавантаження проксі.
  6. Увімкніть і перевірте systemd timer для оновлень.
  7. Запустіть certbot renew --dry-run і перевірте, що живий сертифікат співпадає з файловою системою після перезавантаження.

Покроково: проксі з рідною ACME (Traefik/Caddy стиль)

  1. Персистуйте ACME-стан у томі/bind mount (це не опціонально).
  2. Заблокуйте права на ACME-сторедж (тут живуть ключі облікових записів).
  3. Використовуйте HTTP-01 тільки якщо порт 80 надійно доступний; інакше використовуйте DNS-01 з обмеженими обліковими даними DNS API.
  4. Тестуйте поведінку оновлення і спостерігайте логи на події оновлення.
  5. Бекапте ACME-сторедж і тестуйте відновлення на непроактивному інстансі.

Часті питання

Чи запускати Certbot всередині контейнера?

Можна, але за замовчуванням не варто. Якщо хост може запускати Certbot, оновлення на хості плюс монтування для читання в проксі легше аудитити і відновлювати.

Чи безпечно Traefik/Caddy ACME?

Так, якщо ви персистуєте і захищаєте ACME-сторедж. Небезпечна версія — залишати ACME-дані в ефермерному файловому просторі контейнера або монтувати їх RW у всі сторони.

Чому не термінувати TLS в кожному контейнері додатка?

Бо приватні ключі розповзаються, оновлення множаться, а відлагодження перетворюється на полювання на сліди. Централізуйте TLS на edge, якщо тільки у вас немає специфічних вимог з комплаєнсу чи архітектури.

Який найбезпечніший тип челенджу з Docker?

DNS-01 найбільш дружній до інфраструктури, коли порт 80 ускладнений (балансувальники, закриті фаєрволи, кілька ingress). Але він переносить ризик на облікові дані DNS API і час реплікації.

Чи потрібен порт 80 відкритий, якщо я всюди використовую HTTPS?

Якщо ви використовуєте HTTP-01 — так. ACME-сервер має забрати токен по HTTP. Звичне рішення: дозволити HTTP тільки для /.well-known/acme-challenge/ і редиректити все інше на HTTPS.

Як уникнути простою під час поновлення сертифікатів?

Використовуйте проксі, що підтримує граціозне перезавантаження, і тригерніть його через deploy hook (або покладіться на вбудоване перезавантаження проксі). Перевіряйте через openssl s_client після оновлення.

Де зберігати сертифікати на диску?

На хості в захищеній директорії (звично /etc/letsencrypt), якщо хост виконує ACME. Або в виділеному, бекапованому томі, якщо проксі виконує ACME. Тримайте приватний ключ читаним лише тим, хто повинен його обслуговувати.

Що робити, якщо у мене кілька Docker-хостів за балансувальником?

Виберіть одне: per-host видача (зазвичай DNS-01) або централізований підхід розповсюдження сертифікатів. Уникайте ad-hoc спільних файлових систем, якщо не протестували блокування, бекапи й поведінку при відмові.

Як зрозуміти, чи близькі ви до лімітів Let’s Encrypt?

Шукайте повторювані ACME-помилки в логах і припиніть шторм повторів. Кілька контрольованих повторів — нормально; тісні цикли під час простою — те, як ви опинитесь у режимі очікування охолодження.

Чи підходять Docker secrets для TLS приватних ключів?

У Swarm Docker secrets можуть бути хорошим примітивом, але вам все одно потрібна ротація і семантика перезавантаження. У простому Docker Compose «секрети» часто вироджуються в монтування файлів без управління життєвим циклом.

Висновок: наступні практичні кроки

Якщо ви візьмете лише одне рішення з цього матеріалу: централізуйте відповідальність за TLS на ingress. Потім оберіть, як видаються сертифікати:

  • Використовуйте проксі з рідним ACME, коли ви можете персистувати й захистити його стан чисто.
  • Використовуйте Certbot на хості, коли хочете передбачуване планування, логування і контроль файлової системи.
  • Використовуйте Certbot в контейнері тільки коли встановлення на хості заборонене, і ставтеся до тому сертифікатів як до секретного сховища.

Наступні кроки, які ви можете зробити цього тижня:

  1. Запустіть перевірку «подається сертифікат» з openssl s_client і зафіксуйте дату закінчення десь видимо.
  2. Запустіть тест сухого оновлення і виправте будь-які помилки, поки ви не під тиском.
  3. Підтвердіть семантику перезавантаження і реалізуйте deploy hook, що доведено працює.
  4. Додайте один алерт: «сертифікат закінчується через 14 днів». Нудний алерт. Рятівний алерт.

Після цього можна цивілізовано сперечатися про набори шифрів і HTTP/3. Спочатку — перестаньте дозволяти сертифікатам витікати термін дії.

← Попередня
WordPress 404 на записах: виправлення пермалінків без шкоди для SEO
Наступна →
Контейнери проти віртуальних машин: який профіль CPU підходить для чого

Залишити коментар