🏠: linux

Systemd для непривилегированного пользователя

Уже год я работаю девопсом в банке. Это первая работа за много лет, где у меня нет полных прав в системе, то есть, делать что-либо под учёткой root и пользоваться sudo я не могу. Одновременно с этим на серверах могут быть дополнительные ограничения, например, может не быть доступа к планировщику cron, даже к личному через crontab -e. Также, в число моих обязанностей входит развёртывание собранных приложений (в основном это запуск файлов .jar), соответственно, также под непривилегированными учётками. Часто люди пытаются запускать такие приложения в фоне прямо в консольной сессии:

nohup "java -jar be-app-project-${VERSION}.jar --server.port=9090 &"

А потом, чтобы остановить приложение, приходится искать его идентификатор процесса (PID), используя порой вот такие громоздкие конструкции (видел в одном пайплайне развёртывания):

ps -ef | grep be-app-project-*.jar | grep -v grep | awk '$1=="project" {print $2}' | xargs kill

Можно, конечно, для отлавливания PID пользоваться pgrep, что несколько улучшит вид вышеприведённой строки, но общей кривизны решения это не меняет, к тому же, если пытаться такие практики применять в системах CI/CD типа Gitlab или Teamcity, то раннер может не запустить фоновый процесс и будет бесконечно висеть или выдаст ошибку.

Решением всех этих проблем является systemd, сервисы, таймеры и прочие сущности (юниты) которого будут работать в пространстве пользователя, т. е., для управления ими не требуется никакого повышения привилегий. Более того, отпадёт необходимость ухищряться в фоновом запуске приложения из консольной сессии и последующего отлавливания PID. Да и в целом грех не использовать systemd — это современная, гибкая и удобная система, основными препятствиями к освоению которой являются косность мышления, предубеждение против новых решений и нежелание учиться новому. С этим надо бороться по мере сил. С запуском непривилегированных юнитов есть один небольшой нюанс, о нём ниже.

Начнём с замены крона, опишу самый минимум. Предположим, каждые 3 минуты нужно запускать скрипт, который будет добавлять текущее время в текстовый файл. Создаём сам скрипт, например, по пути /home/user/scripts/add-time.sh, в котором обязательно нужно указать шебанг, чтобы скрипт гарантированно ассоциировался с bash.

#!/bin/bash
date +'%F %T' >> ~/add-time.txt

Затем сделать его исполняемым.

user@k3:~$ chmod u+x /home/user/scripts/add-time.sh

Теперь приступим к созданию службы, или сервиса, или демона. Все команды systemctl нужно будет запускать с параметром --user, что указывает на работу не c системными, а с пользовательскими юнитами. Следующая команда создаёт сервис add-time.service в каталоге ~/.config/systemd/user, где будут храниться все сервисы, таймеры и прочие юниты пользователя. Если этого каталога нет, он будет создан автоматически. Эту же команду можно использовать и для редактирования уже существующего юнита.

user@k3:~$ systemctl edit add-time --user --full --force

# Вставить в открывшийся редактор следующий текст и выйти с сохранением.
# В ExecStart= указывается абсолютный путь к скрипту. Скрипт должен быть исполняемым, иначе не заработает.
[Service]
ExecStart=/home/user/scripts/add-time.sh

Удобство команды systemctl edit в том, что после неё не нужно перечитывать список служб командой systemctl --user daemon-reload. Теперь создаём таймер, запускающий одноимённую службу с заданной периодичностью.

user@k3:~$ systemctl edit add-time.timer --user --full --force

# Вставить в открывшийся редактор следующий текст и выйти с сохранением.
[Timer]
OnCalendar=*:0/3

[Install]
WantedBy=timers.target

Таймер в systemd исключительно гибкий, он более функционален, чем cron. Например, можно запускать пропущенные задания, если сервер в нужный момент не работал, запускать задания через определённое время после запуска системы или в зависимости от активности/неактивности сервиса, настраивать точность срабатывания таймера для распределения нагрузки, когда в одно и то же время задание начинается на многих серверах и т. д. Секция [Install] здесь нужна для того, чтобы таймер можно было прописать в автозагрузку, т. е. он продолжит срабатывать после перезапуска сервера.

Теперь запустим таймер, пропишем его в автозагрузку и проверим его состояние. Видно, что таймер запускает сервис add-time.service и до его запуска осталась 1 мин 48 сек.

# Запуск
user@k3:~$ systemctl --user start add-time.timer
# Автозагрузка
user@k3:~$ systemctl --user enable add-time.timer
Created symlink /home/user/.config/systemd/user/timers.target.wants/add-time.timer → /home/user/.config/systemd/user/add-time.timer.
# Статус
user@k3:~$ systemctl --user status add-time.timer
● add-time.timer
     Loaded: loaded (/home/user/.config/systemd/user/add-time.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Wed 2025-04-16 15:46:08 MSK; 3s ago
    Trigger: Wed 2025-04-16 15:48:00 MSK; 1min 48s left
   Triggers: ● add-time.service

Apr 16 15:46:08 k3 systemd[952]: Started add-time.timer.

Через некоторое время видно результат в текстовом файле:

user@k3:~$ cat add-time.txt
2025-04-16 15:48:43
2025-04-16 15:51:43
2025-04-16 15:54:43
2025-04-16 15:57:43
2025-04-16 16:00:43
2025-04-16 16:03:43

Теперь про тот небольшой нюанс запуска пользовательских непривилегированных сервисов systemd, о котором я упоминал. Дело в том, что такие сервисы работают только тогда, когда пользователь работает в системе. Если он не зашёл, его сервисы не работают. К счастью, можно включить возможность выполнения сервисов пользователя вне зависимости от его логина. Сделать это нужно один раз, но команда включения при повторном применении ошибок не даёт, поэтому её можно спокойно использовать без предварительной проверки, например, в плейбуках Ansible.

# "Linger: no" - службы работать без входа в систему не будут
user@k3:~$ loginctl user-status
user (1000)
           Since: Sat 2025-04-12 12:24:26 MSK; 4 days ago
           State: active
        Sessions: *2
          Linger: no
    [...]

# Включить для текущего пользователя
user@k3:~$ loginctl enable-linger $USER

# Теперь порядок
user@k3:~$ loginctl user-status
user (1000)
           Since: Sat 2025-04-12 12:24:26 MSK; 4 days ago
           State: active
        Sessions: *2
          Linger: yes
    [...]

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

[Unit]
Description=Project App Backend {{ backend_maven_version }}

[Service]
Restart=always
Environment=REPORT_PATH={{ backend_report_directory }}
ExecStart=/usr/bin/java -Xrs -jar {{ backend_lib_directory }}/be-app-project-{{ backend_maven_version }}.jar

[Install]
WantedBy=multi-user.target

Шаги копирования шаблона и запуска службы в плейбуке.

- name: "Copy unit file"
  template:
    src: "{{ service_name }}.service.j2"
    dest: "~/.config/systemd/user/{{ service_name }}.service"

- name: "Start service"
  systemd:
    scope: user
    daemon_reload: true
    name: "{{ service_name }}"
    state: started
    enabled: true

Теперь можно просто и удобно работать со службами systemd (которые, к слову, могут и перезапускаться сами в случае сбоя, быть зависимыми друг от друга и много чего ещё) и не вспоминать про ловлю процессов и висящие в фоне терминалы, и всё это безо всяких привилегий root.

Новый домашний сервер

4-го августа я переехал со старого неттопа на новый GMKtec NucBox G3. Внутри — процессор Intel N100 Alder Lake 12-го поколения, 32 ГБ памяти Kingston PC4-25600 DDR4, 1 ТБ SSD Samsung 980 Pro и Wi-fi Realtek RTL8852BE (802.11ax), также имеется дополнительный порт для SSD-накопителя формата M.2 2242 (SATA). Я после установки системы Ubuntu 24.04 стал использовать wi-fi на время настройки, планируя потом подключиться к роутеру по проводу, да так на нём и остался — удобно, работает стабильно и скорость хорошая.

Внешний вид сервера Внешний вид сервера

Из минусов — мелкий вентилятор в нижней части корпуса, но пока минус этот скорее теоретический, потому что сейчас шума от него не слышно вообще после следующих настроек в BIOS, которые я подсмотрел на Reddit:

  1. Выставить TDP на 8 Вт (с 10 Вт), макс. потребление будет 19 Вт (с 24 Вт). В покое 8-9 Вт, выключенный — 1,2 Вт. Power -> Power limit select: 8W
  2. Включить Cstates (по умолчанию выключено). Advance -> CPU - Power management control -> C States: enabled
  3. Выключить турборежим процессора при загрузке. Advance -> CPU - Power management control -> Boot performance mode: Max Non-Turbo performance
  4. Повысить порог срабатывания вентилятора. Advance -> Hardware monitor -> Smart fan function -> Fan off: 40, Fan start: 65

Почему я решил поменять железо? Началось с того, что какой-то малолетний кретин бил мне по входной двери ногой и убегал. Чтобы выяснить, кто это делает, я организовал видеонаблюдение через глазок, на который установил альтернативный веб-сервер, позволивший мне получить с глазка потоки RTSP, которые шли на сервер Frigate NVR, поднятый всё в том же Докере. Для установки глазка пришлось немного рассверлить в двери дырку под него, купив очень красивое ступенчатое сверло.

Frigate Frigate

У Frigate есть возможность использовать различные варианты аппаратного видеоускорения и моделей обнаружения объектов, но на старом неттопе была доступна только чисто процессорная обработка. Это работало, но загрузка была довольно приличная, да и в целом эта конфигурация уже устарела — ей восемь лет, последние три из которых она работала круглосуточно. Так что в конце 2023 года я купил новый неттоп, который провалялся без дела до конца июля, когда, наконец, у меня дошли руки перевезти все сервисы со старого.

Сервисы на сегодняший день такие:

  • Реверс-прокси Træfik — обновил c версии 2.6 на 3.1, немного изменился синтаксис ярлыков, теперь там используются регулярные выражения вместо перечисления нескольких суффиксов или имён хоста, а ещё HTTP/3 сменил экспериментальный статус на стабильный.
  • Вышеупомянутый Frigate, у которого я включил видеоускорение VAAPI и модель OpenVino. Позже, наверное, надо бы попробовать добавить к нему Home Assistant как управляющую оболочку.
  • Блог на прекрасном движке Datenstrom Yellow
  • Вики на DokuWiki
  • Файловый сервис Nextcloud
  • Фотосервис Photoprism
  • Генеалогическое древо Webtrees
  • Страничка мониторинга (node_exporter, docker metrics, glances, smartctl_exporter, Prometheus, Grafana)

С мониторингом была сложная история. Я использовал PhpSysInfo в контейнере, и мне хотелось избавиться от довольно кривого способа сбора информации раз в полчаса на самом хосте и подкладывания файлов в веб-каталог приложения. Оказалось, что PhpSysInfo умеет работать через SSH, поэтому я создал на хосте выделенного пользователя и разрешил ему выполнять команды sensors и docker stats через sudo без запроса пароля.

sudo visudo /etc/sudoers.d/phpsysinfo

Cmnd_Alias PHPSYSINFO=/usr/bin/docker stats*, /usr/sbin/smartctl
phpsysinfo ALL = NOPASSWD: PHPSYSINFO

Пришлось открыть пару тем в репозитории Гитхаба из-за того, что какие-то моменты были непонятны, а какие-то не работали, но автор приложения, как и раньше, невероятно отзывчив и решает проблемы просто молниеносно, так что всё более-менее работало и прочитанные/записанные данные диска теперь показывались в понятных гигабайтах, а не в секторах, как раньше. Тем не менее, теперь вывод страницы после захода на неё стал очень тормозной — PhpSysInfo каждый раз лезет на хост по SSH и собирает данные (на полях отмечу утилиту sshpass, позволяющую автоматически подставлять пароль для входа). Вдобавок, перестало нормально работать отображение скопившихся системных обновлений, так что я начал искать альтернативу.

Glances Glances

Через некоторое время я нашёл Glances, у которого мне очень понравился внешний вид, но проект пока сыроват и документация у него достаточно фрагментарная, поэтому я решил вернуться к давней идее, на которую у старого неттопа не хватало ресурсов — Prometheus + Grafana. В конце концов, популярная вещь и по работе пригодится. Провозившись несколько дней, вспоминая, как там всё устроено, я нарисовал страницу более-менее в соответствии с моими предпочтениями. Что до производительности, то неттоп легко справляется с новым мониторингом. Glances я оставил как сборщик данных о контейнерах: он умеет экспортировать метрики для Prometheus.

Grafana Grafana

Все контейнеры с БД я обновил до MariaDB 11.4 с версии 10.6, завелось без проблем. PHP был обновлён на версию 8.3 ещё на старом неттопе. Docker volumes переделал просто в mount points — мне кажется, мигрировать данные и делать резервные копии так удобнее, каких-то преимуществ docker named volumes в моём случае я не вижу.

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

Пока мне нравится, поживём — увидим.

Настройка пульта для встроенного ИК-датчика Orange Pi

Дано: Orange Pi PC Plus с установленным LibreELEC 11, которым хотелось бы управлять с помощью пульта. Инфракрасный приёмник у Orange Pi имеется, поэтому нужно просто научить систему понимать с этого пульта сигналы.

Прежде всего нужно купить какой-нибудь пульт или взять уже имеющийся, у которого есть кнопки стрелок, старт/пауза, стоп, ОК и тому подобные, чтобы можно было нормально управлять медиацентром. Я купил пульт для телевизора Supra RS41-MOUSE за 200 рублей.

Заходим по SSH в систему. В инструкции по настройке пультов сначала рекомендуют подбирать совместимые конфигурации из списка по пути /usr/lib/udev/rc_keymaps, но их там полторы сотни и нет ничего похожего по названию, так что я не стал возиться, а сразу перешёл к созданию собственной конфигурации, описанному в разделе Advanced.

Выводим список поддерживаемых протоколов управления (supported kernel protocols):

OrangePiPCPlus:~ # ir-keytable
Found /sys/class/rc/rc0/ with:
        Name: sunxi-ir
        Driver: sunxi-ir
        Default keymap: rc-empty
        Input device: /dev/input/event0
        LIRC device: /dev/lirc0
        Attached BPF protocols:
        Supported kernel protocols: lirc rc-5 rc-5-sz jvc sony nec sanyo mce_kbd rc-6 sharp xmp imon rc-mm
        Enabled kernel protocols: lirc
        bus: 25, vendor/product: 0001:0001, version: 0x0100
        Repeat delay = 500 ms, repeat period = 125 ms

Нужно подобрать протокол, с которым совместим пульт. В моём случае подошёл nec, после включения которого в консоли начали отображаться коды кнопок при их нажатии на пульте:

OrangePiPCPlus:~ # ir-keytable -p nec -t
Protocols changed to nec
Testing events. Please, press CTRL-C to abort.
1018.929441: lirc protocol(necx): scancode = 0x710205
1018.984459: lirc protocol(necx): scancode = 0x710205 repeat
1021.562363: lirc protocol(necx): scancode = 0x710205
1024.293455: lirc protocol(necx): scancode = 0x710204
1024.348472: lirc protocol(necx): scancode = 0x710204 repeat
1028.355072: lirc protocol(necx): scancode = 0x710268
1028.410087: lirc protocol(necx): scancode = 0x710268 repeat
1028.517783: lirc protocol(necx): scancode = 0x710268 repeat
1030.043425: lirc protocol(necx): scancode = 0x710262
1030.098426: lirc protocol(necx): scancode = 0x710262 repeat

Отлично, теперь нужно нарисовать карту кнопок (keymap), где прописывается протокол пульта и соответствие кодов кнопок с их функциями. Список функций можно посмотреть с помощью команды irrecord -l | grep ^KEY или в секции <remote device="devinput"> файла /usr/share/kodi/system/Lircmap.xml.

В итоге у меня получилась конфигурация, приведённая ниже. Закомментированы кнопки, к которым я не нашёл близкой функции, а кнопка POWER была чуть позже изменена на ENTER, потому что одноплатник выключался, но включить его потом с пульта было нельзя.

# table supra_rs41, type: nec
# 0x710202 KEY_POWER
0x710202 KEY_ENTER
# 0x71020f source
0x710220 KEY_RED
0x710234 KEY_GREEN
0x71022b KEY_YELLOW
0x71022c KEY_BLUE
0x710227 KEY_MUTE
0x710225 KEY_ZOOM
# 0x710200 freeze
0x710228 KEY_TEXT
0x710203 KEY_FAVORITES
0x710232 KEY_SUBTITLE
0x710240 KEY_AUDIO
0x710255 KEY_RECORD
0x710226 KEY_REWIND
0x71021e KEY_FORWARD
0x710239 KEY_PREVIOUS
0x710213 KEY_NEXT
0x71021a KEY_PLAY
0x710201 KEY_STOP
0x710260 KEY_UP
0x710261 KEY_DOWN
0x710265 KEY_LEFT
0x710262 KEY_RIGHT
0x710268 KEY_ENTER
0x71022d KEY_MENU
0x71021f KEY_ESC
0x710207 KEY_VOLUMEUP
0x71020b KEY_VOLUMEDOWN
0x710222 KEY_HOME
# 0x710221 mouse
0x710212 KEY_CHANNELUP
0x710210 KEY_CHANNELDOWN
0x710204 KEY_1
0x710205 KEY_2
0x710206 KEY_3
0x710208 KEY_4
0x710209 KEY_5
0x71020a KEY_6
0x71020c KEY_7
0x71020d KEY_8
0x71020e KEY_9
0x710211 KEY_0
0x710223 KEY_DISPLAYTOGGLE
# 0x710250 return

Теперь надо создать файл с нашей картой кнопок и запустить его:

OrangePiPCPlus:~ # nano /storage/.config/rc_keymaps/supra_rs41
OrangePiPCPlus:~ # ir-keytable -c -w /storage/.config/rc_keymaps/supra_rs41
Read supra_rs41 table
Old keytable cleared
Wrote 39 keycode(s) to driver
Protocols changed to nec

Пульт сразу же начинает работать. Осталось добавить эту конфигурацию в автозагрузку:

OrangePiPCPlus:~ # echo "* * supra_rs41" > /storage/.config/rc_maps.cfg

Компьютер грузится только с подключенным монитором

Мой сайт уже без малого 2 года крутится на неттопе с установленной Ubuntu 20.04 LTS. С некоторых пор неттоп начал испытывать проблемы при загрузке: например, ставишь обновления, перезагружаешь систему — и она уже не возвращается. Подключаешь монитор, чтобы посмотреть, в чём дело — и система загружается нормально. Какой-то бред, как будто железка издевается над тобой.

Я проверял настройки BIOS несколько раз, проверял блок питания на предмет нехватки мощности (хотя как в этом случае помогло бы подключение монитора?) — никаких проблем не обнаружил. Думал даже о покупке нового неттопа. Сегодня, когда в очередной раз, используя непарламентские выражения, я подключал монитор, чтобы загрузить железку после очередного невозврата из перезагрузки, решил поискать в интернете, мало ли, у кого-то такая же дурацкая проблема?

И нашёл! Оказалось, дело в настройках загрузчика GRUB. Нужно выключить у него графический режим и потом применить настройку:

sudo sed -i '/GRUB_TERMINAL=/c GRUB_TERMINAL=console' /etc/default/grub
sudo update-grub

Теперь перезагружается без проблем. И покупать ничего не потребовалось.

Проброс USB по сети

Берём «железный» linux-сервер, например, Ubuntu 22.04 LTS, на котором есть порты USB, вставляем туда ключ HASP, широко использующийся для аппаратной защиты программ типа 1С или цифровой подписи. Проверяем, определился ли он в системе:

Определился, прекрасно. Теперь ставим usbip.

# Установка (usbip входит в стандартный набор утилит)
apt install linux-tools-common linux-tools-generic
# Вкл модуль ядра для сервера:
modprobe usbip-host
echo "usbip-host" >> /etc/modules
# Вкл службу
usbipd -D
# Проверить версию
usbip version # usbip (usbip-utils 2.0)

Выводим список локальных устройств usbip и предоставляем нужное устройство в общий доступ.

Теперь нужно настраивать клиента, в данном случае на Windows 11, работающей в виртуальной среде Hyper-V, где, как известно, USB с хоста пробросить в виртуалку нельзя (и это правильно). Предварительно на клиентской машине необходимо установить драйвер usbip, а для этого импортировать сертификат, идущий в комплекте, и отключать цифровую подпись драйверов. Это можно сделать из меню восстановления при загрузке, так как команда bcdedit /set TESTSIGNING ON в современных системах Windows уже не действует.

Итак, драйвер установлен, теперь можно посмотреть, какие устройства доступны на удалённом сервере, и подключить нужное.

Работает! Наш ключ определился как три устройства Sentinel (после установки драйверов HASP). Ниже виден ранее установленный usbip.

Чтобы отключить устройство от клиента, нужно знать порт usbip, на котором оно сидит.

Всё функционирует корректно. Флешка и внешний привод DVD-RW тоже успешно подключаются, а вот веб-камеру мне пробросить на Windows не удалось.

Конечно, применимость этой технологии в промышленной среде под вопросом, прежде всего из-за возни с неподписанными драйверами и отключения механизма проверки подписи для Windows-клиента (Upd: автор подписал драйвер, теперь всё гораздо проще). Как будут себя вести клиенты, если сервер перезагрузится? А если убрать устройство на сервере без отключения на клиенте - как восстанавливать работу, не повиснет ли система? Вопросами автоматической привязки и подключения устройств на сервере и клиентах я также пока не занимался, хотя и знаю, что это возможно. Ещё одной особенностью, которую необходимо учитывать, является невозможность ограничения доступа к USB-устройству, опубликованному на сервере; по всей видимости, нужно будет это делать с помощью межсетевого экрана.

Тем не менее, технология интересная и потенциально полезная, нужно только как-то грамотно подобрать железо для сервера ключей - а им может быть что угодно, хоть Raspberry Pi - и получше оттестировать её.