У меня уже был опыт настройки автоматического получения сертификатов от Let's Encrypt. Плёвое дело, подумал я, и взял очередной заказ. Скопировал по аналогии все скрипты с другого сервера. Проверил с командной строки, поставил в cron. На всякий случай, на ближайшую дату. И что же. Наступает этот день и ничего. Стоит старый сертификат красуется. В чём дело, непонятно. С командной строки запустил, оп, и сертификат обновился. Совсем другое дело.
Выясняя, а почему такая разница между запуском из командной строки, в несколько итераций увеличил детализацию certbot. Он по какой-то причине долго молчал как партизан, ни слова из него вытащить не удавалось. Если потоки в файл перенаправить, они создаются, но пустые, хоть там какой debug и ещё вдогонку три раза verbose указать в параметрах. Понимайте как хотите. Опытным путём установлено, что у него развязывается язык, если запускать его из терминала. А если в cron нет терминала, то в качестве терминала сойдёт screen. Добавил в начало cron-скрипта
if [ -z "$STY" ]; then exec screen -dm -S Lets-Encrypt-Renewal /bin/bash "$0"; fi
Наконец, certbot разговорился. И начал жаловаться
requests.exceptions.ConnectionError: HTTPSConnectionPool(host='acme-v02.api.letsencrypt.org', port=443): Max retries exceeded with url: /directory (Caused by NewConnectionError(': Failed to establish a new connection: [Errno -3] Temporary failure in name resolution'))
Я в crontab время подгонял, чтоб скрипт запустился ещё раз и ещё раз. Получал в журнале Temporary failure in name resolution. Нет ничего более постоянного, чем временное. Практика показывает, что такая ошибка часто, но не всегда, а потом ещё может ошибка соединения возникать. Но стоит запустить скрипт из командной строки, и, о чудо, сертификат обновился с одной попытки. Правда, у LE есть rate limit, так что если с командной строки несколько успешных попыток сделать, потом двое суток нельзя будет убедиться, что скрипт действительно работает.
В системе используется systemd-resolved. В /etc/resolv.conf прописан сервер 127.0.0.53. Это вот с ним связь не ладится в контексте cron. Я могу с командной строки попинговать сервер Let's Encrypt, и в кеше DNS IP точно будет, вот только до 127.0.0.53 не сможет вот так просто взять и добраться программа, запущенная из cron. Тяжело связаться с локалхостом, связь с локалхостом по старым телефонным проводам, ну или не знаю, как это понимать. Попинговав с командной строки, я пришёл к идее попинговать из cron тоже. Результат:
PING ca80a1adb12a4fbdac5ffcbc944e9a61.pacloudflare.com (172.65.32.248) 56(84) bytes of data.
64 bytes from 172.65.32.248 (172.65.32.248): icmp_seq=1 ttl=56 time=5.50 ms
--- ca80a1adb12a4fbdac5ffcbc944e9a61.pacloudflare.com ping statistics ---
4 packets transmitted, 1 received, 75% packet loss, time 3058ms
rtt min/avg/max/mdev = 5.500/5.500/5.500/0.000 ms
Ещё бывают ответы Destination host unreachable от IP сервера, с которого делается пинг. С командной строки пинг 100%, из cron 25%. Стабильно проблемы. Сеть работает ни к чёрту из cron, а с командной строки (ssh) нормально. Один и тот же сервер, а почему такая разница.
Итак, у нас проклятый cron. cron запускает скрипт, и проклятье переходит на скрипт. Скрипт запускает screen и себя в нём, и проклятье переходит на экземпляр screen и на скрипт внутри screen. И дальше рекурсивно проклятье переходит на все программы, запущенные из скрипта, хоть ping, хоть certbot.
Ну что это может быть? Что-то такое, что наследуется процессами. Я подумал в сторону ulimit. Запустил
ulimit -a -S && ulimit -a -H
Из командной строки (SSH) и в журнал в контексте cron. Поиграл в игру «найди хоть одно отличие» и проиграл. Получилось два побайтно идентичных файла с содержимым:
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 63378
max locked memory (kbytes, -l) 2041946
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) 63378
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 63378
max locked memory (kbytes, -l) 2041946
max memory size (kbytes, -m) unlimited
open files (-n) 1048576
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) unlimited
cpu time (seconds, -t) unlimited
max user processes (-u) 63378
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
И с командной строки (SSH), и в cron одни и те же ограничения, и не видно разницы. Но разница есть. Какой-то яйцеголовый придумал ещё какие-то наследуемые особенности процесса, запрятал, и когда что-то пошло не так, я просто не знаю, где их искать.
Linux version 5.11.0-49-generic (buildd@lcy02-amd64-054) (gcc (Ubuntu 10.3.0-1ubuntu1) 10.3.0, GNU ld (GNU Binutils for Ubuntu) 2.36.1) #55-Ubuntu SMP Wed Jan 12 17:36:34 UTC 2022
bullseye/sid
Так что же это может быть
Дополнение 1.
Если создать пару service+timer, проблемы такие же, как в cron. Если запустить systemctl start my.service из командной строки (ssh), работает хорошо. Ещё попробовал сделать отдельный service для продления сертификатов, и другую пару service+timer, а старый таймер выключил и удалил. В новом service вызывается
ExecStart=/usr/bin/systemctl start old.service
Результат неизменный. Всё, что по таймеру, не работает, а с командной строки абсолютно такой же запуск systemctl start old.service работает. То есть, вместо того, чтобы честно передавать RPC в главный процесс systemd, чтобы главный процесс честно запускал одну и ту же службу всегда одним и тем же способом, systemctl запускает службу прямо из своего процесса systemctl и поэтому наследует тайное проклятье.
Я ещё раскопал prlimit, и он мне поведал, что в контексте cron необычно низкий memlock, 65536, а в ssh сильно больше. Поднял в скрипте memlock до миллиона, но чё-то как-то всё равно не помогло.