Ubuntu 24.04: Certbot оновив сертифікат, але ваш застосунок усе ще не працює — виправте дозволи й хуки перезавантаження

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

Ви запускаєте certbot renew. Консоль каже «Congratulations.» Ваш моніторинг каже «Абсолютно ні.» Користувачі бачать помилки TLS, або застосунок продовжує віддавати сертифікат, що закінчився вчора. Це той особливий різновид подразнення, коли термінал веселий, а продакшн палає.

На Ubuntu 24.04 звичайний винуватець — не сам Let’s Encrypt. Це нудна інфраструктура навколо: дозволи файлів, символьні посилання в /etc/letsencrypt/live, systemd timers, що оновлюють, але нікого не перезавантажують, і застосунки, які не можуть (або не хочуть) перечитати сертифікати без чіткого поштовху.

Що насправді відбувається, коли оновлення «успішне»

Оновлення сертифіката Certbot — лише одна нога триноги:

  1. Видача/оновлення: Let’s Encrypt підписує новий сертифікат, а Certbot зберігає його під /etc/letsencrypt/archive/<name>/, а потім оновлює символьні посилання в /etc/letsencrypt/live/<name>/.
  2. Доступ: Ваш застосунок (nginx, Apache, HAProxy, Java-сервіс, контейнер) має мати змогу читати fullchain.pem і privkey.pem. «Мати змогу» означає права Unix і можливість проходження шляхом через кожну батьківську директорію. Не лише «файл існує».
  3. Перезавантаження: Процес повинен перечитати файли (або бути перезапущеним) після оновлення. Деякі демони перечитують на SIGHUP. Деяким потрібна перевірка конфігу перш ніж перезавантажитись. Деяким потрібен повний рестарт. Деякі ніколи не перечитують сертифікати під час роботи і радо віддаватимуть старий сертифікат до наступного розгортання.

Більшість відмов трапляється на ногах 2 або 3. Certbot не знає автоматично, як ваш сервіс споживає сертифікати. Також на Ubuntu 24.04 systemd і пакети за замовчуванням підштовхують до автоматизації (timers, services), що добре — доки хтось не підключив частину «перезавантажити справжню річ».

Одна операційна істина: оновлений сертифікат марний, поки процес, який його подає, не перечитав пару ключів. Клієнта цікавлять лише байти, які він отримує під час TLS-рукопотискання, а не те, що вивів certbot.

Цитата одна, бо вона й досі правдива: «Сподівання — не стратегія.» — General Gordon R. Sullivan

Жарт №1: оновлення TLS без хука перезавантаження — це як замінити батарейки в детекторі диму, який ви ще не встановили. Технічно прогрес, фактично дим.

Швидка діагностика (першочергові кроки)

Якщо ви на виклику, вам не потрібна лекція. Потрібен найкоротший шлях до «Чи оновлено сертифікат, чи його можна прочитати й чи він завантажений?» Використовуйте цей порядок. Це мінімізує метушню.

Перший крок: який сертифікат реально бачить клієнт?

  • Перевірте термін дії й серійний номер сервера, з погляду клієнта.
  • Якщо сертифікат старий: це питання перезавантаження/вибору/маршрутизації. Не звинувачуйте Let’s Encrypt.
  • Якщо сертифікат новий, але клієнти все одно падають: можлива проблема з ланцюгом, невідповідністю SNI або неправильним віртуальним хостом.

Другий крок: чи має процес доступ до шляху приватного ключа?

  • Підтвердіть, що шлях у конфігурації сервісу співпадає з очікуваним символьним посиланням у /etc/letsencrypt/live.
  • Перевірте права від імені користувача сервісу (або використайте namei для перевірки traversal).
  • Шукайте обмеження AppArmor/SELinux (на Ubuntu часто: AppArmor).

Третій крок: що (або не) тригерить перезавантаження?

  • Certbot може виконуватись по таймеру й тихо оновлювати. Ваш nginx не помітить цього сам.
  • Перевірте логи Certbot на «Deploying Certificate» та виконання хуків.
  • Додайте deploy hook, який перезавантажує ваш сервіс лише коли фактично відбулося оновлення.

Лише після цих трьох переходьте до перевірки DNS-челенджів, лімітів ACME, правил фаєрволу або випадкового поведінки хмарного балансувальника. Вони трапляються, але зазвичай не в тих випадках, коли оновлення відмічене як успішне.

Цікаві факти та контекст (чому це часто підводить команди)

  • Дебют Let’s Encrypt (2015) зробив TLS очікуванням за замовчуванням, але також перетворив «оновлення сертифікатів» на повторюване операційне завдання, а не на нагадування в календарі.
  • Модель зберігання Certbot використовує розклад «archive» плюс «live symlink» саме для безпечних атомарних оновлень: нові файли з’являються в archive/, а посилання оновлюються в live/.
  • Приватний ключ у /etc/letsencrypt/live зазвичай має права 0600 root:root. Це коректна позиція з безпеки, і саме тому після «корисних» рефакторів нерутові застосунки часто ламаються.
  • systemd timers замінили cron для багатьох пакованих оновлень, бо таймери інтегруються з journald та статусами сервісу. Вони також полегшують забування, що «оновлення» — це не «деплой».
  • nginx може перечитати конфігурацію без втрати з’єднань, але лише якщо перезавантаження було тригеровано. Без цього nginx буде використовувати те, що він завантажив під час старту.
  • Graceful-поведінка Apache залежить від MPM і стеку модулів; він здатний на це, але зламані дозволи чи провальна перевірка конфігу можуть змусити його продовжувати працювати зі старим сертифікатом.
  • ACME-челенджі (http-01, dns-01, tls-alpn-01) вирішують видачу, а не розміщення. Команди плутають «челендж пройшов» з «сайт виправлено», бо обидва відбуваються в одному виводі команди.
  • Snap-пакетований Certbot змінив шляхи й поведінку обмеження для деяких інсталяцій, що може здивувати людей при переході між версіями Ubuntu або слідуванні застарілим гайдам.

Практичні завдання: команди, виводи й рішення (12+)

Це реальні операційні завдання. Кожне містить: команду, реалістичний вивід, що це означає, і яке рішення прийняти.

Завдання 1: Підтвердіть, який сертифікат бачить клієнт (термін дії, subject, issuer)

cr0x@server:~$ echo | openssl s_client -servername app.example.com -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -subject -issuer -dates
subject=CN = app.example.com
issuer=C = US, O = Let's Encrypt, CN = R11
notBefore=Dec 29 02:10:11 2025 GMT
notAfter=Mar 29 02:10:10 2026 GMT

Що це означає: Процес на localhost:443 віддає сертифікат, дійсний до Mar 29. Якщо ваш алерт каже «прострочено», можливо, ваш алерт перевіряє інший endpoint або інший шар проксі віддає старий сертифікат.

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

Завдання 2: Перевірте файли сертифікатів, які Certbot вважає актуальними

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

Found the following certs:
  Certificate Name: app.example.com
    Serial Number: 4e6a0f9a4b3c17c2a3b9e1d0c4a1a9f2
    Key Type: ECDSA
    Domains: app.example.com www.app.example.com
    Expiry Date: 2026-03-29 02:10:10+00:00 (VALID: 89 days)
    Certificate Path: /etc/letsencrypt/live/app.example.com/fullchain.pem
    Private Key Path: /etc/letsencrypt/live/app.example.com/privkey.pem

Що це означає: Погляд Certbot — в нормі: у нього є дійсний сертифікат і канонічні шляхи.

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

Завдання 3: Переконайтеся, що live-символічні посилання вказують на найновішу archive-версію

cr0x@server:~$ sudo ls -l /etc/letsencrypt/live/app.example.com/
total 4
lrwxrwxrwx 1 root root  43 Dec 29 02:10 cert.pem -> ../../archive/app.example.com/cert4.pem
lrwxrwxrwx 1 root root  44 Dec 29 02:10 chain.pem -> ../../archive/app.example.com/chain4.pem
lrwxrwxrwx 1 root root  48 Dec 29 02:10 fullchain.pem -> ../../archive/app.example.com/fullchain4.pem
lrwxrwxrwx 1 root root  46 Dec 29 02:10 privkey.pem -> ../../archive/app.example.com/privkey4.pem
-rw-r--r-- 1 root root 692 Jun  1  2024 README

Що це означає: Live-посилання тепер вказують на «4». Оновлення повернуло посилання.

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

Завдання 4: Переконайтеся, що конфіг nginx/Apache посилається на правильні шляхи

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -R --line-number -E "ssl_certificate(_key)?\s" /etc/nginx/sites-enabled/* | head
/etc/nginx/sites-enabled/app.conf:12:    ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
/etc/nginx/sites-enabled/app.conf:13:    ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;

Що це означає: nginx налаштований використовувати очікувані шляхи-символічні посилання.

Рішення: Якщо ви бачите /etc/letsencrypt/archive/... захардкоженим, змініть це. Захардкоджені archive-файли гарантують більший біль під час наступного оновлення.

Завдання 5: Перевірте, чи сервіс реально запущений і від імені якого користувача

cr0x@server:~$ systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since Mon 2025-12-29 02:11:03 UTC; 2h 14min ago
       Docs: man:nginx(8)
   Main PID: 1842 (nginx)
      Tasks: 3 (limit: 19092)
     Memory: 8.3M
        CPU: 1.742s
     CGroup: /system.slice/nginx.service
             ├─1842 "nginx: master process /usr/sbin/nginx -g daemon on; master_process on;"
             ├─1843 "nginx: worker process"
             └─1844 "nginx: worker process"

Що це означає: nginx запущено. Поради: воркери зазвичай працюють як www-data.

Рішення: Якщо сервіс падає або флапає, помилки доступу ймовірно будуть видні у journald тут. Якщо він запущений, але віддає старий сертифікат — ймовірно, не відбулося перезавантаження.

Завдання 6: Шукайте помилки дозволів у journald

cr0x@server:~$ sudo journalctl -u nginx -n 50 --no-pager
Dec 29 02:10:59 server nginx[1842]: nginx: [emerg] cannot load certificate "/etc/letsencrypt/live/app.example.com/fullchain.pem": BIO_new_file() failed (SSL: error:8000000D:system library::Permission denied:calling fopen(/etc/letsencrypt/live/app.example.com/fullchain.pem, r) error:10080002:BIO routines::system lib)
Dec 29 02:10:59 server systemd[1]: nginx.service: Control process exited, code=exited, status=1/FAILURE
Dec 29 02:10:59 server systemd[1]: nginx.service: Failed with result 'exit-code'.

Що це означає: Класика. nginx не може прочитати файл сертифіката. Часто через те, що файл доступний лише для root, а nginx у некоректний момент працює як невідпорядкований користувач, або через блокування traversal директорій.

Рішення: Не робіть chmod 777. Виправте дозволи за продуманою моделлю (див. розділ про дозволи).

Завдання 7: Перевірте traversal шляхів за допомогою namei (ловить пастку «директорія 0700»)

cr0x@server:~$ sudo namei -l /etc/letsencrypt/live/app.example.com/privkey.pem
f: /etc/letsencrypt/live/app.example.com/privkey.pem
drwxr-xr-x root root /
drwxr-xr-x root root etc
drwxr-xr-x root root letsencrypt
drwx------ root root live
drwxr-xr-x root root app.example.com
lrwxrwxrwx root root privkey.pem -> ../../archive/app.example.com/privkey4.pem

Що це означає: /etc/letsencrypt/live має права 0700, тому некореневі користувачі не можуть пройти в нього, навіть якщо сам файл мав більш відкриті біти.

Рішення: Якщо ваш застосунок працює не від root і читає прямо з live, він не зможе. Або зробіть застосунок читати з контрольованого копійованого шляху, або використайте ACL ретельно.

Завдання 8: Перевірте, чи файли сертифікатів правильно парсяться (ловить часткові записи або неправильні файли)

cr0x@server:~$ sudo openssl x509 -in /etc/letsencrypt/live/app.example.com/fullchain.pem -noout -text | grep -E "Not After|Subject:"
        Subject: CN = app.example.com
            Not After : Mar 29 02:10:10 2026 GMT

Що це означає: Файл на диску — валідний X.509 сертифікат і має очікуваний термін дії.

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

Завдання 9: Підтвердіть, що таймер/сервіс Certbot присутні й коли останній раз запускались

cr0x@server:~$ systemctl list-timers --all | grep -E "certbot|letsencrypt"
Mon 2025-12-29 02:07:41 UTC  10h left Mon 2025-12-29 00:08:12 UTC  11h ago certbot.timer                certbot.service

Що це означає: Оновлення обробляється таймером. Це нормально, але потрібно подивитися, що саме робить сервіс.

Рішення: Інспектуйте визначення certbot.service і поведінку хуків далі.

Завдання 10: Подивіться, що виконує systemd-сервіс Certbot (де можуть бути підключені хуки)

cr0x@server:~$ systemctl cat certbot.service
# /usr/lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/certbot/readme.Debian.gz
Wants=network-online.target
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

Що це означає: Він виконує certbot -q renew тихо. Тут немає --deploy-hook. Тож якщо ви не налаштували хуки в іншому місці, нічого не перезавантажиться.

Рішення: Додайте deploy hook у /etc/letsencrypt/renewal-hooks/deploy/ або зробіть override systemd-юниту (детальніше далі).

Завдання 11: Перевірте логи Certbot на виконання хуків і результат оновлення

cr0x@server:~$ sudo tail -n 60 /var/log/letsencrypt/letsencrypt.log
2025-12-29 02:10:11,214:INFO:certbot._internal.renewal:Cert is due for renewal, auto-renewing...
2025-12-29 02:10:12,992:INFO:certbot._internal.client:Successfully received certificate.
2025-12-29 02:10:13,103:INFO:certbot._internal.storage:Writing new private key to /etc/letsencrypt/archive/app.example.com/privkey4.pem.
2025-12-29 02:10:13,214:INFO:certbot._internal.storage:Deploying certificate to /etc/letsencrypt/live/app.example.com/fullchain.pem.
2025-12-29 02:10:13,215:INFO:certbot._internal.storage:Deploying key to /etc/letsencrypt/live/app.example.com/privkey.pem.

Що це означає: Оновлення відбулося. Але немає свідчення, що deploy hook запустився (рядки з виконанням хуків з’являються тут, якщо вони налаштовані).

Рішення: Реалізуйте deploy hooks. Якщо хуки є, але не виконуються — перевірте права виконуваності скриптів.

Завдання 12: Ручне перезавантаження з перевіркою конфігурації (щоб не перезапустити у зламаний стан)

cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Що це означає: Перезавантаження безпечне.

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

Завдання 13: Перезавантажте сервіс і перевірте віддаваний сертифікат

cr0x@server:~$ sudo systemctl reload nginx
cr0x@server:~$ echo | openssl s_client -servername app.example.com -connect 127.0.0.1:443 2>/dev/null | openssl x509 -noout -dates
notBefore=Dec 29 02:10:11 2025 GMT
notAfter=Mar 29 02:10:10 2026 GMT

Що це означає: Після перезавантаження віддаваний сертифікат збігається з оновленим.

Рішення: Ваше виправлення — «забезпечити перезавантаження після успішного оновлення». Тепер автоматизуйте це deploy hook-ом.

Завдання 14: Тест прав від імені користувача сервісу (єдиний тест, що має значення)

cr0x@server:~$ sudo -u www-data bash -lc 'head -n 1 /etc/letsencrypt/live/app.example.com/fullchain.pem'
head: cannot open '/etc/letsencrypt/live/app.example.com/fullchain.pem' for reading: Permission denied

Що це означає: Як і очікувалося, www-data не може прочитати файл. Якщо nginx потребує читання сертифікатів під час перезавантаження як www-data, у вас буде збій.

Рішення: Не «виправляйте» це зробивши приватні ключі доступними всім. Використайте модель з перезавантаженням від імені root, або контрольоване копіювання/ACL-підхід.

Виправлення дозволів без створення інциденту безпеки

Спокуса миттєва: chmod -R 755 /etc/letsencrypt, перезавантажитись, додому. Це працює до того моменту, як ви зрозумієте, що зробили приватні ключі читабельними для ширшого кола принципалів. В окремих середовищах це саме по собі інцидент.

Ось практична ментальна модель:

  • Секретність приватного ключа — головне. Якщо нападник читає privkey.pem, він може видати себе за ваш сервіс до моменту відкликання/ротації сертифіката й доки клієнти довіряють старому ланцюгу.
  • Більшість демонів не потребують, щоб воркер-користувач читав ключ, якщо master-процес стартує/перезавантажується як root і потім знижує привілеї. nginx — класичний приклад: master запускається як root, читає ключі, потім воркери працюють як непривілейовані.
  • Проблеми виникають, коли ви запускаєте сервіс повністю не-root (контейнери, жорсткі unit-і, кастомний користувач) і все ще вказуєте на /etc/letsencrypt/live.

Оберіть одну з трьох продуманих схем

Патерн A (переважний): перезавантаження сервісу виконується як root; файли сертифікатів залишаються root-only

Якщо ви можете перезавантажити nginx/Apache/HAProxy як root через systemd, залиште значення /etc/letsencrypt за замовчуванням. Це найпростіше й найбезпечніше.

Що робите: створіть deploy hook, що виконує nginx -t, а потім systemctl reload nginx. Файли сертифікатів лишаються root-only. Перезавантаження виконується з привілеями, тож сервіс зможе прочитати ключ.

Патерн B: контрольоване копіювання в директорію, доступну застосунку (добре для не-root застосунків)

Деякі застосунки (або контейнери) працюють повністю не-root і мусять читати ключі напряму. Не вказуйте їм /etc/letsencrypt/live. Натомість:

  • Створіть виділену директорію типу /etc/ssl/app.example.com/ з жорсткими правами й власником.
  • У deploy hook-у копіюйте fullchain.pem і privkey.pem туди за допомогою install (встановлює режим/власника атомарно достатньо для наших цілей).
  • Перезавантажте сервіс після копіювання.

Це зменшує площу ураження: ви не послаблюєте /etc/letsencrypt, а відкриваєте лише те, що потрібно, для конкретного користувача застосунку.

Патерн C: ACL на конкретні шляхи (використовуйте з обережністю, документуйте)

Ви можете використати POSIX ACL, щоб надати права читання/traverse користувачу сервісу лише для потрібних файлів/директорій. Це працює, але легко забути та складно аудитити в поспіху.

Якщо обираєте ACL, вбудуйте перевірку в runbook. Інакше ваше майбутнє «я» знову «виправить» це chmod о 3:00.

Чого не робити (якщо не любите інциденти)

  • Не робіть privkey.pem груповочитабельним для широкої групи на кшталт www-data, якщо в тій групі є інші сервіси. Це боковий рух як функція.
  • Не вказуйте сервіси напряму на /etc/letsencrypt/archive. Імена файлів інкрементуються на оновлення; ваша конфігурація не буде слідувати.
  • Не робіть хуки, які перезапускають критичні сервіси на кожному запуску таймера, навіть коли нічого не оновилося. Це самозавдана метушня.

Хуки перезавантаження правильно (deploy hooks, systemd і підводні камені)

Certbot має кілька типів хуків. Той, який вам потрібен для «перезавантажити після успішного оновлення», зазвичай — deploy hook. Він виконується лише коли сертифікат реально оновлено (або щойно видано), що зберігає стабільність сервісів у дні без змін.

Директорія deploy hook-ів (найпростіший і найменш несподіваний варіант)

Покладіть виконуваний скрипт у:

  • /etc/letsencrypt/renewal-hooks/deploy/

Certbot запустить його після того, як запише новий сертифікат і оновить символьні посилання в live.

Приклад: hook для reload nginx з перевірками безпеки

cr0x@server:~$ sudo install -d -m 0755 /etc/letsencrypt/renewal-hooks/deploy
cr0x@server:~$ sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload-nginx >/dev/null <<'EOF'
#!/bin/bash
set -euo pipefail

# Only reload if nginx is installed and running.
if ! command -v nginx >/dev/null 2>&1; then
  exit 0
fi

if ! systemctl is-active --quiet nginx; then
  exit 0
fi

# Validate config before reload; fail hook if config is broken.
nginx -t

# Reload picks up new cert without dropping connections.
systemctl reload nginx
EOF
cr0x@server:~$ sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx

Чому цей скрипт опініонований: він нічого не робить, якщо nginx відсутній або неактивний (корисно на серверах з кількома ролями), і відмовляється перезавантажувати при зламаному конфігу. Хуки мають бути безпечними в присутності чужих змін.

Протестуйте хук без очікування дня оновлення

Використайте dry run Certbot. Він імітує оновлення (staging) і запускає хуки.

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

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

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

Що це означає: Dry-run пройшов. Тепер підтвердіть, що хук виконався, перевіривши логи перезавантаження nginx або часові мітки в journald.

Рішення: Якщо dry-run працює, а продакшн-оновлення не перезавантажує — перевірте виконуваність файлів, SELinux/AppArmor або відмінності confinement у Snap-інсталяції.

Коли варто використовувати --deploy-hook замість директорії

Якщо ви хочете прив’язати хук до конкретного виклику (наприклад, спеціального юніта або для конкретного сертифіката), можна передати --deploy-hook в командному рядку. Але на Ubuntu з systemd-таймерами директорія хуків зазвичай проста й зручна, бо застосовується послідовно навіть коли люди вручну запускають certbot renew.

systemd override: коли треба контролювати поведінку централізовано

Якщо в організації наполягають, що «все має бути в systemd-юнитах», зробіть override сервісу:

cr0x@server:~$ sudo systemctl edit certbot.service
cr0x@server:~$ sudo systemctl cat certbot.service
# /usr/lib/systemd/system/certbot.service
[Unit]
Description=Certbot
Wants=network-online.target
After=network-online.target

# /etc/systemd/system/certbot.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/certbot -q renew --deploy-hook "systemctl reload nginx"

Що це означає: Ви замінили ExecStart на такий, що містить deploy hook. Він перезавантажить nginx лише при фактичних оновленнях.

Рішення: Виберіть або директорію хуків, або override юніту. Не робіть обох одночасно, якщо не хочете загадкових подвійних перезавантажень.

Підводний камінь: reload проти restart

Надавайте перевагу reload, якщо сервіс це підтримує правильно. Це менш руйнівно. Використовуйте restart коли:

  • Демон не вміє перечитувати сертифікати коректно (деякі app-сервера).
  • Ви всередині контейнера, де немає механізму «reload» і доводиться ребутати процес.
  • Ви перевірили, що reload не підхоплює нові ключі (рідко, але буває, залежно від інтеграції).

Жарт №2: Якщо ваш hook на оновлення для всього перезапускає сервіс, ви винайшли «планове відключення», лише з більшим елементом несподіванки.

Контейнери й зворотні проксі: місця, де перезавантаження вмирають

Ubuntu 24.04 не винайшла контейнери, але вона успадкувала їх улюблену властивість: вони роблять припущення про файлову систему чужою проблемою. Certbot запускається на хості, оновлює файли на хості, а ваш TLS-термінатор може бути:

  • nginx на хості (просто)
  • nginx у контейнері (проблеми з шаром файлів і сигналами)
  • Traefik/HAProxy у контейнері (опції динамічного перезавантаження різняться)
  • хмарний балансувальник (оновлення Certbot не має значення, якщо ви не завантажуєте туди сертифікати)

Випадок контейнера 1: сертифікати з хоста змонтовані в режимі read-only

Поширений патерн: змонтувати /etc/letsencrypt/live/app.example.com в контейнер. Контейнер може читати файли, але він не дізнається про зміни, якщо:

  • процес полює або стежить за змінами, або
  • ви тригерите сигнал перезавантаження в контейнері.

Certbot успішно оновив; контейнер все ще подає старий сертифікат; усе виглядає «добре». Це не проблема сертифіката. Це проблема lifecycle.

Випадок контейнера 2: не-root контейнер не може пройти крізь /etc/letsencrypt/live

Навіть із bind-монтом права директорії можуть блокувати доступ. Пам’ятайте попередній namei з live як 0700. Якщо ви монтуєте /etc/letsencrypt загалом, ви все одно можете бути заблоковані на корені монту. Правильний хід — зазвичай Патерн B: копіювання сертифікатів у директорію, доступну контейнеру з жорсткими правами, і монтування саме її.

Невідповідність шару зворотного проксі

Ще класика: ви оновлюєте сертифікати на сервері застосунку, але TLS термінується на фронт-проксі (nginx/HAProxy) або на балансувальнику. Ваш застосунок ніколи не віддає сертифікат клієнту, тож оновлення нічого не змінює. Тим часом важливий сертифікат лежить деінде й спокійно прострічає.

Операційна порада: мапуйте шлях рукопотискання. Важливий сертифікат — той, що на першому TLS-хопі від клієнта. Все інше — внутрішній трафік, хіба ви не робите mTLS по всьому ланцюгу.

Три корпоративні міні-історії (з болем)

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

У них була чиста налаштування: Certbot на Ubuntu, nginx, що термінує TLS, і кілька upstream-сервісів. Хтось перевів купу серверів на «жорсткіший базовий образ» і гордо зняв root-привілеї з кількох systemd-юнитів. nginx теж потрапив під це, бо «least privilege». Заява про зміну виглядала розумно; вона мала навіть підпис по безпеці.

Настав день оновлення Certbot. Таймер запустився, записав нові файли, оновив symlink-і й вивів успіх. Deploy hook тригернув перезавантаження. nginx спробував заново відкрити файли сертифікатів. І відразу впав, бо unit тепер працював як непривайледжований користувач без доступу до /etc/letsencrypt/live (яке мало права 0700 на рівні директорії).

Припущення було підступним: «Якщо воркери nginx можуть працювати не-root, то й nginx можна запускати не-root». Не завжди. Традиційно master nginx стартує як root спеціально, щоб зв’язати низькі порти і прочитати матеріал ключа, а потім знижує привілеї воркерів. Запуск всього nginx як не-root змінює, що він може читати, і раптом оновлення сертифікатів стає Reliability-подією.

Вони виправили це, відкотивши жорсткі політики для unit-у nginx (зберігши відокремлення воркерів) і записали явне правило: сервіси, що термінують TLS, повинні мати чіткий, переглянутий метод доступу до приватних ключів. Постмортем не звинувачував Certbot. Він звинувачував відсутність end-to-end тесту, який перевіряв би «термін дії сервера після оновлення».

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

Платформна команда хотіла зменшити кількість перезавантажень. Вони мали десятки сертифікатів по багатьох vhost-ах і вирішили запускати certbot renew щогодини «на всяк випадок», але перезавантажувати nginx лише раз на добу в окремому job-і. Менше перезавантажень, менше шуму, менше шансів порушити довгоживучі з’єднання. Ідея добре виглядала в табличці.

Потім додали новий домен з сертифікатом, що оновився раніше за щоденний час перезавантаження. Certbot його охоче оновив, але nginx віддавав старий сертифікат майже 24 години. Один клієнт з суворою перевіркою TLS почав падати. Служба підтримки бачила «удачний» лог оновлення і думала, що це проблема клієнта. Це не так було.

Оптимізація зламала прихований контракт: оновлення мусить бути пов’язане з деплоєм. Ви можете оптимізувати частоту перезавантажень лише якщо ваш TLS-термінатор вміє динамічно завантажувати сертифікати за handshake-ом (багато не вміють) або якщо ви реалізуєте розумнішу умову для перезавантаження. Інакше ви оптимізуєте не ту метрику: кількість перезавантажень замість коректності.

Виправили це, перезавантажуючи лише при реальних оновленнях (deploy hook) і додали перевірку-страхувальник: «сервіс має віддавати сертифікат з терміном як мінімум 20 днів». Це замінило припущення вимірюванням.

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

Інша організація мала нудне правило: кожен TLS endpoint повинен мати локальний скрипт, що виводить термін дії віддаваного сертифіката і порівнює його з тим, що на диску. Скрипт виконувався як щоденна перевірка стану і під час деплоїв. Ніхто його не любив. Ніхто не робив футболки з ним. Він просто був.

Одного ранку відбулося оновлення і проксі не перезавантажився. Hook існував, але пакетне оновлення замінило systemd-override і прибрало кастомний --deploy-hook. Таймер все одно запускався. Certbot оновлював сертифікат. Сервіс стояв і віддавав старий сертифікат, тож монітори uptime мовчали.

Нудний скрипт виявив це до того, як постраждали користувачі: «served expiry не збігається з live expiry». On-call мав одну команду, одне місце для перевірки і одне виправлення: відновити deploy hook через /etc/letsencrypt/renewal-hooks/deploy (що пережило зміну пакування краще).

Ця практика не запобігла помилці, але перетворила її на спокійний maintenance ticket замість публічного інциденту. Нудно, правильно і дивно героїчно.

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

1) «Certbot каже оновлено, але браузер показує прострочений сертифікат»

Симптом: certbot renew відпрацював успішно; клієнти все ще бачать старий термін.

Корінь: Сервіс не перезавантажився, або TLS endpoint — не той сервіс, який ви оновлювали (проксі/балансувальник).

Виправлення: Додайте deploy hook, щоб перезавантажити правильний процес; перевірте зі сторони клієнта за допомогою openssl s_client проти реального endpoint.

2) «nginx не може перезавантажитись після оновлення через Permission denied»

Симптом: journald показує BIO_new_file() failed ... Permission denied.

Корінь: Шлях до ключа/сертифіката нечитабельний через traversal (/etc/letsencrypt/live має 0700) або сервіс працює не-root.

Виправлення: Залиште master nginx привілейованим для читання ключів, або копіюйте сертифікат/ключ у контрольовану директорію, доступну користувачу сервісу.

3) «Hook запускається, але сервіс все одно віддає старий сертифікат»

Симптом: Хук виконався; команда перезавантаження пройшла; віддаваний сертифікат не змінився.

Корінь: Сервіс не використовує шляхи /etc/letsencrypt/live, або ви маєте інший vhost/SNI, або є інший TLS-термінатор попереду.

Виправлення: Підтвердіть шляхи конфігурації за допомогою nginx -T / конфігів Apache; перевірте SNI з -servername; простежте шлях рукопотискання.

4) «Все працює вручну, але автоматизація ламається»

Симптом: Ручний запуск certbot renew працює; systemd-таймер оновлює, але не перезавантажує.

Корінь: Ваша ручна команда містить прапорці/хук; у таймера їх нема. Або скрипти хуків не виконувані у контексті таймера.

Виправлення: Покладіть хуки у /etc/letsencrypt/renewal-hooks/deploy і переконайтесь у chmod 0755. Перевірте за допомогою certbot renew --dry-run.

5) «Після оновлення деякі клієнти падають з помилками ланцюга»

Симптом: Періодичні помилки «unable to get local issuer certificate», деякі клієнти працюють, деякі — ні.

Корінь: Ви налаштували ssl_certificate на cert.pem замість fullchain.pem, або міксований ланцюг між проксі.

Виправлення: Подавайте fullchain.pem для типових налаштувань nginx/HAProxy; тестуйте з openssl s_client -showcerts.

6) «Оновлення працює, але контейнер все одно віддає старий сертифікат»

Симптом: Файли на хості оновлені; трафік контейнера показує старий сертифікат.

Корінь: Процес у контейнері не перезавантажився, або точки монтування/права перешкоджають бачити оновлення.

Виправлення: Реалізуйте hook, що посилає сигнал контейнеру (або перезапускає його) після оновлення, або перемістіть TLS-термінацію на проксі, що підтримує динамічну зміну сертифікатів.

Чеклісти / покроковий план

Чекліст A: Зупинити кровотечу (відновити валідний TLS зараз)

  1. Перевірте віддаваний сертифікат з того endpoint, що бачить користувач (openssl s_client з SNI).
  2. Якщо сервіс віддає старий сертифікат: перезавантажте TLS-термінатор (systemctl reload nginx після nginx -t).
  3. Якщо перезавантаження падає: читайте journald на предмет помилок прав і виправляйте модель доступу (не давайте приватним ключам широкі права).
  4. Перевірте віддаваний сертифікат після перезавантаження. Лише потім закривайте інцидент.

Чекліст B: Зробити так, щоб оновлення реально розгорталось (щоб цього більше не повторилось)

  1. Вирішіть модель доступу: A (root reload), B (контрольоване копіювання), або C (ACL).
  2. Створіть deploy hook-скрипт у /etc/letsencrypt/renewal-hooks/deploy/.
  3. У хук-і: перевіряйте конфіг (nginx -t / apachectl configtest) перед перезавантаженням/рестартом.
  4. Запустіть certbot renew --dry-run і перевірте, що хук виконується (часові мітки в journald).
  5. Додайте health check, який порівнює віддаваний термін і on-disk термін (ловитиме зламані хуки після оновлень пакування).

Чекліст C: Модель дозволів для не-root сервісів (Патерн B)

  1. Створіть обмежену директорію, власником якої є користувач сервісу (або присвячена група) з правами 0750 або жорсткіше.
  2. Використайте install у deploy hook-і, щоб копіювати сертифікат і ключ з потрібними режимами/власниками.
  3. Налаштуйте застосунок на зчитування з копійованих шляхів, а не з каталогу Let’s Encrypt.
  4. Перезавантажуйте/рестартуйте сервіс після копіювання.
  5. Аудит: переконайтесь, що лише передбачені принципали можуть читати приватний ключ.

Приклад Pattern B hook: копіювання файлів і перезавантаження

cr0x@server:~$ sudo tee /etc/letsencrypt/renewal-hooks/deploy/publish-app-cert >/dev/null <<'EOF'
#!/bin/bash
set -euo pipefail

DOMAIN="app.example.com"
SRC_DIR="/etc/letsencrypt/live/${DOMAIN}"
DST_DIR="/etc/ssl/${DOMAIN}"

install -d -m 0750 -o root -g appsvc "${DST_DIR}"

# Copy with explicit permissions. Private key is readable only by root and group appsvc.
install -m 0644 -o root -g appsvc "${SRC_DIR}/fullchain.pem" "${DST_DIR}/fullchain.pem"
install -m 0640 -o root -g appsvc "${SRC_DIR}/privkey.pem"   "${DST_DIR}/privkey.pem"

# Validate service config if applicable, then reload.
if systemctl is-active --quiet appsvc; then
  systemctl reload appsvc || systemctl restart appsvc
fi
EOF
cr0x@server:~$ sudo chmod 0755 /etc/letsencrypt/renewal-hooks/deploy/publish-app-cert

Точка прийняття рішення: Зробіть групу appsvc вузькою, з лише обліковим записом сервісу. Не перевикористовуйте спільну групу лише тому, що вона існує.

FAQ

1) Чому Certbot успішно оновлює, а мій сайт все одно показує старий сертифікат?

Тому що оновлення змінює файли на диску, а не працюючий процес. Ваш TLS-термінатор має перезавантажитись або рестартнутись, щоб перечитати сертифікат/ключ.

2) Чи варто вказувати nginx на /etc/letsencrypt/archive, щоб уникнути символьних посилань?

Ні. Імена файлів в archive інкрементуються при оновленні (fullchain4.pem, fullchain5.pem). Використовуйте /etc/letsencrypt/live, щоб оновлення слідувало за symlink-ами.

3) Чи безпечно зробити /etc/letsencrypt/live читабельним для www-data?

Зазвичай ні. Це розширює коло, хто може прочитати приватний ключ. Віддавайте перевагу перезавантаженням від імені root (Патерн A) або контрольованому копіюванню в присвячену директорію (Патерн B).

4) У чому різниця між deploy hook і post hook?

Deploy hook виконується лише коли сертифікат реально оновлено/видано. Post hook виконується після кожного запуску Certbot, навіть якщо нічого не змінилося. Для перезавантажень deploy hooks — розумний вибір за замовчуванням.

5) Навіщо потрібен certbot renew --dry-run?

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

6) Мій сервіс працює в Docker. Як перезавантажити його з Certbot?

Або (a) монтуйте опубліковану директорію сертифікатів у контейнер і відправляйте сигнал/рестарт контейнеру через runtime у deploy hook-і, або (b) термінуйте TLS зовні контейнера на хост-проксі.

7) Я перезавантажив nginx, але клієнти все одно падають TLS. Що далі?

Перевірте конфігурацію ланцюга (подавайте fullchain.pem), невідповідність SNI (тестуйте з -servername) та чи не віддає інший фронт-проксі/балансувальник інший сертифікат.

8) Чи змінює Ubuntu 24.04 щось у роботі Certbot конкретно?

Більша зміна — у пакуванні та очікуваннях автоматизації: systemd timers поширені, відмінності між Snap і apt інсталяціями можуть бути, а confinement змінює файлові припущення. Ваші хуки мають відповідати способу інсталяції.

9) Як довести, що робочий процес завантажив новий сертифікат?

Порівняйте серійний номер/термін віддаваного сертифіката (через openssl s_client) з on-disk live сертифікатом. Якщо вони співпадають — процес його завантажив. Якщо ні — не завантажив.

Висновок: наступні кроки, що працюють

Надійний шлях на Ubuntu 24.04 — грубий та ефективний:

  1. Перевіряйте те, що бачать клієнти, а не те, що каже Certbot.
  2. Тримайте приватні ключі під замком. Виправляйте доступ конструктивно, а не через паніку з chmod.
  3. Зв’яжіть оновлення з деплоєм за допомогою deploy hook-а, який перевіряє конфіг і перезавантажує правильний сервіс.
  4. Додайте health check, що порівнює віддаваний термін і on-disk термін, щоб пакетні оновлення або рефактори не ламали вас тихо.

Якщо ви зробите лише одну річ після прочитання цього: створіть deploy hook у /etc/letsencrypt/renewal-hooks/deploy/, що безпечно перезавантажує ваш TLS-термінатор, а потім підтвердіть це за допомогою certbot renew --dry-run. Ось так ви перетворите «сертифікати автоматизовані» з гасла в властивість вашої продакшн-системи.

← Попередня
Ubuntu 24.04: монтування CIFS повертає «Permission denied» — точні опції для виправлення
Наступна →
Драйвери, що знижують продуктивність: ритуал після оновлення

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