Выберите продукт

systemd: отладка unit-файлов, ExecStartPre, capabilities и sandboxing, когда порт не слушает

Иногда systemd показывает active (running), но порт не слушает: процесс завершился слишком рано, старт оборвал ExecStartPre, не хватает прав на 80/443 или sandboxing перекрыл доступ к файлам и сокетам. Разберём диагностику и настройку.
systemd: отладка unit-файлов, ExecStartPre, capabilities и sandboxing, когда порт не слушает

Иногда systemd говорит: «всё отлично, сервис запущен», а мониторинг и ss -lntp отвечают обратным: порт не слушает. Обычно причина приземлённая: упал ExecStartPre, неверно выбран Type, не хватает прав на привилегированный порт или слишком жёсткое systemd sandboxing отрезало процессу доступ к файлам, сокетам или сети.

Ниже — практический чек‑лист для админов/DevOps: как читать логи через journalctl, как «раскрыть» итоговую конфигурацию unit-файла и как аккуратно настраивать AmbientCapabilities и sandboxing так, чтобы безопасность росла, а сервис не ломался.

Быстрый триаж: жив ли сервис и что именно не так

Начинайте с простого: systemd может считать сервис «активным», даже если приложение внутри уже упало или завершилось слишком быстро (часто из-за неверного Type).

1) Статус юнита и последние ошибки

systemctl status myapp.service
systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p MainPID -p Result
systemctl is-failed myapp.service

Если Result не success, systemd обычно пишет конкретную причину. Если MainPID пустой — разберитесь с Type и поведением процесса: возможно, приложение завершилось или юнит вообще не держит процесс «на привязи».

2) Логи через journalctl: точечно и без лишнего шума

journalctl -u myapp.service -b --no-pager
journalctl -u myapp.service -b -n 200 --no-pager
journalctl -u myapp.service -b -p warning..alert --no-pager

Для кейса «порт не слушает» важны первые секунды старта: ошибки биндинга, падение конфиг‑теста, запреты sandboxing, проблемы с правами на файлы и каталоги.

3) Проверка, что порт действительно не открыт (и кем должен быть открыт)

ss -lntp
ss -lntp '( sport = :80 )'

Если порт пустой — выясняем, почему процесс не дошёл до этапа биндинга. Если порт занят другим процессом — это конфликт, а не «не слушает».

Unit debug: увидеть реальную итоговую конфигурацию запуска

Сложность unit-файлов в том, что вы смотрите на ExecStart, а реально запускается цепочка: зависимости, ExecStartPre, ограничения безопасности, окружение, рабочая директория. В отладке помогает «раскрыть» итоговую конфигурацию.

Показать юнит полностью (с drop-in) и найти переопределения

systemctl cat myapp.service
systemctl show myapp.service --no-pager
systemctl show myapp.service -p FragmentPath -p DropInPaths

Частая причина «странностей» — override в /etc/systemd/system/myapp.service.d/override.conf, который когда-то добавили «на время», а он остался.

Проверить синтаксис и ошибки unit-файлов

systemd-analyze verify /etc/systemd/system/myapp.service

Это не панацея, но ловит часть проблем: опечатки, неверные секции, несуществующие директивы (особенно если переносили юнит между версиями systemd).

Важно: systemd не использует shell по умолчанию

Конструкции вида ExecStart=cd /app && ./server не «магически» выполняются в оболочке: systemd запускает бинарник напрямую. Если нужна оболочка — указывайте её явно, но чаще правильнее разложить логику на WorkingDirectory и отдельные простые ExecStartPre.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

ExecStartPre: типичные ловушки и быстрая диагностика

ExecStartPre полезен (миграции, проверка конфигов, подготовка каталогов), но именно он часто делает старт «неочевидным»: человек видит «что-то выполнилось», а systemd уже остановил запуск до ExecStart.

Пример вывода systemctl status и journalctl для диагностики systemd-сервиса

Как systemd обрабатывает ExecStartPre

  • Каждый ExecStartPre запускается по очереди.
  • Если любой ExecStartPre завершился с ненулевым кодом — старт сервиса считается проваленным, ExecStart не выполняется.
  • Префикс - перед командой позволяет игнорировать ошибку, но используйте это только осознанно (иначе легко замаскировать реальную причину).

Понять, какой pre-скрипт упал и почему

systemctl status myapp.service
journalctl -u myapp.service -b -n 200 --no-pager

В логах ищите строку вида «Running ExecStartPre=…». Если команда сложная — вынесите её в отдельный скрипт: так проще контролировать логирование и exit code.

Примеры ExecStartPre без «магии» shell

Вместо «cd и запусти» используйте нативные директивы:

[Service]
WorkingDirectory=/opt/myapp
ExecStartPre=/usr/bin/test -r /opt/myapp/config.yml
ExecStartPre=/usr/bin/env
ExecStart=/opt/myapp/myapp --config /opt/myapp/config.yml

/usr/bin/env как временный ExecStartPre помогает увидеть окружение, с которым стартует сервис (после отладки лучше убрать, чтобы не засорять журнал).

Кейс «порт не слушает» из-за ExecStartPre

Типовой сценарий: ExecStartPre делает конфиг‑тест, но использует относительные пути или ожидает файл в другом месте. Вручную вы запускали из каталога проекта — всё работало. В systemd рабочий каталог другой — precheck падает, и до биндинга портов процесс не доходит.

Решение: задайте WorkingDirectory и используйте абсолютные пути для всех файлов.

Когда сервис «active», но порт не слушает: причины, которые реально встречаются

1) Неверный Type и «успешный» выход процесса

Если демон форкается в фоне, а вы оставили Type=simple, systemd может «потерять» главный процесс. Если демон не форкается, а вы указали Type=forking, systemd будет ждать форк/PID-файл и старт станет «странным».

Для современных приложений чаще подходят Type=simple или Type=notify (если поддерживается systemd-notify). И по возможности избегайте демонизации внутри приложения, если им управляет systemd.

2) Приложение слушает не тот адрес

«Порт не слушает» нередко означает «слушает только localhost». Проверьте конфиг приложения: 127.0.0.1:8080 вместо 0.0.0.0:8080 или вместо нужного IPv6. systemd тут ни при чём, но вы поймаете это на этапе unit debug.

3) Конфликт порта или привязка к 80/443 без прав

Если сервис должен слушать 80/443, а вы запускаете его не от root, нужно право CAP_NET_BIND_SERVICE (или фронтовой прокси перед приложением). Ниже покажу, как выдать capability точечно.

4) Sandboxing отрезал доступ к сокетам/файлам/каталогам

Сервис может стартовать, но не создать Unix-сокет, не открыть конфиг, не записать PID/лог, не прочитать сертификаты — в итоге приложение уходит в ошибку или стартует «в деградированном режиме» без слушающего сокета.

5) Неправильные права на каталоги RuntimeDirectory/StateDirectory

Если вы пишете PID-файл, сокет или временные файлы, лучше использовать RuntimeDirectory, StateDirectory, CacheDirectory, а не изобретать ручные mkdir в ExecStartPre. Так меньше гонок, меньше проблем с правами и понятнее модель владения.

Capabilities: как безопасно дать сервису право слушать 80/443

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

AmbientCapabilities и CapabilityBoundingSet: разница по смыслу

AmbientCapabilities добавляет capability в ambient set, чтобы она наследовалась исполняемым процессом.

CapabilityBoundingSet ограничивает максимум того, что вообще может быть у процесса. Практика простая: bounding set делайте минимальным, а в ambient добавляйте только то, что действительно нужно.

Пример: слушаем 80, но остаёмся не-root

[Service]
User=myapp
Group=myapp
ExecStart=/opt/myapp/myapp --listen :80
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true

Если после этого всё равно «порт не слушает», вернитесь к journalctl: часто приложение падает до биндинга по другой причине, а capability оказалась просто «красивой версией» проблемы.

Проверка capabilities у процесса

systemctl show myapp.service -p MainPID
cat /proc/$(systemctl show myapp.service -p MainPID --value)/status | grep Cap

Если MainPID пустой — сначала разберитесь с типом/стартом, иначе проверять нечего.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

systemd sandboxing: как включать жёсткость без самострела

Под systemd sandboxing обычно подразумевают набор директив, которые уменьшают поверхность атаки: изоляция файловой системы, запрет опасных системных вызовов, ограничение сети, запрет повышения привилегий и т.д.

Включайте sandboxing итеративно: добавили одну группу директив, перезапустили, посмотрели journalctl. Если включить всё сразу, вы получите «порт не слушает» и несколько часов охоты на одну-единственную настройку.

Схема: цепочка запуска systemd (ExecStartPre/ExecStart) с capabilities и sandboxing

Базовый «разумный минимум» для сетевого демона

[Service]
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ProtectControlGroups=true
ProtectKernelTunables=true
ProtectKernelModules=true
LockPersonality=true
RestrictSUIDSGID=true
RestrictRealtime=true
MemoryDenyWriteExecute=true
RuntimeDirectory=myapp
StateDirectory=myapp
ReadWritePaths=/var/lib/myapp /run/myapp

Ключевой момент: если включили ProtectSystem=strict, почти наверняка нужно описать, куда сервису можно писать. Делайте это через StateDirectory/RuntimeDirectory и точечные ReadWritePaths.

SystemCallFilter и «внезапно перестало работать»

SystemCallFilter полезен, но это самая «хрупкая» часть. Неправильный фильтр может приводить к падениям библиотек, TLS-стека, рантаймов языков. На практике часто больший эффект дают более простые директивы: NoNewPrivileges, ProtectSystem, PrivateTmp, ProtectHome, RestrictSUIDSGID.

RestrictAddressFamilies: сеть «вроде есть», но сокет не создаётся

Если вы ограничили адресные семейства, приложение может не создать IPv6-сокет или Unix-сокет — и в итоге не поднять слушатель.

[Service]
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6

Если используете только IPv4, можно убрать AF_INET6, но сначала убедитесь, что приложение/библиотеки не рассчитывают на dual-stack.

PrivateDevices и доступ к системным ресурсам

Некоторые демоны читают системную информацию (например, для метрик) и могут ломаться при слишком жёстких ограничениях устройств и псевдо‑файловых систем. Если вы включили PrivateDevices=true и поведение стало странным — это хороший кандидат на временное отключение для подтверждения гипотезы.

Практический шаблон юнита: подготовка, capabilities, sandboxing и понятные логи

Ниже — скелет unit-файла, который удобно отлаживать. Пути и команды подставьте свои.

[Unit]
Description=MyApp HTTP service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStartPre=/usr/bin/test -r /opt/myapp/config.yml
ExecStart=/opt/myapp/myapp --config /opt/myapp/config.yml --listen :80
Restart=on-failure
RestartSec=2
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
RuntimeDirectory=myapp
StateDirectory=myapp
PrivateTmp=true
ProtectHome=true
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp /run/myapp
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Методика «включаем по одному»: быстро найти директиву, которая всё сломала

Если вы усилили юнит и получили «не стартует» или «порт не слушает», не откатывайте всё сразу. Работает метод двоичного поиска, но обычно хватает итераций по смысловым группам.

  1. Сначала временно уберите sandboxing (жёсткие директивы), оставив User, WorkingDirectory, ExecStart, ExecStartPre.
  2. Если заработало — возвращайте директивы пачками: файловая система (ProtectSystem/ReadWritePaths), затем tmp/devices, затем capabilities/NoNewPrivileges, затем address families.
  3. После каждого изменения делайте reload и перезапуск.
systemctl daemon-reload
systemctl restart myapp.service
systemctl status myapp.service
journalctl -u myapp.service -b -n 200 --no-pager

При отладке полезно временно поставить Restart=no, чтобы сервис не уходил в цикл рестартов и не перемешивал логи.

Частые сообщения в journalctl и что они обычно означают

Permission denied при bind на 80/443

Обычно это отсутствие CAP_NET_BIND_SERVICE при запуске не от root. Исправляется через AmbientCapabilities или переносом прослушивания на непривилегированный порт с проксированием.

No such file or directory на ExecStartPre/ExecStart

Либо путь неверный, либо вы полагаетесь на PATH (в unit он может быть минимальным), либо бинарник не исполняемый. Указывайте абсолютные пути.

Read-only file system / Operation not permitted после ProtectSystem

Сервис пытается писать туда, куда теперь нельзя. Решение: писать в каталоги, которые создал systemd (StateDirectory/RuntimeDirectory), и добавлять ReadWritePaths точечно.

Failed to determine user credentials

Неправильный User/Group или пользователь не создан. Проверьте:

getent passwd myapp

Финальный чек‑лист перед тем как считать проблему решённой

  • Сервис стабильно в состоянии active (running) без рестарт‑петель.
  • Порт действительно слушается: ss -lntp.
  • Ошибок в journalctl при старте нет, предупреждения понятны.
  • Права минимальны: вы не запускаете от root без необходимости, а CapabilityBoundingSet ограничен.
  • sandboxing включён итеративно и документирован: вы понимаете, зачем нужна каждая директива.

Если дальше хочется довести unit до «боевого» уровня, следующий шаг — добавить понятные сигналы готовности (например, Type=notify там, где это поддерживается) и ограничители ресурсов в cgroup (CPU/RAM/FD), чтобы один сбойный процесс не уронил всю машину. Но фундамент почти всегда один: корректный запуск, наблюдаемость через journalctl и минимальные, проверяемые права.

Когда сервис уже стабилен, имеет смысл переносить такие настройки на предсказуемую инфраструктуру: например, развернуть приложение на VDS, где вы полностью контролируете версии systemd, права, firewall и сетевые политики.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: конфликт systemd-resolved DNSStubListener на 127.0.0.53 с dnsmasq, Unbound и BIND OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: конфликт systemd-resolved DNSStubListener на 127.0.0.53 с dnsmasq, Unbound и BIND

Если локальный DNS в Debian или Ubuntu не стартует с ошибкой address already in use, причина часто в systemd-resolved и DNSStubLis ...
Debian/Ubuntu: как исправить NFS mount.nfs: access denied by server while mounting OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить NFS mount.nfs: access denied by server while mounting

Ошибка mount.nfs: access denied by server while mounting в Debian и Ubuntu обычно указывает на проблему на стороне NFS-сервера: не ...
Debian/Ubuntu: как устранить конфликт systemd-resolved DNSStubListener с BIND9, dnsmasq и AdGuard Home OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как устранить конфликт systemd-resolved DNSStubListener с BIND9, dnsmasq и AdGuard Home

Если в Debian или Ubuntu DNS-сервер не стартует из-за ошибки port 53 busy, часто причина в systemd-resolved с локальным слушателем ...