systemd изнутри: юниты, зависимости, ограничения ресурсов и песочница сервисов
Когда на 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 — это набор связанных навыков, вот соседние темы серии: