В этой статье
02:15. Задание выполнилось успешно — и это проблема
Дежурный видит обычную ночную операцию: обслуживание приложения завершилось без ошибок. В sudoers сотруднику разрешён ограниченный набор действий. Пароль root ему не выдавали, в административную группу не добавляли. Формально всё аккуратно. Но системная служба запускает сценарий из каталога, который доступен этому сотруднику для изменения.
Это вымышленный составной кейс. Мы не будем подменять сценарий, создавать привилегированную оболочку или проверять атаку на production. Нам достаточно восстановить цепочку доверия: кто меняет входные файлы, кто исполняет их содержимое, когда это происходит и какие ограничения действуют на каждом этапе.
Главная находка может оказаться не уязвимостью systemd и не кражей пароля. Система честно выполняет конфигурацию, в которой обслуживание приложения незаметно стало управлением привилегированным кодом. Чтобы увидеть это, нужно перестать считать выданную роль полным описанием реальных возможностей.
Право менять программу — тоже полномочие
Представьте две разрешённые операции. Первая позволяет редактировать отчётный шаблон, который приложение читает как данные. Вторая позволяет менять сценарий, который ночью исполняет root. В файловой системе обе выглядят как право записи. Но последствия определяет потребитель: формат, интерпретатор, момент чтения и полномочия процесса.
Поэтому фраза «у него только доступ к папке приложения» ничего не говорит о безопасности без следующего вопроса: кто доверяет содержимому этой папки? Если компонент читается как код более привилегированным процессом, право обслуживания пересекает границу. Масштаб последствий зависит от доступных ресурсов, namespaces, capabilities и обязательных политик, а не только от UID.
Это ещё не доказательство злоупотребления. Установленная возможность и состоявшийся инцидент — разные выводы. Аудит должен сначала доказать сам путь передачи доверия, затем отдельно выяснять, использовал ли его кто-нибудь.
Рисуем не дерево файлов, а цепочку исполнения
Начальная схема выглядит просто: unit → ExecStart → сценарий. Через десять минут она обрастает деталями: интерпретатор, подключаемая библиотека, файл параметров, каталог plugins, ссылка current, вспомогательная программа и отдельная команда ExecStartPre. У каждого ребра свой смысл: исполнить, импортировать, разобрать параметры или выбрать следующий путь.
На каждом узле записываем четыре вещи: кто может изменить объект, кто использует его содержимое, откуда берётся путь и в какой момент объект открывается. Пятая колонка — ограничение проверки: не прочитан код, неизвестен ACL, отсутствует история, не разобран namespace.

Автоматически вытащить первый путь из ExecStart и назвать аудит завершённым нельзя. Программа может выбирать модули во время работы, а shell-сценарий — подключать другой файл. Граф доверия строят по конфигурации и фактической логике приложения, не по одной красивой строке.
Сначала выбираем одну службу и правильный manager
Проверку начинаем с точного имени одной согласованной system-службы. Это не пользовательский manager systemctl —user. Не подставляйте шаблон всех сервисов и не смешивайте вывод разных серверов. Для экземпляра template-unit требуется фактическое имя экземпляра, а не только заготовка с @.
# Bash on the approved Linux host; one SYSTEM service, not systemctl --user.
unit='REPLACE_EXACT_SERVICE.service'
if [[ "$unit" == REPLACE_* || "$unit" != *.service || "$unit" == -* || "$unit" == *[!a-zA-Z0-9_.@:-]* ]]; then
printf '%s\n' 'Set one exact, simple service name; no patterns.' >&2
return 1 2>/dev/null || exit 1
fi
systemctl --version
date --utc --iso-8601=seconds
systemctl --system show "$unit" --no-pager \
--property=Id,LoadState,ActiveState,SubState,FragmentPath,DropInPaths,NeedDaemonReload,MainPID,ControlGroup
# Read-only. Do not daemon-reload/restart merely to make two views agree.
# Preserve the same approved Bash session for later unit-based blocks.
LoadState=not-found — не безопасная служба, а неверная цель или недоступная конфигурация. FragmentPath показывает источник основного определения, DropInPaths — применяемые дополнения. Сначала фиксируем состояние, затем читаем конфигурацию. Нельзя выполнять daemon-reload только потому, что предупреждение мешает красивому отчёту: это уже изменение состояния manager.
Примеры предназначены для Bash на Linux с GNU/util-linux и установленными средствами чтения ACL. Не повышайте права автоматически: если данных не видно, зафиксируйте пробел и согласуйте диагностический доступ.
Файл на диске, конфигурация manager и запущенный процесс
В нашем кейсе коллега открывает unit в редакторе: User=appsvc, выглядит безопасно. Но это лишь один документ. Есть drop-in, состояние, загруженное manager, и уже работающий процесс. Они могут относиться к разным моментам. Это три снимка, а не один факт.
# Unit content and command arguments may contain secrets: local restricted review only.
if [[ -z "$unit" || "$unit" == REPLACE_* ]]; then
printf '%s\n' 'Run the unit-selection block first.' >&2
return 1 2>/dev/null || exit 1
fi
systemctl --system cat "$unit" --no-pager
systemctl --system show "$unit" --no-pager \
--property=User,Group,SupplementaryGroups,DynamicUser,WorkingDirectory,RootDirectory,RootImage,EnvironmentFiles \
--property=ExecCondition,ExecStartPre,ExecStart,ExecStartPost,ExecReload,ExecStop,ExecStopPost \
--property=NoNewPrivileges,CapabilityBoundingSet,AmbientCapabilities,ProtectSystem,ReadWritePaths,PrivateTmp
# No Environment= dump and no EnvironmentFile sourcing.
# cat = files on disk; show = manager properties; neither reconstructs all past executions.
# Missing properties/version limitations are collection gaps, not a safe verdict.
Согласно systemctl, cat показывает файлы на диске; они могут не совпадать с пониманием manager после несогласованных изменений. show предоставляет свойства manager, но не доказывает всю историю запуска. NeedDaemonReload — повод сохранить расхождение, не исправлять его немедленно.
Unit и аргументы команд могут содержать секреты. Смотрите вывод в разрешённой закрытой среде; не публикуйте его целиком. Мы намеренно не выгружаем Environment и /proc/environ. Даже «диагностический» сбор должен иметь границу по чувствительности.
Почему одного User= недостаточно
Для обычной системной службы отсутствие явного User= и динамического пользователя обычно означает запуск от root; для пользовательского manager модель другая. Но даже явно заданная сервисная личность не позволяет остановить расследование: смотрим каждую команду жизненного цикла, её префиксы и дальнейшие переходы внутри программы.
Особенно важно не пропустить ExecCondition, ExecStartPre, ExecStartPost, ExecReload, ExecStop и ExecStopPost. В версиях, где поддерживаются префиксы + и !, они меняют применение определённых ограничений к отдельной команде. Их семантика различается; плюс не следует считать безусловным обходом вообще всей политики ОС. Сверяйте локальный systemd.service.
Дежурный в нашем сюжете обнаруживает, что обсуждали основной процесс, а обслуживаемый ночной этап относится к другой system-службе. В ней доверенная конфигурация поручает более привилегированной личности читать изменяемый компонент. Ошибка была не в одной строке User=, а в неполной карте запуска. Версионная документация systemd.service.
Проверяем живой процесс — без чтения его секретов
Если служба работает, сравним конфигурацию с фактическим основным процессом. В /proc/PID/status интересуют UID/GID, дополнительные группы, capabilities, NoNewPrivs и Seccomp. Это текущий снимок, а не описание всех дочерних и уже завершившихся процессов.
# Main process only, at collection time; helpers and short-lived stages need separate evidence.
if [[ -z "$unit" || "$unit" == REPLACE_* ]]; then
printf '%s\n' 'Select the service first.' >&2
return 1 2>/dev/null || exit 1
fi
service_pid=$(systemctl --system show "$unit" --property=MainPID --value) || {
return 1 2>/dev/null || exit 1
}
if [[ "$service_pid" =~ ^[1-9][0-9]*$ && -r "/proc/$service_pid/status" ]]; then
ps -p "$service_pid" -o pid,ppid,lstart,user,group,comm
awk '/^(Name|Pid|PPid|Uid|Gid|Groups|CapInh|CapPrm|CapEff|CapBnd|CapAmb|NoNewPrivs|Seccomp):/' "/proc/$service_pid/status"
readlink -- "/proc/$service_pid/exe"
readlink -- "/proc/$service_pid/cwd"
else
printf '%s\n' 'No readable live MainPID: inspect lifecycle and permissions; not proof of safety.'
fi
# PID reuse/process exit can race this snapshot. Re-check unit/PID/time before correlating.
# No /proc/environ or command-line secret collection, no tracing or process execution.
MainPID=0 для завершившегося oneshot не означает, что его команды никогда не выполнялись. Короткий подготовительный этап можно вообще не застать в proc. PID способен исчезнуть или быть переиспользован между запросами; поэтому корреляция требует времени и повторной привязки к unit.
Поля Uid/Gid содержат несколько идентификаторов, а capabilities — разные наборы. Не сокращайте вывод до «UID не ноль, значит полномочий нет». Для интерпретации используйте proc_pid_status. Наличие ограничений тоже не говорит, какие бизнес-данные процесс законно видит.
ExecStart — не обещание shell-семантики
Следующий поворот возникает, когда администратор читает ExecStart как команду из своего терминала. У systemd собственные правила разбора; нельзя без проверки переносить туда ожидания о pipes, перенаправлениях и подстановках shell. Если явно запускается интерпретатор, дальше разбираем уже его правила, а не правила systemd.
При запуске сценария важны сам интерпретатор и подключаемые им файлы. Исполняемый файл и прочитанный интерпретатором текст — разные объекты контроля. Отсутствие executable-бита на тексте не доказывает невозможность его интерпретации, если доверенный процесс явно передаёт его интерпретатору как вход.
Также проверяем разрешение относительных имён, рабочий каталог и механизм поиска вспомогательных программ. Но не запускаем подозрительный сценарий ради вывода help или version: такие аргументы не гарантируют отсутствие побочных действий. Изучаем исходник или согласованный артефакт статически. Правила команд systemd.service; execve и интерпретаторы.
Подключаемый файл может быть важнее главного
Главный сценарий принадлежит root и выглядит неизменяемым для сотрудника. На этом легко остановиться. Но внутри есть загрузка общей библиотеки из каталога приложения. Она читается позже, в том же процессе и с его контекстом. Защищённая оболочка не создаёт доверия к изменяемому содержимому, которое она решила исполнить.
По той же причине изучаем plugins, startup hooks, конфигурацию с командами и выбранные приложением пути. Обычный параметр размера очереди не равен коду; имя загружаемого модуля или команды — уже другой тип влияния. Нужно знать семантику конкретного приложения, не объявлять любой writable-конфиг готовым root-доступом.
Нельзя выполнять source над EnvironmentFile для удобного разбора. Для systemd это специальный формат окружения, не обязательно shell-программа. Само по себе EnvironmentFile не означает выполнение его текста. Риск зависит от того, как потребитель использует полученные значения. Секреты из него в отчёт не копируют.
Почему владелец root и режим 0644 ещё не ответ
Коллега показывает stat: файл принадлежит root, группа не имеет права записи. Но путь к нему проходит через каталог, которым управляет обслуживающая группа. Возможность изменять байты существующего файла и возможность менять запись имени в каталоге — разные полномочия. Оценивать нужно оба.
Обычно для переименования и удаления важны запись и поиск в родительских каталогах, а не разрешение записи в содержимое самого файла. Однако sticky bit, ACL, mount options, immutable-атрибуты и обязательная политика могут менять результат. Поэтому не превращаем одно значение mode в доказательство успешной замены. Ограничения rename.
В нашем учебном кейсе подозрительная область — не весь /opt, а конкретный управляемый релиз и путь его выбора. Это важно и для исправления: массовый chown -R способен сломать приложение, не устранив доверие к другому изменяемому источнику.
Ссылки, current и права на каждый компонент пути
Ссылка current удобна для атомарного выбора релиза. Но если менее доверенная личность выбирает её назначение, а root позднее доверяет этому пути, переключение релиза становится частью привилегированной цепочки. Важен не только конечный файл, но и то, кто контролирует сам выбор.
namei -l показывает компоненты пути и переходы по символическим ссылкам. Затем отдельно проверяем ACL исходной цепочки и разрешённого назначения. Одна проверка realpath не заменяет первую: она может скрыть управляющий каталог исходной ссылки. namei; разрешение пути в Linux.
Контекст пространства имён тоже важен. RootDirectory, RootImage и bind mounts могут означать, что процесс видит иной объект по тому же текстовому пути. Приведённый ниже сборщик смотрит файловое пространство аудитора; автоматически приравнивать его к представлению службы нельзя.
Группа есть в справочнике. А в работающем сеансе?
Проверяем конкретную личность обслуживания, а не только имя владельца файла. id показывает сведения об учётной записи из доступных баз. Но уже открытый сеанс может сохранять прежний список дополнительных групп. Поэтому удаление членства в справочнике само по себе не доказывает, что доступ исчез у всех действующих процессов.
# This queries account databases. It does NOT impersonate the maintenance identity.
maintenance_account='REPLACE_APPROVED_MAINTENANCE_ACCOUNT'
if [[ "$maintenance_account" == REPLACE_* || "$maintenance_account" == -* || -z "$maintenance_account" ]]; then
printf '%s\n' 'Set the exact approved maintenance account.' >&2
return 1 2>/dev/null || exit 1
fi
id -- "$maintenance_account"
getent passwd "$maintenance_account"
# Existing sessions may retain older supplementary groups.
# Compare with the actual approved session, ACLs and applicable MAC policy.
# Do not run application scripts as this user or as root to test a hypothesis.
Для POSIX ACL учитываем именованные записи и маску эффективных разрешений. Default ACL описывает начальные права создаваемых объектов, а не дополнительное разрешение для каждого уже существующего файла. Вывод getfacl полезнее догадки по значку плюс у ls, но и его нужно читать в контексте личности и всего пути. acl(5).
Не проверяйте права записи от root и не приписывайте результат сотруднику. Даже проверка от согласованной тестовой личности отражает конкретный текущий контекст, а не все пути делегирования и исторические полномочия.
Сборщик метаданных для заранее выбранных путей
Ниже самостоятельный Bash-скрипт audit-paths.sh. Сохраните его в доверенном рабочем каталоге и передайте от одного до двенадцати уже проверенных абсолютных путей: unit, drop-in, сценарий, библиотеку или конфигурацию. Он не ищет файлы по всему диску, не читает содержимое сценариев и ничего не исполняет из исследуемого приложения.
#!/usr/bin/env bash
# audit-paths.sh: metadata-only review of 1..12 explicitly approved existing paths.
# Usage: bash audit-paths.sh /absolute/reviewed/path [another/path ...]
# No recursive search, source/eval, file modification, service operation or impersonation.
set -u
if (( $# < 1 || $# > 12 )); then
printf '%s\n' 'Provide 1..12 explicit approved absolute paths.' >&2
exit 2
fi
for tool in namei realpath stat getfacl dirname; do
command -v "$tool" >/dev/null 2>&1 || { printf 'Missing tool: %s\n' "$tool" >&2; exit 2; }
done
result=0
for target in "$@"; do
if [[ "$target" != /* || "$target" == *$'\n'* || "/$target/" == */../* || "/$target/" == */./* ]]; then
printf 'Rejected path: %q\n' "$target" >&2
result=1
continue
fi
printf '\nPATH %q\n' "$target"
if ! resolved=$(realpath -e -- "$target"); then
printf '%s\n' 'Unresolved/inaccessible: collection gap.' >&2
result=1
continue
fi
namei -l -- "$target" || result=1
printf 'RESOLVED %q\n' "$resolved"
# Inspect both the lexical chain and canonical chain, including their parent ACLs.
for chain in "$target" "$resolved"; do
cursor=$chain
while :; do
printf 'OBJECT %q\n' "$cursor"
stat --format='mode=%A uid=%u gid=%g inode=%i type=%F name=%N' -- "$cursor" || result=1
getfacl --absolute-names -- "$cursor" || result=1
[[ "$cursor" == / ]] && break
cursor=$(dirname -- "$cursor") || { result=1; break; }
done
done
done
printf '\n%s\n' 'Metadata snapshot only; no effective-access or exploitability verdict.'
exit "$result"
Скрипт выводит метаданные и ACL родительских каталогов для исходного и канонического пути. Требует существующих объектов и доступного чтения; ошибки отмечает ненулевым кодом, а не зелёным итогом. Повторяющиеся родительские записи ожидаемы. Большие каталоги не обходятся рекурсивно, но длинные и удалённые пути всё равно требуют оценки доступности.
Это не сканер эксплуатации и не вычислитель эффективного доступа выбранного сотрудника. Он не оценивает автоматически mount namespace, MAC, атрибуты файлов, network filesystem semantics и будущие гонки замены. Используйте его как воспроизводимую основу ручной проверки, не как сертификат безопасности.
Формулируем находку так, чтобы её можно было проверить
Собрав данные, не пишем «сотрудник получил root» только по writable-каталогу. Хорошее заключение связывает конкретный объект, право изменения, привилегированного потребителя и достижимый момент чтения. Если один элемент неизвестен, это ограничение или гипотеза, а не установленный переход.
| Наблюдение | Что нужно подтвердить | Чего оно не доказывает |
|---|---|---|
| Файл принадлежит root | Права всего пути, ACL и заменяемость | Неизменяемость содержимого для сотрудника |
| Есть право записи | Личность, группы, ACL, реальный контекст | Исполнение с root автоматически |
| Есть привилегированный этап | Какие компоненты он читает как код | Чтение именно изменяемого объекта |
| Есть триггер запуска | Достижимый момент потребления | Прошлое злоупотребление |
| NoNewPrivs установлен | Имеющиеся полномочия процесса | Доверенное происхождение кода |
Для нашей модели вывод может звучать так: доверенный ночной процесс с подтверждённым контекстом использует код из области, которую обслуживающая личность может менять; ограничения не разрывают этот путь. Это подтверждённая проблема границы доверия. Использовал ли кто-то возможность, остаётся самостоятельным вопросом расследования.
Почему NoNewPrivileges не исправляет эту архитектуру
Название звучит почти как обещание: новых привилегий не будет. Но механизм препятствует определённому получению дополнительных полномочий при execve. Он не превращает уже привилегированный процесс в непривилегированный и не делает загружаемый код доверенным. Если процесс уже имеет нужный доступ, запрет дальнейшего повышения может не затрагивать основной риск. Документация ядра.
В результате обе строки могут одновременно быть правдой: NoNewPrivileges установлен, а менее доверенное содержимое используется с имеющимися полномочиями службы. Противоречия нет. Защита решает другую часть задачи.
Разбирать нужно не название параметра, а конкретный запрещаемый переход. То же относится к NoNewPrivs в proc: это полезное подтверждение флага для выбранного процесса, не доказательство безопасного происхождения всех его модулей и сценариев.
Read-only для службы — не read-only для сотрудника
ProtectSystem и ReadOnlyPaths ограничивают представление файловой системы для процессов службы. Из этого нельзя автоматически заключать, что другой пользователь хоста больше не меняет исходный код. Если внешний авторизованный writer может обновить читаемый компонент, запрет записи из самой службы не равен запрету внешней модификации.
ReadWritePaths, capabilities, IPC и доступные mounts тоже входят в модель. Ограничение записи не тождественно запрету чтения секретов или обращения к привилегированному сервису через сокет. Более того, допустимое влияние кода может находиться целиком в разрешённой области: испортить данные самого приложения тоже серьёзно.
Поэтому sandbox — слой ограничения ущерба, а контроль происхождения кода — самостоятельная граница. Их стоит сочетать, но нельзя подменять одно другим. Конкретную семантику и доступность настроек сверяют с systemd.exec установленной версии и возможностями ядра.
Оценка systemd-analyze — не вердикт о коде
Инструмент полезен, когда нужно систематически увидеть настройки изоляции. Но число exposure не отвечает, кто может менять подключаемую библиотеку или кто согласовал релиз. Хороший балл не превращает изменяемый сценарий в доверенный. Плохой — сам по себе не доказывает эксплуатацию.
# Review only the selected service; no hardening changes are applied.
if [[ -z "$unit" || "$unit" == REPLACE_* ]]; then
printf '%s\n' 'Select the exact service first.' >&2
return 1 2>/dev/null || exit 1
fi
systemd-analyze security --no-pager "$unit"
# Exposure score describes assessed unit settings, not code provenance or compromise.
# Do not add every suggested restriction or change ownership recursively on production.
# Verify local man pages: unsupported directives must not be assumed active.
Документация systemd-analyze прямо ограничивает анализ механизмами systemd. Доступ к IPC и защита, реализованная приложением, требуют отдельной оценки. Используем результат для вопросов и пилотного плана, а не для соревнования «сделать ноль любой ценой».
Если предложенная настройка несовместима с обновлением, резервированием или запуском приложения, это инженерная задача. Не отключайте защиту молча, но и не ломайте работу ради числа. Сначала выделите реальные обязанности службы и допустимые области записи.
Почему не спасает запрет на restart
Иногда ответ звучит так: «Сотрудник не может перезапустить службу, значит изменённый файл не выполнится». Но запуск может происходить по таймеру, после загрузки, по запросу другого компонента или при очередном согласованном обслуживании. Некоторые приложения перечитывают модули и конфигурацию без полного restart.
Поэтому отдельно устанавливаем триггер и момент потребления. Для timer-unit не предполагаем автоматически службу с похожим именем: Unit= может выбирать другую. Если активная служба не перезапускается при очередном срабатывании таймера, это тоже меняет временную модель. systemd.timer.
Отсутствие права немедленно инициировать запуск может снижать управляемость момента, но не обязательно устраняет доверие к изменяемому коду. В кейсе именно ночное расписание превращает сегодняшнее право записи в завтрашнее исполнение. Никакой демонстрационной подмены для установления этой зависимости не требуется.
История: запуск виден, изменение файла — не всегда
Нам нужна временная линия: кто изменил релиз, когда выбрали его путь, что прочитала служба и в каком контексте завершился этап. Journal помогает с жизненным циклом, но не является автоматическим полным аудитом всех файловых операций. Успешный статус не доказывает, какое историческое содержимое было прочитано.
# Explicit unit and UTC incident window. Journal output may contain secrets.
incident_start='REPLACE_UTC_START'
incident_end='REPLACE_UTC_END'
if [[ -z "$unit" || "$unit" == REPLACE_* || "$incident_start" == REPLACE_* || "$incident_end" == REPLACE_* ]]; then
printf '%s\n' 'Set the selected unit and approved time window.' >&2
return 1 2>/dev/null || exit 1
fi
# Example timestamp FORMAT only: YYYY-MM-DD HH:MM:SS UTC. Replace with actual boundaries.
journalctl --system --unit="$unit" --since="$incident_start" --until="$incident_end" \
--utc --no-pager --output=short-iso-precise --lines=200
systemctl --system show "$unit" --no-pager --property=TriggeredBy,Triggers,Result,ExecMainCode,ExecMainStatus
# At most 200 matching records displayed: missing older matches are NOT absence of activity.
# Inspect actual timer/path/socket units from TriggeredBy; do not assume a same-name timer.
# Journal absence does not reconstruct past file writes or every executed helper.
В примере вывод ограничен последними 200 совпадениями. Для полного согласованного расследования потребуется другой объём и порядок сохранения; молчание этой выборки не оправдывает систему. Сопоставляем доступный audit, deployment/CI, систему контроля версий и независимо хранимые журналы. Сбор аудита, которого вчера не было, не восстанавливает вчерашние события.
mtime, владелец и хеш текущего файла — текущие свойства, не доказательство авторства и прошлого состояния. Даже хеш совпавшего релиза не отвечает, не использовался ли раньше другой путь. Недостающие свидетельства прямо отмечаем в отчёте.
Исправление начинается с разделения кода и данных
Правильная цель не «запретить сотруднику всё». Ему действительно нужно обслуживать приложение. Но ежедневные данные, загрузки, логи и временные файлы не должны автоматически становиться кодом более привилегированного этапа. Выделите области, где запись разрешена по работе, и области, откуда привилегированный процесс берёт доверенные исполняемые компоненты.
Для релизов полезна отдельная публикация: подготовка менее привилегированной личностью, проверка артефакта и контролируемое продвижение в защищённую область. Проверка должна учитывать содержимое, зависимости и неизменность выбранного артефакта до использования. Одна подпись бесполезна, если та же личность свободно подменяет проверяющий код или определяет, что считать доверенным.
Если приложение может работать без root, это сильный способ уменьшить пересечение границ. Если нужен короткий привилегированный этап, его обязанности и входы стоит сузить отдельно. Но готового универсального drop-in для всех приложений нет: реальные требования определяют проверенный проект, а не набор модных флагов.
План изменения без chown -R по работающему приложению
До изменения фиксируем точные пути, владельцев, ACL, mount context, используемые релизы и способ отката. Согласуем окно с владельцем приложения. Проверяем, где ожидается запись: кэш, очередь, сокет, lock-файл, журнал, временный каталог, обновление. Иначе можно «усилить безопасность» до отказа бизнес-операции.
| Этап | Проверка | Условие перехода |
|---|---|---|
| Исходное состояние | Unit + drop-ins + пути + ACL | Точные объекты и закрытый архив |
| Пилот | Один компонент и несекретная копия | Согласованные роли и откат |
| Изменение модели | Разделение кода и рабочих данных | Нет массовой рекурсивной правки |
| Положительный тест | Запуск, обслуживание, backup/restore | Полезная операция работает |
| Отрицательный тест | Доверенный код вне контроля обслуживающего | Без payload и подмены production |
| Следующий релиз | Повторная проверка цепочки | Deployment не вернул прежний риск |
Меняем один пилотный компонент, отдельно проверяем законное обслуживание и недоступность записи в доверенный код. Не используем рекурсивное исправление всех ACL как замену пониманию. Для production-изменений нужны согласованные права, резервная копия конфигурации и рабочий откат; в командах статьи таких изменений нет.
Как доказать исправление без вредоносного сценария
На несекретной тестовой копии проверяем матрицу допустимых действий. Обслуживающая личность меняет рабочие данные там, где это положено. Доверенные unit, drop-ins, интерпретатор, сценарии, подключаемый код и путь выбора релиза остаются вне её управления. Штатный запуск, обновление, остановка и восстановление продолжают работать.
Не обязательно доказывать отказ записи созданием файла в production. Метаданные, расчёт прав и согласованные отрицательные проверки в испытательной среде позволяют подтвердить границу без внедрения payload. При необходимости функционального теста его заранее описывают: личность, объект, ожидаемый отказ и способ наблюдения.
Отдельно проверяем повторный релиз: часто исходное исправление исчезает после следующего deployment, когда pipeline снова создаёт каталог с прежней группой и default ACL. Защита должна воспроизводиться процессом поставки, а не жить в памяти одного администратора.
Чем заканчивается ночная история
В начале сотруднику «не выдавали root». В конце выясняется, что выдали влияние на содержимое, которому доверяет более привилегированный процесс. Это не игра слов: в эксплуатации полномочия возникают не только из паролей и sudo, но и из отношений между изменяемыми компонентами и их потребителями.
В учебном кейсе проблема найдена без подмены файлов. Мы установили привилегированного потребителя, путь чтения, контроль менее доверенной стороны и доступный момент исполнения. Историческое злоупотребление этим путём не объявляем доказанным: для него нужна собственная временная линия.
Для аудита Linux-приложения полезный результат — карта цепочки исполнения, доказанные области записи, ограничения проверки и пилотный план исправления. Если нужна такая диагностика, начинать стоит с одной критичной службы и её реального процесса обслуживания. Это точнее, чем ещё раз убедиться, что в sudoers нет слова ALL.
Источники и честные границы статьи
Документация проверена 22 октября 2026 года. Online latest сейчас описывает systemd 262; это не означает, что такая версия установлена на вашем сервере. Используйте systemctl —version и локальные man-страницы, учитывайте патчи дистрибутива. Неподдерживаемые настройки и недоступные свойства нельзя молча считать действующей защитой.
Основные источники: systemd.exec, systemd.unit, systemd.service 252, getfacl. Остальные первичные источники указаны рядом с выводами. Текст — самостоятельный разбор модели доверия, не инструкция по эксплуатации чужого сервера.
Все семь блоков прошли статическую проверку синтаксиса Bash. Команды чтения systemd и ACL не проверялись на полноценном Linux-стенде с целевой службой. Сначала разрешённый пилот. Примеры не меняют ACL, не запускают исследуемый код, не перезапускают службы и не устанавливают правила аудита. Неполный сбор всегда остаётся неполным сбором, а не доказательством безопасности.



