Когда на srv-linux01 что-то не стартует после перезагрузки, первым делом ты открываешь systemctl status. Но многие администраторы так и остаются на уровне «start, stop, enable». А systemd умеет гораздо больше: строить граф зависимостей, ограничивать процессы по памяти и процессору, запирать сервис в песочницу так, что даже взломанный процесс почти ничего не сможет сделать. В этой статье разберём устройство юнитов и соберём защищённый сервис для srv-web01 в домене itpages.lab.local.

Все примеры проверялись на Debian 12 и Rocky Linux 9 (systemd 252 и новее). Команды выполняются от root или через sudo.

Как устроены юниты

Типы юнитов и где они лежат

Юнит — это описание любого объекта, которым управляет systemd. Самые частые типы: .service (процесс), .socket (сокет для активации по запросу), .timer (замена cron), .mount (точка монтирования), .target (группа юнитов, аналог runlevel).

Файлы юнитов systemd ищет в трёх местах, по убыванию приоритета: /etc/systemd/system (твои настройки), /run/systemd/system (временные) и /usr/lib/systemd/system (файлы из пакетов). Правило простое: файлы пакетов не правим никогда, всё своё кладём в /etc.

systemctl list-units --type=service --state=running
systemctl cat nginx.service
systemctl show nginx.service -p FragmentPath,MainPID,MemoryCurrent

Команда systemctl cat показывает итоговый файл вместе со всеми drop-in фрагментами, и это лучший способ понять, что реально применяется к сервису.

Drop-in вместо правки оригинала

Чтобы изменить поведение пакетного юнита, используй drop-in каталог. Команда systemctl edit создаёт его и открывает редактор:

systemctl edit nginx.service
# в открывшемся файле:
[Service]
Restart=on-failure
RestartSec=5s

Файл появится как /etc/systemd/system/nginx.service.d/override.conf. После обновления пакета твои правки останутся на месте, а оригинал обновится. Если нужно заменить файл целиком, есть systemctl edit --full.

Свой сервис с нуля

Допустим, на srv-web01 работает внутреннее приложение, слушающее порт 8080. Создаём юнит:

cat > /etc/systemd/system/itpages-app.service <<'EOF'
[Unit]
Description=IT-Pages internal app
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=itapp
Group=itapp
WorkingDirectory=/opt/itpages-app
ExecStart=/opt/itpages-app/bin/server --listen 192.168.10.20:8080
Restart=on-failure
RestartSec=3s

[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now itpages-app.service

Не забывай daemon-reload после каждого изменения файлов юнитов: без него systemd продолжит работать со старой копией и покажет предупреждение.

Зависимости и порядок запуска

Requires, Wants, After: в чём разница

Здесь чаще всего путаются. Директивы зависимости (Requires=, Wants=, BindsTo=) отвечают на вопрос «что запускать вместе». Директивы порядка (After=, Before=) отвечают на вопрос «в какой очерёдности». Они независимы друг от друга.

  • Wants= — мягкая зависимость: если соседний юнит не запустился, твой всё равно стартует.
  • Requires= — жёсткая: если зависимость упала при старте, твой юнит тоже не запустится.
  • BindsTo= — ещё строже: юнит остановится, как только остановится тот, к которому он привязан.
  • After= — только порядок. Без него Requires запустит оба юнита одновременно.

Типичная ошибка: написать Requires=postgresql.service и забыть After=postgresql.service. Приложение стартует параллельно с базой и падает на первом же подключении.

Смотрим граф зависимостей

systemctl list-dependencies itpages-app.service
systemctl list-dependencies --reverse postgresql.service
systemd-analyze critical-chain itpages-app.service
systemd-analyze blame | head -15

critical-chain показывает, какие юниты задерживают запуск твоего сервиса, а blame — кто дольше всех стартовал при загрузке. Если srv-linux01 грузится три минуты, ответ почти всегда находится именно здесь, например в юните, ждущем network-online.target при недоступном NFS-сервере.

Таймеры вместо cron

Раз уж зашла речь о юнитах, посмотри на таймеры. У них есть журнал, зависимости и защита от одновременного запуска. Пример еженедельной очистки:

cat > /etc/systemd/system/cleanup.timer <<'EOF'
[Unit]
Description=Weekly cleanup

[Timer]
OnCalendar=Sun 03:30
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target
EOF
systemctl enable --now cleanup.timer
systemctl list-timers

Параметр Persistent=true догоняет пропущенный запуск, если хост был выключен в назначенное время. Сам сервис cleanup.service описывается отдельно, с Type=oneshot.

Ограничения ресурсов через cgroups

Память, процессор, процессы

Каждый сервис systemd запускает в собственной cgroup, и этим можно пользоваться. Допустим, приложение иногда съедает всю память и тянет за собой соседей по srv-web01. Ограничиваем:

systemctl edit itpages-app.service
[Service]
MemoryHigh=768M
MemoryMax=1G
CPUQuota=150%
TasksMax=200
IOWeight=50

MemoryHigh — мягкий порог: после него ядро начинает притормаживать процесс и вытеснять его страницы. MemoryMax — жёсткий: при превышении сработает OOM-killer, но только внутри этой группы. CPUQuota=150% означает полтора ядра. TasksMax защищает от fork-бомбы.

Результат проверяется так:

systemctl show itpages-app.service -p MemoryMax,CPUQuotaPerSecUSec,TasksMax
systemd-cgtop -d 2

Команда systemd-cgtop — это «top» по группам: видно, какой сервис ест процессор и память, без угадывания по именам процессов.

Ограничения на лету

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

systemctl set-property --runtime itpages-app.service MemoryMax=512M
systemctl set-property itpages-app.service CPUQuota=100%

Без --runtime настройка сохранится в drop-in и переживёт перезагрузку. Это удобно для экстренных мер, которые потом оформляются в постоянную конфигурацию.

Песочница для сервисов

Базовые директивы изоляции

Самая недооценённая возможность systemd — встроенная песочница. Несколько строк в юните ограничивают то, что сможет сделать процесс, даже если его взломают. Начни с набора, который подходит почти любому сетевому демону:

[Service]
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictSUIDSGID=true
RestrictRealtime=true
LockPersonality=true
ReadWritePaths=/var/lib/itpages-app /var/log/itpages-app

Что это даёт. ProtectSystem=strict монтирует всю файловую систему только для чтения, кроме явно указанных в ReadWritePaths. ProtectHome=true прячет /home, /root и /run/user. PrivateTmp выдаёт сервису собственный пустой /tmp, поэтому он не увидит чужие временные файлы. NoNewPrivileges запрещает повышать привилегии через setuid-бинарники.

Сеть и системные вызовы

Дальше можно ограничить сетевые семейства и разрешённые системные вызовы:

RestrictAddressFamilies=AF_INET AF_UNIX
IPAddressDeny=any
IPAddressAllow=192.168.10.0/24
SystemCallFilter=@system-service
SystemCallArchitectures=native
CapabilityBoundingSet=
AmbientCapabilities=

Пара IPAddressDeny=any и IPAddressAllow=192.168.10.0/24 разрешает сервису общаться только с нашей внутренней сетью, и это работает на уровне ядра через eBPF, без правил iptables. Фильтр @system-service оставляет набор вызовов, нужный обычным демонам, и отрезает экзотику вроде ptrace или монтирования. Пустой CapabilityBoundingSet лишает процесс всех capabilities. Если твоему сервису нужен порт ниже 1024, оставь только CAP_NET_BIND_SERVICE.

Оцениваем защищённость

Чтобы не гадать, насколько хорошо закрыт сервис, есть специальный анализатор:

systemd-analyze security itpages-app.service
systemd-analyze security --no-pager | head -20

Он выводит таблицу проверок и общую оценку от 0.0 (идеально) до 10.0 (открыто всё). Типичный нетронутый сервис получает 9.x, а после набора выше оценка опускается до 2–3. Не гонись за нулём: сервис, который перестал работать, защищён идеально, но бесполезен. Цель — разумный минимум прав.

Как найти, что именно мешает приложению в песочнице? Включай ограничения по одному и смотри журнал:

systemctl restart itpages-app.service
journalctl -u itpages-app.service -b --no-pager | tail -30
systemd-analyze syscall-filter @system-service

Если приложение падает с «Permission denied» или «Read-only file system», значит, ему нужно что-то дописать в ReadWritePaths. Если процесс убит сигналом SIGSYS, фильтр системных вызовов слишком строг, и можно временно заменить его на SystemCallFilter=@system-service @mount или проверить конкретный вызов через strace.

Динамические пользователи и каталоги состояния

Ещё одна полезная пара — DynamicUser=yes и StateDirectory=. Systemd сам создаёт временного пользователя на время работы сервиса и каталог /var/lib/имя с нужными правами:

[Service]
DynamicUser=yes
StateDirectory=itpages-app
LogsDirectory=itpages-app
RuntimeDirectory=itpages-app
RuntimeDirectoryMode=0750

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

Диагностика и повседневная работа

Несколько команд, которые стоит держать под рукой на srv-linux01:

systemctl --failed
systemctl status itpages-app.service -l
journalctl -u itpages-app.service -f
systemctl reset-failed itpages-app.service
systemd-analyze verify /etc/systemd/system/itpages-app.service

Команда verify ловит опечатки в именах директив и несуществующие пути ещё до запуска. Привыкни прогонять её после каждого редактирования юнита, это экономит массу времени.

Для проверки ограничений в реальном времени можно запустить разовую команду в песочнице без написания юнита:

systemd-run --pty -p ProtectSystem=strict -p PrivateTmp=yes -p MemoryMax=256M /bin/bash

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

Практический пример: ставим всё вместе

Сборка итогового юнита

Соберём всё сказанное в один файл для приложения на srv-web01. Идея в том, чтобы ты увидел, как отдельные директивы работают вместе, а не лежат россыпью в документации. Итоговый юнит получает пользователя без привилегий, ограничения по ресурсам, защищённую файловую систему и сетевой фильтр.

[Unit]
Description=IT-Pages internal app (hardened)
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service

[Service]
User=itapp
ExecStart=/opt/itpages-app/bin/server --listen 192.168.10.20:8080
Restart=on-failure
MemoryMax=1G
CPUQuota=150%
TasksMax=200
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/itpages-app
IPAddressDeny=any
IPAddressAllow=192.168.10.0/24

[Install]
WantedBy=multi-user.target

Прогони его через systemd-analyze verify, запусти и оцени командой systemd-analyze security. Если приложению нужно подключиться к внешнему API, расширь IPAddressAllow, но делай это осознанно и записывай причину в комментарии прямо в файле. Комментарии в юнитах начинаются с символа решётки и никак не влияют на работу.

Что делать, если сервис не стартует

Вначале прочитай статус: systemd показывает код выхода, сигнал и последние строки журнала. Код 203/EXEC означает, что исполняемый файл не найден или не имеет прав на запуск, 217/USER говорит об ошибке с пользователем, а 226/NAMESPACE указывает на проблемы с песочницей: например, в ReadWritePaths указан несуществующий каталог. Многие директивы изоляции требуют, чтобы пути существовали заранее, иначе сервис упадёт ещё до запуска приложения. Создавай каталоги через StateDirectory и LogsDirectory, и такой проблемы не будет. В случае зависания при остановке посмотри параметр TimeoutStopSec: по умолчанию systemd ждёт девяносто секунд, а потом шлёт SIGKILL, и для приложений с долгим сбросом данных на диск этого времени может не хватить.

Ещё один совет: храни свои юниты и drop-in файлы в системе контроля версий, а на серверы раскладывай их автоматически. Тогда любая правка проходит ревью, а восстановить конфигурацию после сбоя srv-web01 можно за минуты.

Сокет-активация и перезапуск при сбоях

Сервис по требованию

Одна из самых изящных возможностей systemd — активация по сокету. Systemd сам слушает порт и запускает сервис только при первом подключении. Это экономит память на хостах, где редко используемые службы большую часть времени простаивают, и позволяет обновлять сервис без потери входящих соединений: пока процесс перезапускается, ядро держит очередь подключений в сокете. Для этого рядом с юнитом создаётся файл itpages-app.socket с директивой ListenStream=192.168.10.20:8080, а приложение должно уметь принимать готовый дескриптор (многие современные серверы это умеют через переменные LISTEN_FDS).

Политика перезапуска без петель

Параметр Restart=on-failure удобен, но без ограничений превращается в бесконечную петлю: сервис падает, стартует, снова падает и грузит процессор. Защита встроена: StartLimitIntervalSec=300 и StartLimitBurst=5 в секции Unit остановят попытки после пяти сбоев за пять минут, а юнит перейдёт в состояние failed. Сбросить его можно через systemctl reset-failed. Добавь также OnFailure=notify-admin@%n.service, чтобы при падении запускался отдельный юнит, отправляющий письмо или сообщение в мессенджер. Это превращает молчаливый сбой в событие, о котором ты узнаёшь сразу, а не из жалобы пользователя.

Для проверки готовности сервиса используй Type=notify, если приложение умеет сообщать о готовности через sd_notify, или ExecStartPost с короткой проверкой порта. Зависимые юниты тогда стартуют не раньше, чем основной действительно готов принимать запросы, а не просто запущен как процесс.

Частые ошибки и как их избежать

  • Правка файлов в /usr/lib/systemd/system. При обновлении пакета твои изменения затрутся. Используй systemctl edit и drop-in файлы в /etc.
  • Requires без After. Зависимость есть, а порядка нет, поэтому сервисы стартуют одновременно и падают. Всегда указывай оба, а результат проверяй через systemd-analyze critical-chain.
  • Включение всей песочницы разом. Если добавить два десятка директив сразу, непонятно, какая сломала сервис. Добавляй по три-четыре, перезапускай, читай журнал, и только потом двигайся дальше. Не забывай про daemon-reload после каждой правки.

Что дальше

Администрирование Linux — это набор связанных навыков, вот соседние темы серии: