Правка ответа DNS

Потребовалось мне тут подменять “на ходу” ответы DNS. Это то самое, чем балуются провайдеры, когда на запросы типа facebook.com отдают 127.0.0.1. Только у меня ситуация чуть проще: и DNS мой и ответы мне нужны тоже … свои.

Конечно, самое простое решение – это подправить /etc/hosts. Но у меня машин много, бегать по каждой… Следующим – заиспользовать zone views. Но у меня зоны раскиданы и фиг его знает, откуда я приду. Немного погуглил и нашел другой вариант. В общем, иду на машинку, где работает named. Создаю файлик зоны. Он совершенно стандартный, за исключением записей.

# cat db.override
$TTL 60
@            IN    SOA  localhost. root.localhost.  (
                          2015112501   ; serial
                          1h           ; refresh
                          30m          ; retry
                          1w           ; expiry
                          30m)         ; minimum
                   IN     NS    localhost.

localhost       A   127.0.0.1
hostname.ru   CNAME    hrentam.zone.ru.

Следом прописываю этот файлик как мастер

zone "override" {
  type master;
  file "/etc/bind/db.override";
};

И добавляю в опции одну команду

options {
       .....
        response-policy { zone "override"; };
};

И после перезагрузки named мне начал отвечать тем, что описано в этой зоне. А на остальные запросы – как обычно.

Yubikey, ssh, неужели финал?

На всякий: в общем-то описанное ранее все работает и даже не вызывает раздражения, но только если ваша работа не связана с регулярными походами по многим ssh серверам. В реальности регулярное ожидание “прикоснитесь к токену” начинает раздражать уже на втором десятке коннектов. А если запустить какой-нибудь ансибл плейбук, так сидишь и настукиваешь по токену, пока плейбук пройдет по серверам. А потом, где-то на середине, опять начинаешь, ибо сессия протухла. В общем, бесит неимоверно.

После гуглежа выяснилось, что у юбиков для fido2 нет опции “не требовать прикосновения Н времени”. Для gpg есть, а для fido – нет. Боль и печаль. Поэтому самый простой способ купировать это поведение – это использовать controlpath в ssh.

Для ssh это лечится добавлением в .ssh/config следующих строк:

    ControlMaster auto
    ControlPath ~/.ssh/master-%r@%h:%p.socket
    ControlPersist 120m

Теперь, стоит вам зайти на любой хост, как любой последующий вход в течении двух часов будет происходить по уже установленной сессии. Никаких касаний и прочего не будет. Для ансибла делаем аналогично:

$ cat .ansible.cfg 
[defaults]
[ssh_connection]
ssh_args=-C -o ControlPath=~/.ssh/master-%r@%h:%p.socket -o ControlMaster=auto -o ControlPersist=120m

Теперь что ssh, что ansible будут использовать одни и те же пути, что приводит к повышению производительности. Но боль “коснись 100500 раз” просто загоняется вглубь. Что при первом запуске, что при последующих – все тоже самое. Опять гуглю. Выходит, что мимо gpg хоть так, хоть так не пройти. Ладно, у меня есть gpg!

$ gpg -K
...
sec   rsa4096 2017-09-11 [SC]
      F60000F98C6BC1B31FCAE943D9C4611813E9F51E
uid           [ultimate] Viacheslav Kaloshin <kiltum@kiltum.tech>
ssb   rsa4096 2017-09-11 [E]

Ох, аж с 2017 живет и есть пить-не просит, это хорошо. Так как у нас нынче не моден RSA, то добавляю 3 ключа ed25519:

$ gpg --quick-add-key F60000F98C6BC1B31FCAE943D9C4611813E9F51E ed25519 sign never
...
$ gpg --quick-add-key F60000F98C6BC1B31FCAE943D9C4611813E9F51E cv25519 encr never
...
$ gpg --quick-add-key F60000F98C6BC1B31FCAE943D9C4611813E9F51E ed25519 auth never
...
$ gpg -K
...
sec   rsa4096 2017-09-11 [SC]
      F60000F98C6BC1B31FCAE943D9C4611813E9F51E
uid           [ultimate] Viacheslav Kaloshin <kiltum@kiltum.tech>
ssb   rsa4096 2017-09-11 [E]
ssb   ed25519 2025-03-04 [S]
ssb   cv25519 2025-03-04 [E]
ssb   ed25519 2025-03-04 [A]

В принципе, для ssh можно добавить только последний ключ, для auth. Но я решил танцевать по-максимуму. Так, если верить докам, то сейчас у меня уже должена появиться возможность проэкспортить ssh ключ

$ gpg --export-ssh-key F60000F98C6BC1B31FCAE943D9C4611813E9F51E
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIImOGsDlDGh27/geO2YAf+lHcQAANqjjASvNhqY0Dp1H openpgp:0x0629B65F

Да, появилось. Это не может не радовать. Быстренько добавляю в стартап скрипты следующие строки, попутно вынося все, что было с ssh-agent

export GPG_TTY="$(tty)"
export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
gpgconf --launch gpg-agent

И добавляю поддержку ssh в gpg-agent

echo enable-ssh-support >> $HOME/.gnupg/gpg-agent.conf

Открываю соседний терминал и смотрю

$ ssh-add -L
The agent has no identities.

Вот на тебе по всей морде, а не ssh ключ! Обидно, гуглю дальше. Оказывается, gpg-agent слишком секурный и просто так никому ничего не покажет. Ему надо конкретно указать, какой ключ нужен. Для этого смотрим ид ключей и добавляем в спец-файлик

$ gpg -K --with-keygrip
/Users/vvkaloshin/.gnupg/pubring.kbx
------------------------------------
sec   rsa4096 2017-09-11 [SC]
      F60000F98C6BC1B31FCAE943D9C4611813E9F51E
      Keygrip = 5F821C94AA751454412CE7887AD0026C75AC3E54
uid           [ultimate] Viacheslav Kaloshin <kiltum@kiltum.tech>
ssb   rsa4096 2017-09-11 [E]
      Keygrip = 282D042F0CA040CF13C881FBBCD190D4FF3B895E
ssb   ed25519 2025-03-04 [S]
      Keygrip = 0D336DFDE44B04C2135022A5E486A4610C0F80EA
ssb   cv25519 2025-03-04 [E]
      Keygrip = EE62D0C4FE020623BC0372ED44196435A269A045
ssb   ed25519 2025-03-04 [A]
      Keygrip = AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317

$ echo AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317 >> .gnupg/sshcontrol 
$ ssh-add -L
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIImOGsDlDGh27/geO2YAf+lHcQAANqjjASvNhqY0Dp1H (none)

Ура! Заработало! Можно раскидать публичный ключик по серверам и попробовать. Лично у меня заработало…

И работало некоторое время, пока опять не начало клянчить пароль, но на этот раз от gpg. Ну это лечится гораздо проще:

$ cat .gnupg/gpg-agent.conf 
...
default-cache-ttl 34560000
max-cache-ttl 34560000

Итого, оно работает. Но где тут yubikey? А с ним оказалось еще проще. Надо просто перенести ключик из внутренней базы gpg в токен.

ВНИМАНИЕ! Сделайте бекап каталога .gnupg! Он понадобится для второго токена

Для начала выбираем ключ

gpg --edit-key kiltum@kiltum.tech

gpg> key 1

sec  rsa4096/D9C4611813E9F51E
     created: 2017-09-11  expires: never       usage: SC  
     trust: ultimate      validity: ultimate
ssb* rsa4096/B2698444DC05C50F
     created: 2017-09-11  expires: never       usage: E   
ssb  ed25519/406890D12CB9E70B
     created: 2025-03-04  expires: never       usage: S   
ssb  cv25519/973D20344A973490
     created: 2025-03-04  expires: never       usage: E   
ssb  ed25519/24C65C290629B65F
     created: 2025-03-04  expires: never       usage: A   
[ultimate] (1). Viacheslav Kaloshin <kiltum@kiltum.tech>

gpg> key 4

sec  rsa4096/D9C4611813E9F51E
     created: 2017-09-11  expires: never       usage: SC  
     trust: ultimate      validity: ultimate
ssb* rsa4096/B2698444DC05C50F
     created: 2017-09-11  expires: never       usage: E   
ssb  ed25519/406890D12CB9E70B
     created: 2025-03-04  expires: never       usage: S   
ssb  cv25519/973D20344A973490
     created: 2025-03-04  expires: never       usage: E   
ssb* ed25519/24C65C290629B65F
     created: 2025-03-04  expires: never       usage: A   
[ultimate] (1). Viacheslav Kaloshin <kiltum@kiltum.tech>

gpg> key 1

sec  rsa4096/D9C4611813E9F51E
     created: 2017-09-11  expires: never       usage: SC  
     trust: ultimate      validity: ultimate
ssb  rsa4096/B2698444DC05C50F
     created: 2017-09-11  expires: never       usage: E   
ssb  ed25519/406890D12CB9E70B
     created: 2025-03-04  expires: never       usage: S   
ssb  cv25519/973D20344A973490
     created: 2025-03-04  expires: never       usage: E   
ssb* ed25519/24C65C290629B65F
     created: 2025-03-04  expires: never       usage: A   
[ultimate] (1). Viacheslav Kaloshin <kiltum@kiltum.tech>

Обратите внимание на звездочку около ssb – команда key триггерит выбор ключа, а не отменяет последний выбор! И наконец, записываю ключ в yubi

gpg> keytocard
Please select where to store the key:
   (3) Authentication key
Your selection? 3

sec  rsa4096/D9C4611813E9F51E
     created: 2017-09-11  expires: never       usage: SC  
     trust: ultimate      validity: ultimate
ssb  rsa4096/B2698444DC05C50F
     created: 2017-09-11  expires: never       usage: E   
ssb  ed25519/406890D12CB9E70B
     created: 2025-03-04  expires: never       usage: S   
ssb  cv25519/973D20344A973490
     created: 2025-03-04  expires: never       usage: E   
ssb* ed25519/24C65C290629B65F
     created: 2025-03-04  expires: never       usage: A   
[ultimate] (1). Viacheslav Kaloshin <kiltum@kiltum.tech>

Note: the local copy of the secret key will only be deleted with "save".
gpg> save

Вроде ничего не произошло страшного, кроме запроса admin кода, верно? Проверяю

[vvkaloshin@sc-mac-00565 ~]$ gpg -K
/Users/vvkaloshin/.gnupg/pubring.kbx
------------------------------------
sec   rsa4096 2017-09-11 [SC]
      F60000F98C6BC1B31FCAE943D9C4611813E9F51E
uid           [ultimate] Viacheslav Kaloshin <kiltum@kiltum.tech>
ssb   rsa4096 2017-09-11 [E]
ssb   ed25519 2025-03-04 [S]
ssb   cv25519 2025-03-04 [E]
ssb>  ed25519 2025-03-04 [A]

Теперь у ключика, который в yubi, появился символ >. Дескать, он во внешней штуке. Так и должно быть. Но у меня есть второй yubi, в него бы тоже этот ключик засунуть… Как быть? А очень просто: убиваем gpg-agent, восстанавливаем бекап .gnupg, вставляем второй ключ и повторяем операцию с записью ключа. Один-в-один, без всяких изменений.

И теперь барабанная дробь! Проверяю

$ ssh-add -L
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIImOGsDlDGh27/geO2YAf+lHcQAANqjjASvNhqY0Dp1H cardno:23_059_666

$ ssh-add -L
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIImOGsDlDGh27/geO2YAf+lHcQAANqjjASvNhqY0Dp1H cardno:23_059_689

Выше я, абсолютно ничего не делая, просто поменял токен. gpg-agent опять оказался достаточно разумным, чтобы разрулить эту ситуацию. Теперь у меня есть аж два варианта работы с этим ключем: либо с помощью юбиков, либо восстановить .gnupg из бекапа и забить на юбики. Ключ не изменится! А что самое главное, ключ защищен аж в двух местах: пароль на gpg и пин на юбике. Оба хоть и кешируются (тут удобство для меня), но при перезагрузке/смене ключа спрашиваются (тут радуется внутренний безопасник).

Что необходимо сделать следующим? Во-первых, положить бекап .gnupg в сухое место. Во-вторых, пройтись по всяким серверам и сервисам и добавить новый ssh ключ. И наконец, самое больное: пройтись по всяким серверам и сервисам и заменить публичный pgp ключ. Причина простая и прозаическая: мы добавили новых ключей и всякие git (вы же пользуете подпись в git?) стали автоматом использовать свежее и более защищенное. Пруф:

$ git log --show-signature -1
commit 37ad3b5134621b1df18644b10884b8d17f2879f9 (HEAD -> main, origin/main, origin/HEAD)
gpg: Signature made Tue Mar  4 14:12:19 2025 MSK
gpg:                using EDDSA key 12D3317E74826BD633976A76406890D12CB9E70B
gpg: Good signature from "Viacheslav Kaloshin <kiltum@kiltum.tech>" [ultimate]

Вообще зря я вас пугал болью (смаил). Это делается просто:

$ gpg --armor --export
-----BEGIN PGP PUBLIC KEY BLOCK-----

mQINBFm2d2MBEACy9YyMqPeEZEIgndjhyYOOz61WXBZtyrOaeNXlfRy2HjZZ9os2
....
=KxVu
-----END PGP PUBLIC KEY BLOCK-----

И вот этот вот текст вы суете во всем места, где просят GPG public key.

Где были проблемы? На моем пути была только одна: если у вас в gpg стоит pinentry не графический (читай: просто pinentry или pinentry-ncurses), то при попытке добавления другого, старого и проверенного ключа я получал облом.

$ ssh-add .ssh/backup/id_ed25519
Enter passphrase for .ssh/backup/id_ed25519: 
Could not add identity ".ssh/backup/id_ed25519": agent refused operation

Лечится это странной командой

$ echo UPDATESTARTUPTTY | gpg-connect-agent
OK
$ ssh-add .ssh/backup/id_ed25519
Enter passphrase for .ssh/backup/id_ed25519: 
Identity added: .ssh/backup/id_ed25519 (kiltum@kiltum.tech)

На всякий: удаляются такие ключи из базы gpg тоже не сложно:

$ gpg-connect-agent 'keyinfo --ssh-list' /bye 
S KEYINFO AE9CE2AC2E83722BE5F0508C7D131E91CD5FA317 D - - - P - - S
S KEYINFO 716AA423769B73317BBA874ECFD11765948B5EFD D - - - P - - S
OK
$ gpg-connect-agent "delete_key --force 716AA423769B73317BBA874ECFD11765948B5EFD" /bye
OK

Yubikey, часть 2. Опять ssh…

В прошлой части я писал про resident ключи. И вроде все работало. А теперь решил попробовать поработать с non-resident. И тут же столкнулся с тем, что хоть ssh-add и работал, но заходить на сервера не получалось. Если запустить ssh -v, то можно было увидеть строчки from agent: agent refused operation

Немного нагуглив, я нашел рецепт: дескать, ssh-agent нужно подтвердить, а ему не у кого. И дескать, лечится установкой ssh-askpass и правки скрипта, который пускает ssh-agent. Ок, правлю вот так:

eval $(ssh-agent -s; SSH_ASKPASS=/opt/homebrew/bin/ssh-askpass) > /dev/null

Все остальное оставляю прежним. Грохаю все и пытаюсь зайти

...
debug1: Will attempt key: /Users/xxxxxx/.ssh/id_ed25519 
debug1: Will attempt key: /Users/xxxxxx/.ssh/id_ed25519_sk 
debug1: Will attempt key: /Users/xxxxxx/.ssh/id_xmss 
debug1: Offering public key: xxxxxx@yyyyy.ru ED25519-SK SHA256:4GX39PbvB7LoXoBzPrxABAt5xC+x05y6WNtgW3qAN+A authenticator agent
debug1: Server accepts key: xxxxxx@yyyyy.ru ED25519-SK SHA256:4GX39PbvB7LoXoBzPrxABAt5xC+x05y6WNtgW3qAN+A authenticator agent
sign_and_send_pubkey: signing failed for ED25519-SK "xxxxxx@yyyyy.ru" from agent: agent refused operation
debug1: Offering public key: xxxxxx@yyyyy.ru ED25519-SK SHA256:W/dXsAitpJKpPcYNf2zB+ucsG+zx50R0hSzIQ2Lwl74 authenticator agent
debug1: Server accepts key: xxxxxx@yyyyy.ru ED25519-SK SHA256:W/dXsAitpJKpPcYNf2zB+ucsG+zx50R0hSzIQ2Lwl74 authenticator agent

debug1: Enabling compression at level 6.
Authenticated to 10.34.178.1 ([10.34.178.1]:22) using "publickey".
debug1: setting up multiplex master socket
...

В логах видно, что у меня два ключа. Первый я вынул и спрятал. А вот второй – на месте. Вот где я добавил пустую строчку – там он стал ждать прикосновения к ключу. Никаких запросов или там окошек всплывающих не появилось. Нафига нужен ssh-askpass? Не знаю. Я даже попробовал перезагрузиться и ключ вставить-вынуть – ничего не поменялось…

Готовим темплейт под свой вкус

Ситуация совершенно стандартная: есть что-то из proxmox, vmware, openstack и нам изредка требуется создавать в нем машинки. Можно пойти классическим путем: закачиваем исошку, создаем виртуалку с этой исошкой и ставим туда все, что нужно. Для ускорения можно добавить cloud-init, всякие конфиги для анаконд и прочее. Ну и потом все это полирнуть ансиблом и паппетом по вкусу. Есть хипстерский вариант: затащить какой-нить терраформ и клауд образа и им штамповать машинки. Больно, долго и муторно, как и все в IaaC.

А можно совместить приятное с полезным. Сделать виртуалку, напихать в нее всё, что надо и как надо, а потом преобразовать ее в темплейт. И уже потом из этого темплейта штамповать виртуалки в любом количестве. По скорости разворота этот способ уделывает все остальные на порядок.

Но тут есть маленькая проблемка: если сделать все прямо в лоб, то все новые машинки будут иметь одинаковые ссш ключи и будут получать один и тот же ip адрес. Итак, рецепт ниже.

Шаг первый: подготавливаем виртуалку как надо вам.

Шаг второй: делам вот такой вот микроюнит

# cat /etc/systemd/system/regenerate_ssh_key.service
[Unit]
Description=Regenerate SSH host keys
Before=ssh.service

[Service]
Type=oneshot
ExecStartPre=-/bin/dd if=/dev/hwrng of=/dev/urandom count=1 bs=4096
ExecStartPre=-/bin/sh -c "/bin/rm -f -v /etc/ssh/ssh_host_*_key*"
ExecStart=/usr/bin/ssh-keygen -A -v
ExecStartPost=/bin/systemctl disable regenerate_ssh_key

[Install]
WantedBy=multi-user.target

Шаг третий: перечитываем юниты systemctl daemon-reload и включаем этот юнит systemctl enable regenerate_ssh_key.service

Шаг четвертый: чистим machine-id truncate -s 0 /etc/machine-id

Шаг последний: выключаем машинку и делаем из нее шаблон.

Вы великолепны!

Yubikey, часть 1. Ну хотя бы ssh…

Долго ли, коротко, но в руках у меня снова оказался yubikey. Да, вот эта вот штука, которая устами манагеров обещает устойчивость ко взлому, фишингу и прочим ужасам современного ИТ-мира. Прошлый мой подход к юбикам окончился полной неудачей из-за кривого софта, драйверов и в общем-то моим нежеланием разбираться во всей этой мешанине букв и аббревиатур. Но тут отступать оказалось некуда, поэтому пришлось разбираться.

Итак, для начала. Если вы это делаете для себя, то берите минимум (подчеркиваю, минимум) два юбика. Дело в том, что юбики из-за природы своей не бекапятся, не обновляют прошивку и так далее и тому подобное. Если вам это вручили на работе, то вся боль по поводу работоспособности юбиков совершенно не ваша забота.

В общем, для начала надо заиметь свой юбик и поставить на комп Yubikey Manager. Ставим, запускаем, втыкаем ключ и вы должны увидеть что-то подобное этому:

Если увидели, то хорошо. Не увидели – дальше нет никакого смысла продолжать, надо разбираться, почему оно не алле.

Так как я хочу для начала настроить ssh, то (тут пропущено часов Н чтения мануалов и страничек) сходить на вкладку Applications – FIDO2 и нажать на кнопачку Change Pin. Ибо стандартный пин 123456 совершенно не секурный и вообще. После смены пина должно получиться следующее:

Ну а теперь самое интересное: необходимо сделать новый ssh ключ. Старые не подойдут, но в общем-то и не очень и хотелось. И тут внезапно появляется выбор: какой ключ делать? Если по типу шифрования вопросов нет – только ed25519, то вот резидентный или не резидентный ключ генерить…

Немного погуглив, я выяснил, что Discoverable Credential или Resident ключ – это тот, который полностью хранится в памяти ключа. А вот Non-Resident – это хранение ключей как обычно, в файликах, но без юбика они бесполезны. В чем проблема? Если у вас resident ключ и у вас сперли юбик, то злой дядька хакер сможет ходить на ваши сервера как к себе домой. Надежда на пин-код слабая, ибо подсмотреть его как нефиг делать. А если у вас non-resident ключи, то вам нужно будет таскать файлики ключей по своим рабочим машинкам. Выбирать вам. Лично я тут топлю за non-resident.

Далее все совершенно стандартно. Генерим ключ (ВНИМАНИЕ! -O resident тут сами понимаете для чего)

ssh-keygen -t ed25519-sk -O resident

… И копируем получившийся id_ed25519_sk.pub по нужным местам. Тут процедура тоже совершенно стандартная и описана 100500 раз везде.

В общем-то почти все. Набираем ssh user@host и получаем… облом по полной. Полное впечатление, что ssh тупо завис. Однако это не так, ssh просто ждет касания ключа, только почему-то не пишет об этом. Если оно горит (мне нет), то опять гуглим. Оказывается, если у вас для ssh есть обычные ключи и они уже добавлены в ssh-agent (вот тут не уверен), то для нового ключа ssh ничего решает не спрашивать. Правим .ssh/config

Host 10.10.10.10
    IdentitiesOnly yes 
    IdentityAgent none
    IdentityFile ~/.ssh/id_ed25519_sk

И пробуем снова. У меня получилось и он стал спрашивать. Так как я делал resident ключ, то теперь радостно размахивая шашкой, удаляю все честно нагенеренное, имитируя новую машину. Или после ребута. Смотрим, какие есть ключи

$ ssh-add -l
2048 SHA256:oJQZvvLl+oly0wnbSLXLQeaM76/VcZcgVNK/l1SflNQ /Users/vvkaloshin/.ssh/id_rsa (RSA)
256 SHA256:5CRS8At4Z4c3AqWK8v3vfremj7IA6sYVUrlYjWlBct0 kiltum@kiltum.tech (ED25519)

Все ок, это мои старые, “файловые”. Добавляем то, что в юбике

$ ssh-add -K
Enter PIN for authenticator: 
Resident identity added: ED25519-SK SHA256:hnN8L0j69XKrCn7emw8WkP/x0D5UXgYvLizweiL//g8
$ ssh-add -l
2048 SHA256:oJQZvvLl+oly0wnbSLXLQeaM76/VcZcgVNK/l1SflNQ /Users/vvkaloshin/.ssh/id_rsa (RSA)
256 SHA256:5CRS8At4Z4c3AqWK8v3vfremj7IA6sYVUrlYjWlBct0 kiltum@kiltum.tech (ED25519)
256 SHA256:hnN8L0j69XKrCn7emw8WkP/x0D5UXgYvLizweiL//g8  (ED25519-SK)

Вот. ED25519-SK это тот, что “спрятан” в юбике. Пробуем ходить по ssh… ходится! На этом этапе можно начинать бросать чепчики в воздух и кричать “ура!”.

Ну и на всякий, ssh-keygen -K “вытащит” из юбика публичный ключ прямо в текущий каталог. Правда, получившийся приватный ключ получится фейковым non-resident и все равно, без юбика ничего работать не будет.

NixOS – Внезапный переход

Вы не поверите, но снова подтвердилась теория о том, что компьютеры обладают душой. Стоило мне написать предидущий пост, как ubuntu совершила самоубийство. Я просто закрыл ноутбук и он больше не вышел из сна. И потом вообще отказался загружаться – даже груб не мог запуститься.

Глубоко вздохнув, я воткнул флешку с NixOS и поставился. Быстренько поделал работу и просто пошел по сервисам, которые мне нужны. Очень сильно помогла https://nixos-and-flakes.thiscute.world/nixos-with-flakes/introduction-to-flakes , где буквально по шагам описывается, что необходимо сделать.

В общем, буквально пара дней и у меня появилась почти удовлетворяющая меня система. И более того, в случае чего я могу повторить ее буквально за несколько минут. Всякие мелочи типа скорости курсора и прочее пока приходится настраивать руками.

И да, как положено, делюсь своей репой https://github.com/kiltum/nixos

NixOS – первые шаги

Как-то в очередной раз меня задолбала убунта своими приколами. Уже не помню, чем конкретно, но то, что задолбала – помню (смаил). И решил я устроить очередной чес по интернетикам на тему “а не придумали ли чего-то нового”. И, внезапно, индеец Джо обнаружил что у барака нет одной стены! В смысле оказалось что придумали, да еще как придумали!

Когда я первый раз прочитал про NixOS, я почему-то совершенно не поверил прочитанному. Ну согласитесь, все мы любим изредка приукрашивать действительность. А тут декларируют полную развязку системы и пользователя. Дескать, можно одной командой сменить Gnome на KDE и потом сделать так, чтобы от гнома и следов не осталось. Или если что-то накосячил в системе, то бах и вернуться на предидущее состояние. Или на пред-пред-предидущее. Уже хорошо, да? А следом еще одна плюшка: все состояние системы описывается в текстовых файлах. И контрольный в голову: они заранее подумали, что надо работать на линуксах, макосях и WSL. И обещают кросс-платформенное управление домашним каталогом.

В общем, начитался я интернетиков и пошел ставить эту nixos. Первая попытка получилась на удивление удачной. Воодушевившись, я пошел пробовать остальные фичи… И закопался напрочь. Я пробовал раз за разом, но мануалы в духе “рисуем два овала и потом дорисовываем так, чтобы получилась сова” не очень способствовали успеху. Я бы плюнул и списал на “да это еще один дистрибутив для повернутых”, но первая фича работала как железный лом. Чтобы я не делал, как бы не издевался над системой, но я всегда мог откатиться на любое число шагов назад. Единственное, чего она не переживала – это форматирование дисков 🙂

В общем, ниже очень краткая выжимка из кучи мануалов. В результате вы получите голую систему, то, что в других дистрибутивах называют minimal.

Шаг первый: идем на nixos.org и качаем минимальный исошник. Можно попробовать с графикой, но он у меня под виртуалбоксом не запустился почему-то.

Шаг второй: грузимся с этого исошника. Получаем консоль, в которой можно руками настроить сеть (про wifi пока не пробовал, но по ethernet все получается автоматом).

Шаг третий: меняем пароль root и цепляемся в систему снаружи.

Шаг четвертый: размечаем диски. Я ленивый, поэтому использую btrfs на весь диск. А дальше – сабволумами.

DISK="/dev/sda"
parted -s ${DISK} -- mklabel gpt
parted -s ${DISK} -- mkpart ESP fat32 1MB 512MB
parted -s ${DISK} -- set 1 esp on
parted -s ${DISK} -- mkpart nixos btrfs 512MB 100%

mkfs.fat -f -F 32 -n boot ${DISK}1
mkfs.btrfs -f -L NIXOS ${DISK}2

mount -t btrfs ${DISK}2 /mnt
btrfs subvolume create /mnt/root
btrfs subvolume create /mnt/home
btrfs subvolume create /mnt/nix
btrfs subvolume create /mnt/swap
umount /mnt

mount -o compress=zstd,subvol=root ${DISK}2 /mnt
mkdir /mnt/{boot,nix,home,swap}
mount -t vfat -o defaults,noatime ${DISK}1 /mnt/boot
mount -t btrfs -o noatime,compress=zstd,subvol=nix ${DISK}2 /mnt/nix
mount -t btrfs -o noatime,compress=zstd,subvol=home ${DISK}2 /mnt/home
mount -o subvol=swap ${DISK}2 /mnt/swap
btrfs filesystem mkswapfile --size 2g --uuid clear /mnt/swap/swapfile

Все, диск разбит и даже сделан своп фаил на 2 гига.

Шаг пятый: генерируем конфигурацию из того, что есть. Просто nixos-generate-config --root /mnt

И теперь добавляем посредине (вы поймете, там реально просто) нужное nano /mnt/etc/nixos/configuration.nix

fileSystems = {
  "/".options = [ "compress=zstd" ];
  "/home".options = [ "compress=zstd" ];
  "/nix".options = [ "compress=zstd" "noatime" ];
  "/swap".options = [ "noatime" ];
};

swapDevices = [ { device = "/swap/swapfile"; } ];

users.users.kiltum.initialHashedPassword = "$6$e8/m1MmQORdTvNsS$IpPlxYoW9C3UU9uPUE1fO9SlX0f6c86au.NvaxBa34K8HAtoMaOZ3NXa.Jpa9LKCZL5rrz3hlBreTjxU4mfRc0";
users.users.kiltum.isNormalUser = true;
users.users.kiltum.extraGroups = [ "wheel" ];

services.openssh.enable = true;
users.users.kiltum.openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIwTbklxgidWB5w+tpw6aRE2ZZuJdpdyOqGWX44Duu8G kiltum@kiltum.tech"
  ];
users.users.root.openssh.authorizedKeys.keys = [
    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIwTbklxgidWB5w+tpw6aRE2ZZuJdpdyOqGWX44Duu8G kiltum@kiltum.tech"
  ];

Хеш пароля генерится просто nix-shell --run 'mkpasswd -m SHA-512 -s' -p mkpasswd

Все. За исключением первых строчек, которые нужны исключительно для btrfs и выдраны мной из хаутушки, остальные понятны с первого взгляда.

Просто говорим, что пользователь kiltum обычный юзер, хеш его пароля вот такой, и он входит в группу wheel. Далее включаем ssh и указываем публичные ключи для kiltum и root.

Нет, это реально все. После команды nixos-install --no-root-passwd случится вся магия. Оно пойдет, накачает чего-то, надрыгает винтом, пошумит кулером и вывалится назад в консоль. Можно смело ребутить и вытаскивать то, с чего грузились.

Загрузившись, потыкался и понял, что мне опять приходится вводить пароль на sudo. Надо убрать. Читаем доки (ведь надо по-правильному), там пишут, что надо добавить security.sudo.wheelNeedsPassword = false; в /etc/nixos/configuration.nix . И потом sudo nixos-rebuild switch.

Все, запрос пароля пропал. Но если вы сейчас перезагрузитесь, то обнаружите в бутменю два варианта. В последнем запроса пароля не будет, а в предпоследнем – будет. Мелочь? Да. Но это абсолютно точно так же работает и для всего другого. Добавляем следующее:

services.xserver = {
  enable = true;
  displayManager.gdm.enable = true;
  desktopManager.gnome.enable = true;
}

И у нас после sudo nixos-rebuild switch появится Gnome. Надоел? Ок, можно и сменить.

services.xserver.enable = true;
services.displayManager.sddm.enable = true;
services.desktopManager.plasma6.enable = true;

Пересбор конфигурации и у нас KDE. Не понравилось? Можно спокойно вернуться в Gnome или в голую систему. Самое оно для “попробовать что-то другое, но не ломая существующее”. Раньше я такое мог получить только с помощью снапшотов. А тут … магия! 🙂

И, внезапно, вот все вот это выше написанное, типа разбивки дисков можно делать автоматом. Просто скормив в инсталлере длинную строку с урлом репы. Примеры я видел, но пока такой магии не научился. Зачем? Ну если есть куча хостов, то ситуации класса “вылетел диск? Надо вернуть все как было” или “сделай мне еще точно такой же” решаются за столько времени, сколько надо для похода к этому хосту и вставки исошника.

Есть ли минусы? Есть конечно.

Самый главный: вам нужен интернет. Много. Нет, там можно как-то и без него, но это как с гентой: пока все нужное выкачаешь, уже забудешь, что хотел.

Второй: оно реально сложное для первоначального старта. Кажущаяся легкость разбивается о твердые грани хотелок. Но поднимать таким макаром хосты (пока тестовые) у меня получается уже быстрее, чем из темплейта и потом проходить ансиблом.

Остальное… Скажу так, слишком пока неоднозначное для меня.

Соеденить две площадки…

… Казалось бы, что может пойти не так?

Недавно мне потребовалось соеденить две площадки с виртуалками. Обе у нас (ПОДЧЕРКИВАЮ!), обе у достаточно крупных провайдеров. В общем, надо сделать так, чтобы Н машин у одного провайдера видели М машин у другого. Трафик не большой, но достаточно критичный.

“ХА!” – сказал я и нарисовал классическую схему соединения.

Выделяем на каждой площадке машинку (или на одну из доступных вешаем внешний ip), обвешиваемся файрволами и закрываем трафик тем же IPSec. Инструкций много, вариантов много – в общем, прорвемся!

Быстренько собрал, накидал конфиги и получил веслом по морде. Протокол ESP где-то по пути заблокирован. Техподдержка обоих провайдеров клянется, что это не у них, но ipsec не верит и отказывается подниматься. Ладно, хотелось по-корпоративному, пойдем по-молодежному.

OpenVPN шустро поднялся, поначалу начал бодро гонять трафик, но через некоторое время начались проблемы. Он переподсоединялся, слал немного байт и снова уходил в нирвану. Смена протокола с UDP на TCP приносила лишь временное облегчение.

Кто виноват – мне, если честно говорить, абсолютно пофиг. Мне трафик нужно гонять. Поэтому расчехлил тяжелую хипстерскую артиллерию – shadowsocks + v2ray. Качаем с гита, просто распаковываем, плюем в конфиг сервера следующее (1.1.1.1 – это внешний адрес сервера, если что):

{
  "server": "1.1.1.1",
  "server_port": 888,
  "password": "verysecretpass",
  "method": "chacha20-ietf-poly1305",
  "timeout": 7200,
  "no_delay": true,
  "fast_open": true,
  "mode": "tcp_and_udp",
  "plugin": "/opt/shadowsocks/v2ray-plugin_linux_amd64",
  "plugin_opts": "server"
} 

А в конфиг клиента вот это:

{
  "server": "1.1.1.1",
  "server_port": 888,
  "local_address": "127.0.0.1",
  "local_port": 1080,
  "password": "verysecretpass",
  "method": "chacha20-ietf-poly1305",
  "timeout": 7200,
  "no_delay": true,
  "fast_open": true,
  "mode": "tcp_and_udp",
  "plugin": "/opt/shadowsocks/v2ray-plugin_linux_amd64",
  "plugin_opts": ""
}

Запускаем и получаем на клиенте socks5 сервер на 1080 порту. А теперь – финт конем. В конфиг OpenVPN на клиенте добавляем одну единственную строчку:

socks-proxy 127.0.0.1 1080

Ну и перезапускаем все, чтобы прочитало конфиги. Вуаля! У нас получилась следующая схема:

OpenVPN ходит через ShadowSocks, который ходит через хрен знает как настроенный интернет.

Но через некоторое время проявилась та же самая боль: немного погодя клиент терял соединение с сервером. Причем перезапустишь – все снова начинает бегать. Ставил опции ping, менял MSS и MTU – пофиг: рандомно теряем коннект.

Ок, перевел openvpn и shadowsock на TCP. Всё магически исправилось. Коннект стабильный, не рвется, пакетики бегают туда-сюда.

Отключил "mode": "tcp_and_udp", позакрывал фаирволлами и начал тестить.

Итак, прямой tcp линк безо всяких штук

[  1] 0.0000-10.2561 sec   112 MBytes  91.8 Mbits/sec

Да, клиент сидит на дешевом тарифном плане в 100 мегабит, поэтому практически упираемся в полку. Теперь через все эти навороты:

[  1] 0.0000-10.3474 sec  50.8 MBytes  41.1 Mbits/sec

И это без какого-либо тюнинга! Да, tcp-over-tcp-over-tcp еще тот изврат, но ведь работает же! А после тюнинга (банальные буфера и прочее – в любом мануале по openvpn) я получил следующее:

[  1] 0.0000-100.4276 sec   896 MBytes  74.8 Mbits/sec

Что в плюсах:

  1. Нам надо платить меньше. На одной стороне не нужен выделенный ip (а они нынче дорогие). Выпускают всех через SNAT и норм.
  2. Настраивается не просто, а очень просто.
  3. Снаружи на сервере порт OpenVPN можно спокойно закрыть фаирволлом. Соединение идет с локалхоста. Больше сесуретей богу сесурити!
  4. Память не жрет. Можно смело брать самую дешевую виртуалку под “роутеры”. Вся вот эта машинерия + FRR с OSPF сьели 200 мегов.
  5. Оно работает. Реально, за неделю уже не одиного разрыва.

Что в минусах:

  1. Потеряли в скорости. Немного, но есть. Проблема в том, что на стороне клиента я банально уперся в единственный ЦПУ виртуалки. Когда тесты идут, на той стороне в топе такое:

С другой стороны, там трафик в 20-30 мегабит уже редкость, так что в любом случае все в порядке. Но запас карман не тянет.

Ну и нафига я все это делал: мне нужно было среплицировать один MySQL в другой. Предложенная схема их удовлетворила, значит задача выполнена.

Influx и Telegraf

Поначалу я планировал этот пост про InfluxDB. Дескать, вот так поставим, вот так настроим… Однако реальность больно тыкнула вилкой в глаз: поставил, запустил, зашел на influx:8086, установил пароль, завел организацию… и всё. Нет, реально всё. Больше там ничего не потребовалось делать.

А вот с телеграфом пришлось повозиться. Дело в том, что я возжелал с одной машины отправлять разные метрики в разные бакеты. Ну всякие там “загрузка процессора и памяти” в один, а “температура в доме” – в другой. Ну вот захотелось мне так.

Все просто. Для начала прямо в telegraf.conf добавим тег influxdb_database

# Global tags can be specified here in key="value" format.
[global_tags]
  # dc = "us-east-1" # will tag all metrics with dc=us-east-1
  # rack = "1a"
  ## Environment variables can be used as tags, and throughout the config file
  # user = "$USER"
  influxdb_database = "default"

Да, я очень неоригинален в этом плане. Теперь ко всему, что собирает telegraf по умолчанию добавляется тэг influxdb_database со значением default.

Теперь добавляем сборку данных с mqtt. Заполняем файл /etc/telegraf/telegraf.d/mqtt.conf

[[inputs.mqtt_consumer]]
  servers = ["tcp://127.0.0.1:1883"]

  ## Topics that will be subscribed to.
  topics = [
    "/devices/+/controls/temperature",
    "/devices/+/controls/battery",
    "/devices/+/controls/humidity",
    "/devices/+/controls/pressure",
    "/devices/+/controls/co2",
    "/devices/+/controls/MCU Temperature",
    "/devices/+/controls/CPU Temperature",
    "/devices/dark_room_power/controls/voltage",
    "/devices/dark_room_power/controls/power",
    "/devices/sleeping_room_power2/controls/voltage",
    "/devices/sleeping_room_power2/controls/power",
  ]

    data_format = "value"
    data_type = "float"

[inputs.mqtt_consumer.tags]
    influxdb_database = "mqtt"

Тут тоже все просто: указываем адрес сервера, указываем, на какие топики подписаться, их формат и последней строчкой меняем значение тега на mqtt. И теперь самая магия:

[[outputs.influxdb_v2]]
urls = ["http://influxdb.hlevnoe.lan:8086"]
token = "xf3w6SKIPSKIP7z1hPQ=="
organization = "hlevnoe"
bucket = "hlevnoe"
[outputs.influxdb_v2.tagpass]
influxdb_database = ["default"]

И сравните с вторым

[[outputs.influxdb_v2]]
urls = ["http://influxdb.hlevnoe.lan:8086"]
token = "xf3w6nSKIPSKIPvam7z1hPQ=="
organization = "hlevnoe"
bucket = "mqtt"
[outputs.influxdb_v2.tagpass]
influxdb_database = ["mqtt"]

Наверняка для матерых инфлюксоводов это покажестя легкой шуткой, но я честно потратил пару часов на раскуривание функционала tagpass. Больше, правда, на выяснение, как и куда его правильно прописать. В общем, теперь все с influxdb_database = default попадает в бакет hlevnoe, а mqtt в mqtt.

Запустив все это и убедившись в отсутствии ошибок, можно вернуться в influx. Там есть простенький data explorer, позволяющий потыкаться и порисовать графики. Даже можно простейшие дашборды собрать!

Вот, к примеру, график потребления электричества у морозильника.

Или график температуры у меня за окном

В общем, это далеко не графана, но по-быстрому позырить что к чему – можно.

Готовим котёнка

Умная мысля приходит обычно слишком поздно. Вот и тут, я много лет страдал при переходе с одного компа на другой. В каждой операционке своя терминалка, со своими дрючками и глючками. Я ходил, настраивал и пытался их подогнать одну к другой.. Хорошо еще, что хоть чуть-чуть шорткаты устаканились, а то вообще боль была…

В общем, я решил сменить терминалку. Немного подумал и составил офигенно длинный лист требований:

  • Терминалка должна работать под Linux, MacOS и Windows (WSL)
  • Она должна работать одинаково. То есть один и тот же параметр в конфиге должен приводить к одним и тем же изменениям в поведении.
  • Терминалка должна уметь в truecolor.
  • Терминалка должна уметь в шрифтовые лигатуры.

Всё остальное, типа рендеринга на GPU или особо правильной поддержке многих систем, мне было совершенно не нужно. Главное, чтобы я взял ноутбук на другой операционке и получил абсолютно тот же… сейчас это модно называть экспириенсом. Внезапно самым сложным для терминалок оказался последний пункт.

Я не понимаю, чего сложного в подобной конвертации символов -> >> <> != ? Но, например, авторы gnome terminal не осилили подобного и отмазались, что не для всех языков это может работать. Ну сделали бы не для всех…

И, внезапно, всем моим хотелкам удовлетворила только одна терминалка – kitty. Если быть до конца честным, то нет, были и другие, но они не такие гламурные. Ставить котёнка можно 100500 способами, но я предпочел через пакетный менеджер.

После установки я некоторое время испытывал реальную боль. Дело в том, что автор этой терминалки – индус и очень своеобразно подошел к составлению документации. Кусочек тут, кусочек там, а потом можно и вот сюда глянуть… В общем, вот мой конфиг

font_family Fira Code
font_size 12
cursor_shape block
cursor_shape_unfocused hollow
shell_integration no-cursor
scrollback_lines 10000
sync_to_monitor yes
remember_window_size  yes
initial_window_width  800
initial_window_height 600
include theme.conf
tab_bar_style slant
update_check_interval 0
term xterm-256color
paste_actions quote-urls-at-prompt
background_opacity 1
confirm_os_window_close 0
map super+n new_os_window_with_cwd
map super+t new_tab_with_cwd
map super+w close_os_window
map super+c copy_to_clipboard
map super+v paste_from_clipboard

Последние строчки – это дань моей лени, чтобы новые окошки открывались как на макоси. Win+n и не надо пальцы ломать об Ctrl+Shift+n . Мелочь, а удобно. Ну и файлик theme.conf, честно спертый из интернета и имитирующий тему homebrew

# Theme ported from the Mac Terminal application.

background #000000
foreground #00ff00
cursor #23ff18
selection_background #083905
color0 #000000
color8 #666666
color1 #990000
color9 #e50000
color2 #00a600
color10 #00d900
color3 #999900
color11 #e5e500
color4 #0000b2
color12 #0000ff
color5 #b200b2
color13 #e500e5
color6 #00a6b2
color14 #00e5e5
color7 #bebebe
color15 #e5e5e5
selection_foreground #e5e5e5

И вот я уже почти неделю живу на двух операционках и с одним терминалом. Нормально …

Теперь операционко-специфичные штуки.

MacOS: надо в ENV переменные добавить KITTY_CONFIG_DIRECTORY=~/.config/kitty/ , иначе котенок хз где хранит конфиги. А так все в одинаковом месте. Чтобы сменить иконку, надо ее просто рядом с конфигом положить и обозвать kitty.app.png.

Linux: чтобы сменить иконку, надо сделать desktop файл, аналогичный нижеприведенному, и положить его в .local/share/applications/ . Главная боль – путь до иконки должен быть абсолютным.

[Desktop Entry]
Version=1.0
Type=Application
Name=kitty
GenericName=Terminal emulator
Comment=Fast, feature-rich, GPU based terminal
TryExec=kitty
StartupNotify=true
Exec=kitty
Icon=/home/kiltum/.config/kitty/kitty.app

Все 🙂

Всё своё ношу с собой. Локальное зеркало.

Пора, пора описывать то, что было сделано. А то как обычно, забудется и всё, пиши “прощай”.

Итак, давным-давно, когда ещё существовала страна под названием “Украина”, я начал делать свою… Ну даже не знаю, на лабораторию это не тянет, на свой датацентр тоже. В общем, несколько серверов, выполняющих нужные для меня задачи. Часть арендованные, часть свои… И всё было совсем хорошо, пока не началась СВО. Тут же вылезла куча говна и давай блочить доступ. Следом к ним присоеденились убегуны и давай портить до чего руки дотянутся. Дескать, они все такие чувствительные, что не могут позволить такое и вааще, они только за самое хорошее! Но тут не про них, а про то, как сделать себе хорошо и нагло игнорировать подобное сейчас и в будущем.

Итак, условия для этой задачи и последующих простые: я должен иметь возможность поставить или обновить нужное мне в любой момент времени. Есть интернет, нету – все равно. И первым шагом я сделаю зеркало для пакетов дистрибутива ubuntu.

Погуглив, я нашел несколько инструкций, как сделать свое зеркало. Немного потыкался в монстров типа Nexus и прочих JFrog и решил, что лучшее враг хорошего. Итак, для начала нам нужна машинка с более-менее приличным диском. Далее все просто и прямолинейно: ставим необходимое и подготавливаем структуру каталогов

apt install apache2 debmirror gnupg xz-utils rsync

mkdir -p /mirror
mkdir /mirror/debmirror
mkdir /mirror/debmirror/amd64
mkdir /mirror/debmirror/mirrorkeyring
mkdir /mirror/scripts

gpg --no-default-keyring --keyring /mirror/debmirror/mirrorkeyring/trustedkeys.gpg --import /usr/share/keyrings/ubuntu-archive-keyring.gpg

cd /var/www/html
ln -s /mirror/debmirror/amd64 ubuntu

cd /mirror/scripts

А вот теперь скрипт, который все друг у друга таскают. Я не исключение

# cat > debmirroramd64.sh 
#!/bin/bash

## Setting variables with explanations.

#
# Don't touch the user's keyring, have our own instead
#
export GNUPGHOME=/mirror/debmirror/mirrorkeyring

# Arch=         -a      # Architecture. For Ubuntu can be i386, powerpc or amd64.
# sparc, only starts in dapper, it is only the later models of sparc.
# For multiple  architecture, use ",". like "i386,amd64"

arch=amd64

# Minimum Ubuntu system requires main, restricted
# Section=      -s      # Section (One of the following - main/restricted/universe/multiverse).
# You can add extra file with $Section/debian-installer. ex: main/debian-installer,universe/debian-installer,multiverse/debian-installer,restricted/debian-installer
#
section=main,restricted,universe,multiverse

# Release=      -d      # Release of the system (, focal ), and the -updates and -security ( -backports can be added if desired)
# List of updated releases in: https://wiki.ubuntu.com/Releases
# List of sort codenames used: http://archive.ubuntu.com/ubuntu/dists/

release=noble,noble-security,noble-updates,noble-backports

# Server=       -h      # Server name, minus the protocol and the path at the end
# CHANGE "*" to equal the mirror you want to create your mirror from. au. in Australia  ca. in Canada.
# This can be found in your own /etc/apt/sources.list file, assuming you have Ubuntu installed.
#
server=ru.archive.ubuntu.com

# Dir=          -r      # Path from the main server, so http://my.web.server/$dir, Server dependant
#
inPath=/ubuntu

# Proto=        --method=       # Protocol to use for transfer (http, ftp, hftp, rsync)
# Choose one - http is most usual the service, and the service must be available on the server you point at.
# For some "rsync" may be faster.
proto=rsync

# Outpath=              # Directory to store the mirror in
# Make this a full path to where you want to mirror the material.
#
outPath=/mirror/debmirror/amd64

# By default bandwidth is not limited. Uncommend this variable and set it to the apropriate
# value in Kilobytes per second. Also don't forget to uncomment the --rsync-options line in the last section below.
bwlimit=1000

# The --nosource option only downloads debs and not deb-src's
# The --progress option shows files as they are downloaded
# --source \ in the place of --no-source \ if you want sources also.
# --nocleanup  Do not clean up the local mirror after mirroring is complete. Use this option to keep older repository
# Start script
#
debmirror       -a $arch \
                --no-source \
                -s $section \
                -h $server \
                -d $release \
                -r $inPath \
                --progress \
                --method=$proto \
                --rsync-options "-aIL --partial --bwlimit=$bwlimit" \
                $outPath

Скрипт настроен на российский миррор и лимит в 1 мегабайт в секунду, чтобы не забивать канал, как минимум при первоначальной скачке. Ну и релиз поменял на noble, ибо нынче уже 2024 год и в ходу у меня 24.04. Далее просто запускаем этот скрипт и идем заниматься своими делами. Оно медленно и печально высосет кучу гигабайт. Реально КУЧУ гигабайт. На момент написания размер зеркала был в районе двухсот гигов.

Ну а дальше просто везде разбрасываем новый ubuntu.list, заменяя им ubuntu.sources

deb http://mirror/ubuntu noble main restricted universe multiverse
deb http://mirror/ubuntu noble-security main restricted universe multiverse
deb http://mirror/ubuntu noble-updates main restricted universe multiverse

И точно так же миррорим что-то другое. К примеру, возжелал я поставить InfluxDB. Можно пакетики тащить, а можно зеркало сделать.

wget -q https://repos.influxdata.com/influxdata-archive_compat.key
gpg --no-default-keyring --keyring /mirror/debmirror/mirrorkeyring/trustedkeys.gpg --import influxdata-archive_compat.key
mkdir -p /mirror/debmirror/influx/
cat influxdata-archive_compat.key | gpg --dearmor > /mirror/debmirror/influx/key.gpg
cd /var/www/html
ln -s /mirror/debmirror/influx influx

И чуточку поправленный скрипт без комментариев. Изменил только откуда брать, куда ложить и то, что не надо использовать rsync. Адреса и прочее я нагло упер из инструкции с официальной страницы https://www.influxdata.com/downloads/, когда выбрал Ubuntu&Debian

# cat influx.sh 
#!/bin/bash

export GNUPGHOME=/mirror/debmirror/mirrorkeyring
arch=amd64
section=main
release=stable
server=repos.influxdata.com
inPath=/debian
proto=http
outPath=/mirror/debmirror/influx

debmirror       -a $arch \
                --no-source \
                -s $section \
                -h $server \
                -d $release \
                -r $inPath \
                --progress \
                --method=$proto \
                --rsync-extra=none \
                $outPath

Ну и потом на хосте, куда что-то от influx будет ставиться:

wget http://mirror.hlevnoe.lan/influx/key.gpg -O /usr/share/keyrings/influx.gpg

cat > /etc/apt/sources.list.d/influx.sources 
Types: deb
Architectures: amd64
Signed-By: /usr/share/keyrings/influx.gpg
URIs: http://mirror.hlevnoe.lan/influx
Suites: stable
Components: main

Дальше совершенно стандартно: apt update && apt install...

Умный дом. MQTT сервер

С чего начинается любой умный дом? Ну в смысле если он действительно претендует на право называться умным? Правильно, с места хранения состояния всяких устройств и датчиков.

Исторически сложилось, что для централизации подобного используется протокол MQTT. Он легкий, простой и поддерживается всеми участвующими в деле устройствами.

Вообще у меня уже есть один mqtt сервер, встроенный в wirenboad. Но есть два больших но: 1) сам по себе wirenboard очень дохлый (хотя народ радостно на него даже Home Assistant в докере водружает) и 2) у меня будет очень много устройств, общающихся по MQTT.

Поэтому решение простое: поднимаем на виртуалке MQTT сервер и заставляем всех с ним работать. А кто не умеет или умеет не кошерно – забирать с тех данные самим. Для реально больших нагрузок или отказоустойчивости народ ставит EMQX, но у меня такого не будет, поэтому использую mosquitto.

apt install mosquitto mosquitto-clients

Все настройки оставляю по умолчанию, просто добавляю один мост wirenboard->mosquitto.

# cat /etc/mosquitto/conf.d/wirenboard.con
connection wirenboard
address wirenboard:1883
bridge_insecure true
topic # in 0

Принцип простой: тупо подписываемся на все топики с сервера wirenboard. Паролей и логинов нет, всяких TLS тоже не наблюдается. Проверить работоспособность полученного решения тоже проще простого:

mosquitto_sub -t \#

Если все хорошо, то вы должны увидеть бегущую колонку цифр. Wirenboard сам по себе генерирует кучу трафика, так что никаких пауз не будет. А если у вас, как и у меня, уже подключены датчики, то трафик становится довольно большим для глаз.

Первая ачивка получена!

Как сбросить кеш только для одного файла

Дело было как-то вечером. Есть у меня дома контроллер wirenboard. Пока идет знакомство, наладка и отстройка, управляет светом около стола, да и меряет температуру по всему дому. Но не суть. наткнулся я на одну интересную багофичу: если пощелкать переключателями, а потом тут же обрубить питание, то эти самые переключатели после загрузки окажутся в рандомном состоянии.

Небольшое расследование со службой поддержки wirenboard показало, что дело в одном маленьком файлике. Вернее с тем, как linux с ним обращается. Верхнеуровневый софт честно в него пишет нужное, а вот линукс, желая сберечь ресурс emmc, начинает активно кешировать и сбрасывает файловый кеш тогда, когда в 99% уже поздно.

Казалось бы, фигня вопрос: засунь в крон sync и дело в шляпе. Ок, не в крон, это перебор, а после inotify и всё. Но сбрасывать всю FS ради одного файла… В общем, рецепт ниже.

# cat /etc/systemd/system/libwbmqtt-saver.service 
[Unit]
Description=Drop file cache for libwbmqtt.db if it changed

[Service]
ExecStart=/bin/bash -c 'inotifywait -qq -e modify /var/lib/wb-mqtt-gpio/libwbmqtt.db && dd of=/var/lib/wb-mqtt-gpio/libwbmqtt.db oflag=nocache conv=notrunc,fdatasync count=0 status=none'
Restart=always

[Install]
WantedBy=multi-user.target

Всё написано на скорую руку, но тем не менее, прекрасно работает. Вся магия в том, что dd открывает фаил на запись, записывает 0 байт и закрывает, сбрасывая кеш. Такую оптимизацию подсмотрел где-то в инете, мое первоначальное решение было гораздо страшнее (и нет, не покажу).

В общем, жужжит как надо.

Fedora 40 Wifi cannot connect to network

После какого-то обновления мой ноутбук стал плохо соединяться с Wifi. Ну как плохо… Регулярно стал терять сеть, ругаться на то, что не может соедениться и так далее и тому подобное. Так как у меня идет ремонт и я регулярно перетряхиваю сеть, то я грешил на точки доступа, линукс-сервер, выступающий роутером и на прочее, что могло повлиять.

Однако через некотрое время индеец зоркий глаз стал замечать, что на WiFi жалуется только ноутбук на Fedora. Windows, MacOS и андроиды и iOS цеплялись хорошо и никаких призывов не выдавали. И это бы продолжалось еще долго, но вчера линукс сказал “все, я больше не хочу соединяться с сетью”. Совсем.

Симптомы простые: ноутбук видит WiFi, соединяется, но через 45 секунд происходит тайм-аут DHCP и он отсоединяется. Установка статического адреса не помогает. Он перестает отваливаться, но пакеты никуда не ходят.

Проверка с помощью tcpdump показала, что в общем-то на это есть резоны: в канале было тихо. Причем настолько тихо, что даже запущенный с ноутбука ping не было видно. Тут я наконец-то понял, что проблемы именно на моей стороне.

Первым делом я решил, что проблема в сетевой. Воткнул USB, она со второго или третьего раза зацепилась. Тут бы мне насторожиться, но я пропустил этот момент. Начал играться с настройками WIFi на роутере. Ну так отключать/включать MIMO и прочее. Не помогло.

Включил точку доступа на телефоне. Ноутбук мгновенно прицепился и сообщил, что все в порядке.

То есть у ноутбука проблемы именно с моей точкой доступа. Стал разбираться. Ни каналы, ни смены настройки меш-сети – ничего не помогало. В веб-панели роутера я видел, что ноутбук соединяется и потом через некоторое время отваливается.

Так я сделал вывод, что железная часть работает. Дело в софте. И тут мне повезло запустить еще раз tcpdump как раз во время той самой паузы до таймаута. И внезапно я увидел бегающие туда-сюда пакетики, характерные для любой сети. Точно софт!

Стал разбираться. Оказалось, что добрые идиоты, пишущие Network Manager, где-то налажали в последних апдейтах и теперь мак-адрес меняется так, что он вводит сетевую в кому (или что-то еще в потрохах, но разбираться было лень). Ок, значит проблема простая: надо найти, где это выключается и дело в шляпе!

А вот тут возникла проблема. Оно нигде не выключается. Все найденные рецепты про wifi.cloned-mac-address и ethernet.cloned-mac-address не дали ровном счетом ничего. Повторяю: николай, иван, хартон, ульяна, яна. Так как дело было вечером, я решил не мучаться и вернуться на шаг назад. Говоря другими словами, вырубить нафиг NetworkManager.

Первым делом отвязываем wpa_supplicant от NM, хардкодим интерфейс и выключаем сам NM

# cat /usr/lib/systemd/system/wpa_supplicant.service
[Unit]
Description=WPA supplicant
Before=network.target
Wants=network.target
#After=dbus.service

[Service]
#Type=dbus
Type=oneshot
RemainAfterExit=yes
#BusName=fi.w1.wpa_supplicant1
EnvironmentFile=-/etc/sysconfig/wpa_supplicant
ExecStart=/usr/sbin/wpa_supplicant -B -i wlp0s20f3 -c /etc/wpa_supplicant/wpa_supplicant.conf
# -u $INTERFACES $DRIVERS $OTHER_ARGS
ExecStart=/usr/sbin/dhclient wlp0s20f3
ExecStop=/usr/bin/killall -q wpa_supplicant
ExecStop=/usr/bin/killall -q dhclient
[Install]
WantedBy=multi-user.target

# systemctl daemon-reload
# systemctl disable NetworkManager.service
# systemctl stop NetworkManager.service

Потом конфигурируем ручками wpa_supplicant

# cat /etc/wpa_supplicant/wpa_supplicant.conf 
ctrl_interface=/var/run/wpa_supplicant
ctrl_interface_group=wheel

network={
    ssid="multik"
    psk="VERY_SECRET_PASSWORD"
}

И рассказываем системе, что делать с сетью (ну и чтобы dhclient не ругался)

# cat /etc/sysconfig/network-scripts/ifcfg-wlp0s20f3 
DEVICE="wlp0s20f3"
ONBOOT=yes
NETBOOT=yes
IPV6INIT=yes
BOOTPROTO=dhcp
HWADDR="68:3e:26:b0:b1:93"
NAME="wlp0s20f3"
NM_CONTROLLED="no"

И самым последним шагом правим конфиг NSS, чтобы он не страдал херней, ибо всякие resolvd тоже ушли гулять вслед за NM

# cat /etc/nsswitch.conf 
....
#hosts:      files mdns4_minimal [NOTFOUND=return] resolve [!UNAVAIL=return] myhostname dns
hosts:      files myhostname dns
....

Все. Можно перезапустить сервисы, а можно отребутиться для проверки. Сеть есть, внезапно никто ни на что не жалуется и не отваливается.

Update: проверил на live образах. Все отлично коннектится и радуется жизни. То есть это именно обновления как-то и где-то что-то наковыряли

Привожу медиатеку к единому стилю

В предидущем посте я прошелся по библиотеке и расставил везде теги и обложки. Но теперь надо решить другую проблему: у меня есть mp3, m4a и flac файлы. Причем я точно знаю, что некоторые flac и m4a получены из mp3. Да, источники музыки были не всегда адекватными …

Поэтому шаг номер раз: приводим всю библиотеку в mp3 формат. То, что из m4a и flac перезапишутся mp3 – пофиг

find . -type f -name "*.m4a" | parallel 'ffmpeg -y -i {}  -map_metadata 0 -codec:a libmp3lame -qscale:a 1 {.}.mp3'

find . -type f -name "*.flac" | parallel 'ffmpeg -y -i {}  -map_metadata 0 -codec:a libmp3lame -qscale:a 1 {.}.mp3'

Теперь можно грохнуть “оригиналы”. И совсем маленький шаг: необходимо избавиться от дубликатов. Так как источников было много, то и для некоторых популярных песен было аж до 5 вариантов. Пишу маленький скрипт, который удаляет все дубликаты меньшего размера (да, буду считать, что бОльший размер означает лучшее качество).

#!/bin/bash

IFS=$(echo -en "\n\b")

function traverse() { 
    # lets find copies in directory and i dont want use .m?? in search
    for file in $(ls -1 ${1} |grep -e \(1\).mp3 -e \(2\).mp3 -e \(3\).mp3 -e \(4\).mp3 -e \(5\).mp3) 
    do
        l="${1}/${file%????????}*" # cut (X).mp3 from file name
        # delete all files except first and biggest
        ls -1S ${l} | tail -n +2 | xargs -d '\n' rm -f
        # Now rename file. Leave mv alone for errors "the file is same" - its working indicator for me :)
        ffin="${l%?}.mp3"
        mv  $l $ffin        
    done
    # now lets find another directory to deep in
    for file in $(ls "$1")
    do
        if [[ -d ${1}/${file} ]]; then
            traverse "${1}/${file}"
        fi
    done
}

function main() {
    traverse "$1"
}

main "$1"

И натравливаем его на библиотеку. Я специально не стал обрамлять вокруг mv, что бы хоть что-то выводило во время работы, поэтому сообщения типа mv: '/mnt/swamp/Reloaded//松居慶子/Dream Walk/04 Fire in the Desert.mp3' and '/mnt/swamp/Reloaded//松居慶子/Dream Walk/04 Fire in the Desert.mp3' are the same file стали для меня индикатором, что скрипт работает.

В результате у меня теперь есть библиотека музыки, где все разложено по полочкам (иногда полупустым), протегано и с обложками. Но (ох уж это но) есть куча песен с битрейтом 96 и 128 кбит/с. Но как к этому подобраться автоматически – я пока не представляю.

Навожу порядок в музыке

Довольно долго я таскаю с собой архив музыки. Первые треки в нем датируются аж 1995 годом. Естественно, что за все это время внутри образовалась каша. Я много раз пытался подойти к этой свалке, что бы разгрести ее содержимое и разложить все по полочкам. Однако регулярно обламывался из-за банальной лени: все-таки раскидать почти 100 тысяч файлов нужно адское терпение.

Ситуацию усложняло то, что куча файлов имело очень говорящие названия типа 3.mp3. Естественно, каких-либо тегов тоже ожидать не приходилось. Ну вот просто как-то понравилась мне песенка, я ее и забросил в архив …

Наконец в очередной раз я подошел к этой свалке. Тыкался, тыкался, смотрел на всякие редакторы тегов и горестно вздыхал. Пока не решил погуглить, как можно для всего этого поиспользовать всякие шазамы и прочие новомодные (почти) штучки. И вуаля!

https://picard.musicbrainz.org/

Оставлю эту ссылку отдельной строкой. Жрет mp3, ищет по аудиотпечатку что же это за трек и заполняет теги, добавляет обложку и раскладывает получившееся красиво. Я в совершеннейшем восторге. На тестовой выборке в 7 тыщ файлов не нашла соответствие только для 3. Но их никто не знает, ни яндекс, ни яблоки. Мелочь …

Обходим DNS hi-jack

Не так давно некоторые (я не буду показывать пальцем) провайдеры начали заниматься херней. А именно блокировать или подменять DNS ответы. И ладно бы блочили какой-нибудь абстрактный facebook.com, так они ломают к примеру резолвинг обратных записей, что например для почтовых серверов больно и печально. Если обратиться к техподдержку, то там начинают лепетать про защиту от угроз и предлагают пользоваться серверами провайдера, ведь они защищены и вообще лучше всех.

В общем, на данный момент это все лечится очень просто. Берем любой клиент DNS over HTTPS (DoH), ставим его и направляем весь днс трафик через него. Для ubuntu рецепт следующий:

apt install dnscrypt-proxy
systemctl enable dnscrypt-proxy
systemctl start dnscrypt-proxy

В результате на 127.0.2.1:53 появляется DNS сервер, который кладет всевозможные причиндалы на провайдерские потуги. Ну а дальше во всех клиентах просто указываем этот сервер. Лично я просто добавил одну строчку на главном домашнем кеширующем bind

cat /etc/bind/named.conf.options | grep fo
        forwarders { 127.0.2.1 ; };

Ну и попутно подобным шагом вы провайдеру статистику “куда вы ходите” поломаете. Ибо “кто с чем к нам, тот тем и получит”

PS Да, это все лечится VPN, но кто даст гарантию, что и провайдер VPN не займется тем же самым?

Знай свой cgroup

Давеча столкнулся с непонятным (для меня до сегодня) поведением cgroup. Изначально описание проблемы было очень информативным “сервер тормозит”.

Захожу я на сервер и вижу картину маслом:

Куча свободной памяти, но сервер сидит в свопе и выбираться оттуда категорически не желает. Я последовательно начал перебирать все известные мне лимиты и ограничения: везде норм, хорошо и ничего не вызывает подозрений.

Так как эта нода кубернетеса, то я посмотрел и на ограничения подов в /sys/fs/cgroup/memory/. Тоже все согласно описанному, везде memory.limit_in_bytes соответствуют нужному.

Затем я скопипастил микроскрипт что бы посмотреть, кто занял своп

SUM=0
OVERALL=0
for DIR in `find /proc/ -maxdepth 1 -type d -regex "^/proc/[0-9]+"`
do
    PID=`echo $DIR | cut -d / -f 3`
    PROGNAME=`ps -p $PID -o comm --no-headers`
    for SWAP in `grep VmSwap $DIR/status 2>/dev/null | awk '{ print $2 }'`
    do
        let SUM=$SUM+$SWAP
    done
    if (( $SUM > 0 )); then
        echo "PID=$PID swapped $SUM KB ($PROGNAME)"
    fi
    let OVERALL=$OVERALL+$SUM
    SUM=0
done
echo "Overall swap used: $OVERALL KB"

Но скрипт выдал совершенно не совпадающие с системными утилитами значение. Согласно его выводу, своп использовался на 6 гигов. А я вижу на скриншоте выше – 12. Проверил выборочно значения из /proc – совпадают с высчитанными …

Ок, проверю вообще работу подсистему памяти. Набросал быстренько микропрограммку на С, которая раз в секунду сжирала гиг памяти. top честно показал сначала исчерпание free, потом окончание свопа. После пришел OOM и убил программку. Всё правильно, всё так и должно быть.

Долго я ломал голову и пробовал разные варианты. Пока в процессе очередного созерцания top внезапно глаз не зацепился за главного пожирателя памяти. Вернее за его показатель VIRT в 32 гига памяти. Так-то я смотрел на %MEM и RES. В общем, появился резонный вопрос “какого?”

Забрезжила идея, что что-то не так с cgroup. Ок, делаю группу с лимитом памяти в 10 гигов, проверяю, что memory.limit_in_bytes стоят, запускаю снова программку-пожиратель памяти … и вуаля! Через 10 секунд сожралось ровно 10 гигов RAM, и начал жраться своп. Вопрос “какого?” стал более актуальным

Начал гуглить. https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt говорит скромно

memory.memsw.usage_in_bytes # show current usage for memory+Swap (See 5.5 for details)

memory.limit_in_bytes # set/show limit of memory usage

memory.memsw.limit_in_bytes # set/show limit of memory+Swap usage

Про memsw я специально добавил. Но на этой машине Ubuntu 20.04 с cgroup V2 и параметра с memsw нет. Нахожу дальнейшим гуглежом https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html

The main argument for a combined memory+swap facility in the original cgroup design was that global or parental pressure would always be able to swap all anonymous memory of a child group, regardless of the child’s own (possibly untrusted) configuration. However, untrusted groups can sabotage swapping by other means – such as referencing its anonymous memory in a tight loop – and an admin can not assume full swappability when overcommitting untrusted jobs.

Особенно понравились слова про саботаж. То есть, докер ограничивал использование только RAM, но не SWAP. Теперь, когда проблема стала понятной, стало понятно, что и надо гуглить.

https://docs.docker.com/engine/install/linux-postinstall/#your-kernel-does-not-support-cgroup-swap-limit-capabilities

Грубо говоря, надо добавить в конфиг grub

GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"

Но тут нода “боевая”, надо дать доработать сервису, поэтому просто увеличили лимиты для пода.

Как говорится, все “побежало и поскакало”

PS Про то, что на ноде с k8s не должно быть свопа я в курсе. Как и то, что начиная с версии 1.21 он поддерживается.

Свой gitlab на centos 7

Получив очередной счет от gitlab и github одновременно, я как-то задумался: а нафига я им плачу, когда я все это могу поднять на своем сервере? “Все это” – это кучку приватных git и простую ci/cd систему. Поставил и решил написать напоминалку, что бы в следующий раз не гуглить.

Процесс установки не вильно отличается от описанного на сайте, но есть несколько НО:

Во-первых, в centos7 nginx идет без поддержки passenger. Поэтому обновляем на версию из “пассажирской” репы.

curl --fail -sSLo /etc/yum.repos.d/passenger.repo https://oss-binaries.phusionpassenger.com/yum/definitions/el-passenger.repo
yum-config-manager --enable cr
yum install -y passenger
rpm -e nginx-mod-http-perl --nodeps
yum update nginx

Во-вторых, нигде в мануале не указано, что gitlab требует nodejs для работы

yum install nodejs

И наконец, нигде не указано, что gitlab не работает без unicorn. Во всех мануалах написано, что если у вас внешний nginx, отрубите встроенный и unicorn. Так вот, этого делать нельзя, иначе получите неработающий gitlab-workhouse

Из других неочевидных тюнингов стал вынос порта unicorn с 8080 и изменение размера буферов у постгреса. Иначе на моем загруженном сервере он отказывался запускаться.

unicorn['port'] = 8088
postgresql['shared_buffers'] = "100MB"

В остальном единственной засадой было изменение прав на сокеты, но это только из-за моей конфигурации, где куча всяких пользователей лезут в в один каталог. Так как сервер “домашний”, проще стало дать всем права на запись.

Больше никаких отступлений от официального руководства.

Новый датацентр. Сертификаты в почту

Для своих нужд я обычно ставлю Zimbra Open Source Edition. Простой, дуракоустойчивый почтовый сервер со всякими необходимыми плюшками. Но есть в нем одна маленькая проблема: по умолчанию он генерит самоподписанные сертификаты, на которые ругаются всякие почтовые клиенты. Значит, надо подсунуть сертификаты от letsencrypt

Сделаем на сервере каталог для сертификатов

mkdir -p /opt/zimbra/ssl/letsencrypt/
chown zimbra:zimbra /opt/zimbra/ssl/letsencrypt/

Опять же, маленький скриптик, который пускается руками после обновления сертификатов и проверки готовности всего ко всему. Он же zimbra_check

cat /opt/www/chain.pem >> /etc/letsencrypt/live/mail.ka12.co/fullchain.pem 
scp  /etc/letsencrypt/live/mail.ka12.co/* root@mail:/opt/zimbra/ssl/letsencrypt/
ssh root@mail "chown zimbra:zimbra /opt/zimbra/ssl/letsencrypt/*"
ssh root@mail "runuser -u zimbra /opt/zimbra/bin/zmcertmgr verifycrt comm /opt/zimbra/ssl/letsencrypt/privkey.pem /opt/zimbra/ssl/letsencrypt/cert.pem /opt/zimbra/ssl/letsencrypt/fullchain.pem"

Логика понятна из текста: добавляем CA ключ (отсюда) в цепочку, копируем все это в потроха зимбры и проверяем, согласится ли она это съесть. Обычно ответ “да”, но иногда она взбрыкивает и надо смотреть, чего же ей не нравится. Если ошибок нет, то можно запустить zimbra_deploy

#!/bin/bash
ssh root@mail "runuser -l zimbra -c 'zmcontrol stop'"
ssh root@mail "cp -a /opt/zimbra/ssl/zimbra /opt/zimbra/ssl/zimbra.`date \"+%Y%m%d\"`"
ssh root@mail "runuser -u zimbra cp /opt/zimbra/ssl/letsencrypt/privkey.pem /opt/zimbra/ssl/zimbra/commercial/commercial.key"
ssh root@mail "runuser -u zimbra /opt/zimbra/bin/zmcertmgr deploycrt comm /opt/zimbra/ssl/letsencrypt/cert.pem /opt/zimbra/ssl/letsencrypt/fullchain.pem"
ssh root@mail "runuser -l zimbra -c '/opt/zimbra/bin/zmcertmgr deploycrt comm /opt/zimbra/ssl/letsencrypt/cert.pem /opt/zimbra/ssl/letsencrypt/fullchain.pem'"
ssh root@mail "runuser -l zimbra -c 'zmcontrol start'"

Он тормозит нафиг всю зимбру, бекапит и деплоит сертификаты и снова запускает все назад.