О 02:13 ваш телефон викликає знайомий страх: «клієнти не можуть увійти». Ви відкриваєте панель і бачите це: збої TLS-процесу рукостискання, каскад помилок 525/526 і сертифікат, що закінчився… учора. Сервіс працює. Ланцюжок сертифікації — ні. Саме так «прості HTTPS» перетворюються на інциденти в продукції.
Docker тут не винен прямо. Але Docker зробив легшим приховування гострих країв: приватні ключі в ефермерних файлових системах, оновлення, що виконуються в неправильному неймспейсі, ACME-челенджі, що застрягли за проксі, і підхід «просто змонтувати /etc/letsencrypt кудись», який перетворюється на цирк з правами доступу та ротацією.
Рішення: де повинна бути Let’s Encrypt
У вас є два загальні варіанти:
- ACME-клієнт запускається на хості (або на відокремленій «сертифікатній» VM/неймспейсі хоста) і записує сертифікати в контрольоване місце; контейнери читають їх тільки для читання.
- 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-контекстом, шарами і історією.
Оновлення і перезавантаження: що означає «автоматизація»
Оновлення має три задачі:
- Отримати новий сертифікат до закінчення терміну.
- Помістити його туди, де сервер очікує.
- Змусити сервер використовувати його без втрати трафіку.
Гаряче підвантаження 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 файлів)
- Один хост, простий вхід: Шаблон A (проксі з рідною ACME) або Шаблон B (Certbot на хості + проксі читає тільки для читання).
- Кілька хостів за LB: Віддавайте перевагу DNS-01 і per-host видачі, або централізованому підходу розподілу сертифікатів. Уникайте «shared NFS /etc/letsencrypt», якщо ви по-справжньому не розумієте режими відмов.
- Сильно закриті хости: Шаблон A з продуманим ACME-стореджем, плюс бекапи й контроль доступу.
Чекліст загартування (те, про що шкодують, що пропустили)
- Тільки один публічний ingress публікує порти 80/443.
- Сертифікати й ключі персистуються і бекапляться.
- Приватні ключі читає лише ingress (і поновлювач, якщо він окремий).
- Тестування сухого прогону staging для оновлень заплановано.
- Механізм перезавантаження реалізований і перевірений (сигнал/API/грейсфул перезавантаження).
- Моніторинг: алерт на закінчення сертифікату, помилки поновлення і ACME-помилки.
- Рунок: перша команда —
openssl s_client, а не «перевірити Grafana».
Покроково: Certbot на хості + контейнерний nginx/Traefik читає тільки для читання
- Встановіть Certbot на хості та отримайте початковий сертифікат способом, сумісним з вашою маршрутизацією (standalone/webroot/DNS).
- Зберігайте сертифікати в
/etc/letsencryptна хості. - Bind mount
/etc/letsencryptв контейнер проксі як тільки для читання. - Налаштуйте проксі на використання
fullchain.pemіprivkey.pem. - Додайте Certbot deploy hook для коректного перезавантаження проксі.
- Увімкніть і перевірте systemd timer для оновлень.
- Запустіть
certbot renew --dry-runі перевірте, що живий сертифікат співпадає з файловою системою після перезавантаження.
Покроково: проксі з рідною ACME (Traefik/Caddy стиль)
- Персистуйте ACME-стан у томі/bind mount (це не опціонально).
- Заблокуйте права на ACME-сторедж (тут живуть ключі облікових записів).
- Використовуйте HTTP-01 тільки якщо порт 80 надійно доступний; інакше використовуйте DNS-01 з обмеженими обліковими даними DNS API.
- Тестуйте поведінку оновлення і спостерігайте логи на події оновлення.
- Бекапте 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 в контейнері тільки коли встановлення на хості заборонене, і ставтеся до тому сертифікатів як до секретного сховища.
Наступні кроки, які ви можете зробити цього тижня:
- Запустіть перевірку «подається сертифікат» з
openssl s_clientі зафіксуйте дату закінчення десь видимо. - Запустіть тест сухого оновлення і виправте будь-які помилки, поки ви не під тиском.
- Підтвердіть семантику перезавантаження і реалізуйте deploy hook, що доведено працює.
- Додайте один алерт: «сертифікат закінчується через 14 днів». Нудний алерт. Рятівний алерт.
Після цього можна цивілізовано сперечатися про набори шифрів і HTTP/3. Спочатку — перестаньте дозволяти сертифікатам витікати термін дії.