Служба работает от владельца каталога, но не видит свой /home: как открыть один путь при ProtectHome
Служба стартует зелёной, работает от того самого пользователя, что владеет каталогом, права 0700 на месте — а в логе ENOENT на /home/exchange/out. Дежурный делает chmod -R 777, потом chown, потом запускает от root, и ничего не меняется. Разбираю, почему тут бессильны права UNIX, что именно ProtectHome делает с /home, /root и /run/user, как за три команды это диагностировать и как открыть службе ровно один каталог, не выключая защиту целиком.
Симптом: ls от того же пользователя работает, а служба — нет
Картина, которую я вижу раз в пару месяцев. Есть служба обмена, она работает под пользователем exchange, кладёт файлы в /home/exchange/out. Юнит запущен, active (running), никаких падений. В приложении — FileNotFoundError или Permission denied на каталоге, который стопроцентно существует. Заходишь на сервер, делаешь sudo -u exchange ls -la /home/exchange/out — каталог на месте, владелец правильный, права правильные. Логика подсказывает: значит, права всё-таки не те. И дальше начинается вредительство: chmod -R 777, chown -R, потом User=root в юните. Не помогает ничего, а дыра в правах остаётся в проде надолго.
Дело в том, что вы и служба смотрят на разные файловые системы. systemd умеет запускать юнит в отдельном mount namespace, и внутри этого namespace поверх /home примонтировано либо «ничего» (недоступный узел), либо пустая tmpfs. Права доступа тут вообще ни при чём: chmod меняет биты на объекте, которого процесс службы физически не видит. И root не спасает — ProtectHome прячет /home от процессов юнита независимо от того, кто в User=.
Отличить это от реальной проблемы с правами можно за десять секунд. Достаточно сравнить, что видит хост и что видит песочница с теми же настройками:
# что видит хост
sudo -u exchange ls -la /home/exchange/out
# что увидит служба с ProtectHome=yes
sudo systemd-run --pty -p ProtectHome=yes ls -la /home
# и с ProtectHome=tmpfs
sudo systemd-run --pty -p ProtectHome=tmpfs ls -la /homeЕсли первая команда показывает файлы, а вторая — пустой /home (от root) или «Permission denied» (если добавить -p User=exchange), а третья — пустую tmpfs, вопрос закрыт: это изоляция файловой системы, а не права. Дальше уже разговор не про chmod, а про то, какой именно каталог надо вернуть в поле зрения службы и каким ключом.
- служба active (running), но не находит файлы в собственной домашней папке;
- `sudo -u <юзер> ls` показывает данные, а служба — нет;
- chmod 777 и chown ничего не меняют;
- запуск от root ничего не меняет;
- в юните или в drop-in есть ProtectHome=, ProtectSystem=strict, DynamicUser= или готовый hardening-шаблон.
Что именно делает ProtectHome и почему это не про права
ProtectHome= — ключ секции [Service], он появился ещё в systemd 214 (в man так и написано: Added in version 214), и базовая семантика значений с тех пор не менялась — в актуальной версии man-страницы описание то же, что в systemd 252 из Debian 12. Он принимает булево значение либо специальные значения read-only и tmpfs, и действует на три каталога: /home/, /root и /run/user. Именно на все три, а не только на /home — про /run/user ниже отдельный разговор, там прячется половина неочевидных поломок.
Формулировки в systemd.exec(5) предельно конкретные. При значении yes три каталога делаются недоступными и пустыми для процессов юнита. При read-only они остаются видимыми, но становятся доступными только на чтение. При tmpfs на них монтируются временные файловые системы в режиме только для чтения — и вот это значение, цитирую документацию, «полезно, чтобы скрыть домашние каталоги, не относящиеся к процессам юнита, при этом позволяя сделать видимыми нужные каталоги, перечисленные в BindPaths= или BindReadOnlyPaths=».
Там же дана шпаргалка по эквивалентности, которая экономит массу времени при разборе: yes — это по сути те же три каталога в InaccessiblePaths=, read-only — почти то же, что ReadOnlyPaths=, а tmpfs — почти то же, что TemporaryFileSystem= с суффиксом :ro. То есть ProtectHome не изобретает своего механизма, это удобная обёртка над обычными namespace-примитивами systemd, и ведёт себя она по их правилам.
И отдельный пункт, на котором спотыкаются чаще всего: настройка неявно включается, если задан DynamicUser=. Формально в вашем юните строки ProtectHome может не быть вообще — а фактически будет ProtectHome=read-only, потому что DynamicUser=yes подтягивает за собой ещё и ProtectSystem=strict, PrivateTmp= (если не задан явно), NoNewPrivileges=, RestrictSUIDSGID= и RemoveIPC=. Человек читает свой .service, не находит ProtectHome и делает вывод, что дело в другом. Дело именно в этом.
- ProtectHome=no — значение по умолчанию, ограничений нет;
- ProtectHome=yes — /home, /root, /run/user пусты и недоступны (аналог InaccessiblePaths=);
- ProtectHome=read-only — каталоги видны, запись запрещена (аналог ReadOnlyPaths=);
- ProtectHome=tmpfs — поверх смонтированы пустые tmpfs в режиме ro (аналог TemporaryFileSystem= с :ro), содержимое скрыто, но можно вернуть точечно;
- ProtectHome=read-only включается неявно вместе с DynamicUser=yes.
Диагностика: три команды и вопрос закрыт
Первое правило: смотреть не свой .service, а эффективную конфигурацию. В половине случаев ProtectHome прилетает не из вашего файла, а из vendor-юнита пакета или из drop-in, который положил дистрибутив, конфигурационный менеджер или коллега полгода назад. systemctl cat показывает основной юнит и все drop-in подряд, systemctl show — итоговые значения после слияния.
# весь юнит целиком, включая drop-in из /usr/lib и /etc
systemctl cat exchange-agent.service
# итоговые значения после слияния всех фрагментов
systemctl show exchange-agent.service \
-p ProtectHome -p ProtectSystem -p DynamicUser \
-p BindPaths -p BindReadOnlyPaths \
-p ReadWritePaths -p ReadOnlyPaths -p InaccessiblePathsВторое — заглянуть внутрь живого namespace службы. Это железное доказательство, после которого спорить не о чем: вы буквально смотрите глазами процесса. Берём PID из systemctl show -p MainPID и идём внутрь.
PID=$(systemctl show -p MainPID --value exchange-agent.service)
# какие точки монтирования подсунуты юниту
grep -E ' /home| /root| /run/user' /proc/$PID/mountinfo
# посмотреть каталог глазами службы
sudo nsenter -t $PID -m -- ls -la /home /home/exchangeТретье — общий взгляд на песочницу. systemd-analyze security exchange-agent.service покажет таблицу применённых ограничений и итоговый уровень экспозиции по шкале 0.0–10.0, где высокие значения означают, что песочницы почти нет, а низкие — что она затянута туго. Инструмент удобен не столько оценкой, сколько списком: сразу видно, какие ключи реально включены, в том числе неявно. Только не воспринимайте цифру как метрику защищённости — документация прямо предупреждает, что оценивается лишь то, что реализует сам systemd, и что многие ограничения по отдельности обходятся.
Полезно различать и сами коды ошибок — они подсказывают, какой именно ключ сработал. «No such file or directory» на пути внутри /home почти всегда означает ProtectHome=tmpfs без нужного BindPaths= (поверх лежит пустая tmpfs, подкаталога в ней нет) или TemporaryFileSystem=. «Permission denied» при непривилегированном User= — ProtectHome=yes или InaccessiblePaths=: каталог заменён недоступным узлом. А «Read-only file system» (EROFS) при записи — это уже не ProtectHome=yes, а ProtectHome=read-only, ReadOnlyPaths= или ProtectSystem=strict, который делает всю файловую иерархию, кроме /dev, /proc и /sys, доступной только на чтение.
С ProtectSystem=strict правило такое: всё, куда служба пишет, перечисляется в ReadWritePaths= (или создаётся через StateDirectory= и соседние ключи — они сами попадают в список записываемых). Документация прямо разрешает вкладывать ReadWritePaths= внутрь ReadOnlyPaths=, чтобы получить записываемые подкаталоги, но вложить ReadWritePaths= или ReadOnlyPaths= внутрь InaccessiblePaths= нельзя — а ProtectHome=yes по смыслу и есть InaccessiblePaths= для /home, /root и /run/user. Поэтому ReadWritePaths=/home/exchange/out при ProtectHome=yes не «пробьёт» защиту, и разговор снова возвращается к tmpfs.
# какой ключ режет запись — смотрим журнал юнита и errno
journalctl -u exchange-agent.service -b --no-pager | grep -Ei 'denied|read-only|no such file|namespace'
# проверяем гипотезу во временном юните с теми же ключами
sudo systemd-run --pty -p User=exchange -p ProtectSystem=strict \
-p ProtectHome=tmpfs -p BindPaths=/home/exchange/out \
touch /home/exchange/out/probe- systemctl cat — увидеть все drop-in, а не только свой файл;
- systemctl show -p ProtectHome … — увидеть эффективные значения;
- grep ' /home' /proc/PID/mountinfo — увидеть, что подмонтировано юниту;
- nsenter -t PID -m -- ls /home — увидеть каталог глазами службы;
- systemd-analyze security UNIT — увидеть весь набор ограничений разом.
- EROFS = ProtectSystem=strict/read-only, EACCES = ProtectHome=yes, ENOENT под /home = tmpfs без BindPaths=.
Как открыть ровно один каталог: tmpfs плюс BindPaths
Интуитивно хочется оставить ProtectHome=yes и просто прокинуть внутрь нужную папку через BindPaths=. Так не работает, и это описано в документации явно. Для BindPaths= и BindReadOnlyPaths= сказано: каталог назначения должен существовать или systemd должен иметь возможность его создать, поэтому «невозможно использовать эти опции для точек монтирования, вложенных в пути, указанные в InaccessiblePaths=, или под /home/ и другими защищёнными каталогами, если задан ProtectHome=yes. Вместо этого следует использовать TemporaryFileSystem= с ':ro' или ProtectHome=tmpfs». Ровно это и есть ответ на вопрос темы.
Практически это значит: меняем yes на tmpfs и перечисляем нужные пути. Поверх /home ложится пустая tmpfs, systemd создаёт в ней точки монтирования и прокидывает туда реальные каталоги. Все остальные домашки при этом остаются невидимыми — то есть защита не отключена, она стала точечной.
# /etc/systemd/system/exchange-agent.service.d/10-paths.conf
[Service]
ProtectHome=tmpfs
# рабочие каталоги обмена — на запись
BindPaths=/home/exchange/in
BindPaths=/home/exchange/out
# ключи и конфиг — только на чтение, «-» = не падать, если пути нет
BindReadOnlyPaths=-/home/exchange/.gnupg
BindReadOnlyPaths=-/home/exchange/.config/exchangeСинтаксис стоит запомнить целиком, он экономит нервы. Каждое определение — это разделённая двоеточиями тройка «источник:назначение:опция», причём назначение и опция необязательны; если указан только источник, назначение считается тем же путём. Опция — rbind или norbind, то есть рекурсивный или нерекурсивный bind; если опцию не указать, systemd делает рекурсивный bind (в исходниках парсера значение по умолчанию — rbind). Префикс «-» перед определением означает «игнорировать, если источника нет». BindPaths= даёт запись (если сама исходная ФС не смонтирована только для чтения), BindReadOnlyPaths= — только чтение. И ловушка: присвоение пустой строки любому из двух ключей сбрасывает оба списка сразу, и read-only, и обычный.
Есть и более простой вариант, который я применяю, когда служба должна видеть весь /home, но писать только в один подкаталог: ProtectHome=read-only плюс ReadWritePaths= на нужный путь. Документация прямо рекомендует такой приём — вкладывать ReadWritePaths= внутрь read-only-областей, чтобы получить записываемые подкаталоги. Это менее строго, чем tmpfs (чужие домашки остаются читаемыми), зато конфиг короче и предсказуемее.
- нужен один каталог, остальное скрыть — ProtectHome=tmpfs + BindPaths=/BindReadOnlyPaths=;
- нужен весь /home на чтение и один каталог на запись — ProtectHome=read-only + ReadWritePaths=;
- ничего из /home не нужно — ProtectHome=yes и данные вынести в /var/lib;
- в каталоге-источнике есть вложенные точки монтирования (NFS, отдельный LV) — bind по умолчанию рекурсивный и подтянет их; :norbind указывайте, только если вложенное нужно скрыть.
Разбор: «Виниловый причал», выгрузка заказов встала после переезда на Debian 12
Магазин винила «Виниловый причал»: розничный зал, склад и интернет-витрина, 26 рабочих мест, свой сервер в нашей стойке. Переезжали с древней CentOS 7 (systemd 219) на Debian 12 с systemd 252. Среди прочего переезжал самописный агент обмена: 1С выгружает платёжные поручения поставщикам пластинок в /home/exchange/out, агент подписывает их ключом из /home/exchange/.gnupg и отдаёт в банк-клиент, а ответные выписки и статусы складывает в /home/exchange/in. Юнит работал от пользователя exchange, который и владел всеми этими каталогами.
При переезде юнит прогнали через типовой hardening-шаблон: ProtectSystem=strict, PrivateTmp=yes, NoNewPrivileges=yes, ProtectHome=yes, плюс десяток Restrict*-ключей. В 08:40 понедельника служба стартовала зелёной, в 08:44 бухгалтер магазина сообщила, что платежи поставщикам не уходят, а выписки за выходные не пришли. В журнале — обрыв на открытии /home/exchange/out. Дежурный сделал ровно то, что делают все: chmod -R 777 /home/exchange, потом chown -R exchange:exchange, потом убрал User= и запустил от root. Ноль эффекта — и это, к слову, отличный диагностический признак: если root не помог, дело не в правах.
Разбор занял шесть минут. systemctl show -p ProtectHome показал yes, grep ' /home' /proc/$PID/mountinfo — подмонтированный поверх узел, nsenter -t $PID -m -- ls /home — пустоту. Первый фикс был неверным: оставили ProtectHome=yes и добавили BindPaths=/home/exchange/out. Служба после этого перестала стартовать вовсе, падая с 226/NAMESPACE — ровно тот случай, который описан в документации как невозможный. Заменили yes на tmpfs, перечислили три пути, systemctl daemon-reload && systemctl restart exchange-agent.service — обмен пошёл.
sudo install -d /etc/systemd/system/exchange-agent.service.d
sudo tee /etc/systemd/system/exchange-agent.service.d/10-paths.conf >/dev/null <<'EOF'
[Service]
ProtectHome=tmpfs
BindPaths=/home/exchange/in
BindPaths=/home/exchange/out
BindReadOnlyPaths=-/home/exchange/.gnupg
EOF
sudo systemctl daemon-reload
sudo systemctl restart exchange-agent.service
sudo systemd-analyze security exchange-agent.service | head -20Общий простой — 50 минут, из которых 35 ушло на chmod, chown и запуск от root. Осадок остался в другом: права 0777 на каталоге с платёжками и ключами провисели ещё трое суток, пока их не вычистили при плановой проверке. Через неделю мы довели дело до конца — вынесли рабочие каталоги в /var/lib/exchange через StateDirectory=, вернули ProtectHome=yes и убрали Bind-строки совсем. Уровень экспозиции по systemd-analyze security упал примерно с 9 (когда служба крутилась от root без песочницы) до 4 с небольшим. Цифру привожу как ориентир: она сильно зависит от полного набора ключей и от версии systemd, сравнивать её имеет смысл только «до и после» на одной машине.
- root не помог — значит, проблема не в правах, а в namespace;
- ProtectHome=yes + BindPaths под /home = 226/NAMESPACE, служба не стартует;
- ProtectHome=tmpfs + BindPaths решил вопрос за одну перезагрузку юнита;
- финальное решение — вообще уйти из /home в /var/lib через StateDirectory=.
Где на самом деле должны лежать данные службы
Мой практический вывод после десятка таких разборов: если служба лезет в /home, это почти всегда наследство, а не архитектура. Домашние каталоги — для людей. Для служб в systemd давно есть штатный набор: RuntimeDirectory=, StateDirectory=, CacheDirectory=, LogsDirectory= и ConfigurationDirectory=. Каталоги создаются при старте юнита вместе с родителями, владельцем становится пользователь и группа из User=/Group=, а полный путь приезжает в окружение процесса переменными вида $STATE_DIRECTORY — не нужно даже хардкодить пути в коде.
Раскладка по системным юнитам такая: RuntimeDirectory= уходит в /run/, StateDirectory= — в /var/lib/, CacheDirectory= — в /var/cache/, LogsDirectory= — в /var/log/, ConfigurationDirectory= — в /etc/. Для пользовательских юнитов те же ключи ложатся в $XDG_RUNTIME_DIR, $XDG_STATE_HOME, $XDG_CACHE_HOME и $XDG_CONFIG_HOME. Важное отличие RuntimeDirectory= от остальных: его каталоги удаляются при остановке юнита (если не задан RuntimeDirectoryPreserve=), а StateDirectory=, CacheDirectory=, LogsDirectory= и ConfigurationDirectory= при остановке не удаляются — то есть StateDirectory= подходит именно для постоянных данных. Для пользовательских юнитов LogsDirectory= уходит в $XDG_STATE_HOME/log.
[Service]
User=exchange
StateDirectory=exchange
WorkingDirectory=/var/lib/exchange
ProtectHome=yes
ProtectSystem=strict
PrivateTmp=yes
NoNewPrivileges=yesПосле такого переезда ProtectHome=yes перестаёт быть проблемой — служба просто ничего не хочет от /home, и весь вопрос статьи снимается. Отдельно скажу, когда я всё-таки оставляю обмен через домашний каталог: когда путь зашит в закрытый бинарник вендора, когда домашки монтируются по NFS и туда пишут ещё и живые пользователи, и когда каталог — часть согласованного с контрагентом протокола обмена и переименовать его нельзя. Во всех трёх случаях связка tmpfs + Bind* — нормальное, а не костыльное решение.
- RuntimeDirectory= → /run/, удаляется при остановке службы (если не включён RuntimeDirectoryPreserve=);
- StateDirectory= → /var/lib/, переживает остановку, сюда и переносим рабочие данные;
- CacheDirectory= → /var/cache/, LogsDirectory= → /var/log/, ConfigurationDirectory= → /etc/;
- владелец каталогов — User=/Group= юнита, путь приезжает в $STATE_DIRECTORY и аналогичные переменные.
Побочные эффекты, о которых узнают позже
Первое и самое неочевидное — /run/user. ProtectHome прячет не только /home и /root, но и /run/user, а там живёт содержимое $XDG_RUNTIME_DIR: сокет сессионной шины D-Bus, сокеты gpg-agent, ssh-agent, keyring. Симптомы выглядят как поломка совсем другого софта: «gpg: не могу открыть /run/user/1001/gnupg/S.gpg-agent», «Failed to connect to bus», подпись перестала работать после включения hardening. Ищут в gpg, а надо смотреть в ProtectHome. Если службе действительно нужен агент — либо возвращайте нужный подкаталог через BindPaths при tmpfs, либо переводите gpg на явный GNUPGHOME в /var/lib.
Второе — bind-монтирования отключают распространение точек монтирования из юнита наружу. Документация формулирует так: эти настройки разрывают propagation монтирований от процессов юнита к хосту, и потому их нельзя использовать для служб, которые должны устанавливать точки монтирования в основном mount namespace. Если ваш сервис монтирует CIFS/NFS/loop-образы для остальной системы, Bind* его сломает, и это не баг.
Третье — вложенные точки монтирования. Здесь чаще ошибаются в обратную сторону: bind в BindPaths= по умолчанию рекурсивный, так что уже смонтированные внутри источника LV или NFS-шара внутри юнита будут видны. Проблемы начинаются с тем, что смонтировано позже — autofs, ручной mount после старта службы. В systemd.exec(5) прямым текстом сказано, что ограничение доступа не распространяется на подмонтирования, созданные позднее, и что такая блокировка не полная; а для ReadWritePaths=/ReadOnlyPaths= отмечено, что монтирования с хоста продолжают появляться в namespace юнита. Если нужно, наоборот, не тянуть вложенное, пишите BindPaths=/home/exchange/out:/home/exchange/out:norbind.
И честно про уровень защиты. ProtectHome — не тюрьма. Документация не скрывает, что настройка «не может обеспечить защиту во всех случаях» и что эффект этих настроек могут отменить привилегированные процессы — поэтому man рекомендует дополнять их CapabilityBoundingSet=~CAP_SYS_ADMIN или SystemCallFilter=~@mount. Это страховка от чтения чужих персональных данных скомпрометированным демоном и от случайной записи не туда — очень дёшево и очень полезно, но не замена изоляции контейнером или отдельной ВМ. Поэтому мой приоритет такой: сначала вынести данные службы из /home, потом включить ProtectHome=yes, и только если первое невозможно — tmpfs с точечными Bind-путями. Отключать ProtectHome насовсем ради одного каталога не нужно никогда.
- внезапные ошибки gpg-agent, D-Bus и keyring после включения hardening — это скрытый /run/user;
- Bind*-ключи разрывают propagation монтирований наружу — не для служб, монтирующих ФС хосту;
- вложенные монтирования подтягиваются по умолчанию (rbind); смонтированное после старта юнита — отдельная история, проверяйте mountinfo;
- после каждой правки — daemon-reload, рестарт и проверка systemd-analyze security.
Частые вопросы
Почему chmod 777 и запуск от root не возвращают службе доступ к /home?
Потому что ограничение работает не на уровне прав UNIX, а на уровне mount namespace. При ProtectHome=yes поверх /home, /root и /run/user в пространстве имён юнита монтируются пустые недоступные узлы, и внутри службы каталога просто нет — ни для пользователя, ни для root. Права на объекте, которого процесс не видит, роли не играют.
Можно ли оставить ProtectHome=yes и прокинуть один каталог через BindPaths?
Нет. В systemd.exec(5) прямо сказано, что BindPaths= и BindReadOnlyPaths= нельзя использовать для точек монтирования под /home и другими защищёнными каталогами, если задан ProtectHome=yes, и рекомендуется вместо этого применять ProtectHome=tmpfs или TemporaryFileSystem= с :ro. На практике такой юнит, как правило, не стартует с ошибкой настройки пространства имён (status=226/NAMESPACE).
Чем отличается ProtectHome=tmpfs от read-only?
read-only оставляет реальное содержимое /home, /root и /run/user видимым, но запрещает запись. tmpfs монтирует поверх них пустые временные файловые системы в режиме только для чтения — содержимое чужих домашних каталогов не видно вообще, и при этом нужные пути можно вернуть точечно через BindPaths= или BindReadOnlyPaths=. Для строгой изоляции берите tmpfs, для быстрого решения «всё видно, писать нельзя» — read-only с ReadWritePaths= на нужный подкаталог.
В моём юните нет строки ProtectHome, откуда ограничение?
Смотрите эффективную конфигурацию: `systemctl cat` покажет vendor-юнит пакета и все drop-in из /usr/lib/systemd/system/*.d и /etc/systemd/system/*.d, `systemctl show -p ProtectHome` — итоговое значение. Кроме того, ProtectHome=read-only включается неявно при DynamicUser=yes вместе с ProtectSystem=strict, PrivateTmp=, NoNewPrivileges=, RestrictSUIDSGID= и RemoveIPC=.
После включения hardening сломался gpg и подпись файлов. Это тоже ProtectHome?
Очень вероятно. ProtectHome прячет ещё и /run/user, где лежат сокеты gpg-agent, ssh-agent, keyring и сессионной шины D-Bus. Либо верните нужный подкаталог через BindPaths при ProtectHome=tmpfs, либо переведите службу на собственный GNUPGHOME в /var/lib, что надёжнее.
Работает ли ProtectHome в пользовательских юнитах systemctl --user?
Зависит от версии systemd. В man systemd 252 (Debian 12) сказано, что ключ доступен системным службам, а в пользовательских инстансах менеджера — только при включённом PrivateUsers=, для чего ядро должно разрешать непривилегированные user namespace. Если они запрещены sysctl, настройка namespace не удастся и юнит не запустится. Точную формулировку для своей версии смотрите в `man systemd.exec` на самом сервере.
Служба пишет «Read-only file system», хотя ProtectHome выключен. Что это?
Это почти наверняка ProtectSystem=strict (явный или неявный через DynamicUser=yes) или ReadOnlyPaths=. При strict вся иерархия, кроме /dev, /proc и /sys, доступна только на чтение, и каждый каталог для записи нужно перечислить в ReadWritePaths= либо создать через StateDirectory=, CacheDirectory= или LogsDirectory=. Проверить итоговые значения можно командой `systemctl show UNIT -p ProtectSystem -p ReadWritePaths`.
Источники
- systemd.exec(5) — ProtectHome= — Раздел Sandboxing: значения no/yes/read-only/tmpfs, эквивалентность InaccessiblePaths=/ReadOnlyPaths=/TemporaryFileSystem= с :ro, неявное включение при DynamicUser=, оговорка о неполной защите и ограничениях как у ReadOnlyPaths=. Added in version 214. https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ProtectHome=
- systemd.exec(5) — BindPaths=, BindReadOnlyPaths= — Синтаксис «источник:назначение:опция», rbind/norbind, префикс «-», сброс обоих списков пустой строкой и ключевое ограничение: опции нельзя использовать под /home при ProtectHome=yes, вместо этого следует применять TemporaryFileSystem= с :ro или ProtectHome=tmpfs. Added in version 233. Сверено по systemd 252.36 (Debian 12). https://manpages.debian.org/bookworm/systemd/systemd.exec.5.en.html
- systemd.exec(5) — RuntimeDirectory=, StateDirectory=, CacheDirectory=, LogsDirectory=, ConfigurationDirectory= — Таблица «Automatic directory creation and environment variables»: соответствие /run, /var/lib, /var/cache, /var/log, /etc и переменных $RUNTIME_DIRECTORY, $STATE_DIRECTORY и др.; поведение при остановке юнита. https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#RuntimeDirectory=
- systemd-analyze(1) — команда security — Описание расчёта exposure level в диапазоне 0.0…10.0, оговорка о том, что учитываются только средства самого systemd и что отдельные ограничения обходятся. https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html#systemd-analyze%20security%20UNIT...
- systemd.exec(5), исходник man-страницы и парсер BindPaths= — Актуальный текст man/systemd.exec.xml в репозитории systemd (ProtectHome=, BindPaths=, ReadWritePaths=, DynamicUser=, таблица RuntimeDirectory=/StateDirectory=) и src/core/load-fragment.c, функция config_parse_bind_paths — rbind по умолчанию. https://github.com/systemd/systemd/blob/main/man/systemd.exec.xml
- Linux Audit — ProtectHome setting — Разбор значений ProtectHome и рекомендация использовать tmpfs, когда нужно скрыть домашние каталоги, но оставить доступ к конкретным путям через BindPaths/BindReadOnlyPaths. https://linux-audit.com/systemd/settings/units/protecthome/
