АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Как отдать пароль службе с User=app, если файл читает только root

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~26 мин чтения
Как отдать пароль службе с User=app, если файл читает только root
Иллюстрация к статье «Как отдать пароль службе с User=app, если файл читает только root».

Ситуация, которую я вижу у каждого второго нового клиента: лежит /etc/app/db.pass с правами 0400 root:root, служба запущена с User=app, приложение падает с Permission denied. Дальше обычно происходит одно из двух — либо chmod 644, либо пароль переезжает прямо в unit-файл строкой Environment=. Оба варианта раздают секрет всей машине. Ниже — как systemd умеет отдавать секреты службам штатно, начиная с версии 247: исходный файл остаётся 0400 root:root, служба получает его копию, доступную только своему пользователю, и никакой пароль не светится ни в юните, ни в переменных окружения. С командами, разбором реального стенда и списком мест, где это ломают из раза в раз.

Секрет отдаёт не приложение и не root, а сам менеджер служб

Корень путаницы в том, что админ мысленно решает задачу «дать пользователю app доступ к файлу». Отсюда и лечение: chmod 640 плюс chown root:app, или ExecStartPre=/bin/chmod 644, или ACL через setfacl. Все три варианта расширяют круг читателей файла на диске навсегда — а нужно-то было отдать содержимое одному конкретному процессу на время его жизни. systemd умеет ровно это, и механизм называется credentials.

Работает так. В юните вы пишете LoadCredential=ID:PATH. Файл по пути PATH открывает менеджер служб, то есть PID 1, — он root, ему права 0400 root:root не мешают. Содержимое копируется в отдельный каталог, персональный для этого юнита, и уже там файл получает владельца из User= и режим 0400. Каталог по возможности размещается в неподкачиваемой памяти (ramfs) и монтируется в юнит только на чтение. Путь к нему приложение получает в переменной $CREDENTIALS_DIRECTORY, а в самом unit-файле тот же каталог доступен через спецификатор %d. Для системных служб физически это /run/credentials/имя.service.

Что мы с этого получаем, если разложить по пунктам. Исходный файл на диске не трогали — он как был 0400 root:root, так и остался, и следующая инвентаризация прав не найдёт лишнего. Секрет доступен только пользователю службы и суперпользователю: в документации это сказано прямым текстом — «The data is only accessible to the user associated with the unit, via the User=/DynamicUser= settings (as well as the superuser)». Он не попадает в переменные окружения, а значит не наследуется вниз по дереву процессов, включая переход через setuid-бинарники. И его нельзя вытащить через D-Bus, в отличие от Environment=. Отдельно про /proc/<pid>/environ: при Environment= пароль лежит в окружении процесса, и его читает любой процесс того же UID (плюс root) — например, любой дочерний скрипт или отладчик, запущенный от пользователя службы. При LoadCredential= в environ есть только путь к каталогу $CREDENTIALS_DIRECTORY, самого секрета там нет.

Последнее стоит подчеркнуть отдельно, потому что это самый недооценённый риск. Мануал systemd.exec(5) прямо предупреждает: переменные окружения юнита «are exposed to unprivileged clients via D-Bus IPC». То есть непривилегированный локальный пользователь спокойно делает systemctl show app.service -p Environment и читает ваш пароль от базы. Не root, не член группы app — просто любой залогиненный юзер. Я показывал это клиентам на их же машинах ровно один раз, дальше вопросов «а зачем усложнять» не возникало.

Ограничение у механизма одно и оно щадящее: суммарный размер всех credentials одного юнита — 1 МБ. Для паролей, токенов, приватных ключей и сертификатов этого хватает с запасом; упереться можно разве что в жирный Java keystore или связку сертификатов целиком.

Environment= и EnvironmentFile= для паролей не годятся не потому, что «так не принято», а потому что содержимое окружения юнита отдаётся непривилегированным клиентам по D-Bus и наследуется дочерними процессами. Это не теория, это проверяется одной командой systemctl show.
Цифры и версии: Секрет отдаёт не приложение и не root, а сам менеджер служб — схема
Цифры и версии: Секрет отдаёт не приложение и не root, а сам менеджер служб. Открыть схему в полном размере

Минимальный рабочий рецепт: три строки в drop-in

Ничего не правим в файле, который принёс пакет. Только drop-in — так правка переживёт обновление пакета и будет видна следующему админу в одном предсказуемом месте.

# каталог хранилища и файл с секретом: 0700 и 0400, владелец root
install -d -m 0700 -o root -g root /etc/credstore
install -m 0400 -o root -g root /dev/null /etc/credstore/dbpass
# пароль вводим интерактивно, чтобы он не остался в истории shell
systemd-ask-password -n "Пароль БД:" > /etc/credstore/dbpass

systemctl edit app.service

В открывшийся редактор кладём:

[Service]
LoadCredential=dbpass:/etc/credstore/dbpass
Environment=APP_DB_PASSWORD_FILE=%d/dbpass

Дальше systemctl daemon-reload и systemctl restart app.service. Приложение читает пароль из файла, путь к которому пришёл в APP_DB_PASSWORD_FILE — обратите внимание, в окружении лежит путь, а не сам секрет. Если приложение умеет читать переменную вида *_FILE (а так умеет всё больше софта — почтовики, брокеры, экспортёры Prometheus, половина Go-демонов), то на этом задача закрыта.

Каталог /etc/credstore/ я выбрал не случайно. Если у LoadCredential= опустить путь и написать просто LoadCredential=dbpass, systemd сам поищет файл с таким именем в /etc/credstore/, /run/credstore/ и /usr/lib/credstore/ (этот поиск добавлен в systemd 251 — на более старых версиях путь указывайте явно) — это официально рекомендованные места хранения данных credentials на диске. Для зашифрованных вариантов ищутся /run/credstore.encrypted/, /etc/credstore.encrypted/ и /usr/lib/credstore.encrypted/. Сложить все секреты стенда в один каталог с правами 0700 root:root и обращаться к ним по имени — заметно опрятнее, чем плодить пути по всей файловой системе.

Проверять я советую до правки боевого юнита, transient-сервисом. Одна команда, ничего не устанавливается, ничего не остаётся:

systemd-run --wait --pipe --unit=cred-test \
  --property=User=app \
  --property=LoadCredential=dbpass:/etc/credstore/dbpass \
  /bin/sh -c 'ls -la $CREDENTIALS_DIRECTORY; systemd-creds list; cat $CREDENTIALS_DIRECTORY/dbpass'

systemd-creds list внутри службы показывает не только имена и размеры, но и состояние безопасности каждого credential: secure — лежит в неподкачиваемой памяти (ramfs), weak — в обычной памяти, insecure — «if having any access mode that is not 0400». Если вы видите insecure, кто-то в цепочке сломал режим доступа, и это повод остановиться и разобраться, а не идти дальше.

Если абсолютный путь в LoadCredential= указан и файла нет — служба не стартует, и это правильно. А вот если путь опущен и в credstore ничего не нашлось, отсутствие credential фатальным не считается: служба поднимется без секрета и упадёт позже, уже на подключении к базе. Диагностика в этом случае уводит в сторону минут на двадцать.
Как отдать пароль службе с User=app, если файл читает только root — схема
Схема к статье. Открыть схему в полном размере

Разбор: студия дизайна интерьера на 28 рабочих мест и один пароль в четырёх местах

Стенд условной студии дизайна интерьера «ИнтерьерГрад», 28 рабочих мест, которую мы взяли на обслуживание в прошлом году. Две виртуалки у облачного провайдера, Debian 13 (systemd 257): на первой PostgreSQL и внутренний портал проектов — карточки объектов, сметы, спецификации мебели и отделки для заказчиков; на второй вспомогательные задания — синхронизация смет с 1С, ночная подгрузка прайсов поставщиков мебели и материалов, резервное копирование базы. Шесть юнитов собственной разработки подрядчика плюс пара штатных. Один и тот же пароль от учётки crm_app в базе.

Что нашёл аудит конфигов. Пароль лежал в четырёх местах: строкой Environment=PGPASSWORD=… в двух unit-файлах, в /etc/crm/app.conf с правами 0644 (установочный скрипт портала при каждом обновлении честно возвращал эти права обратно), в скрипте /usr/local/bin/price-sync.sh и, отдельным изданием, в общей папке с документацией «чтобы не забыть». На машине жили пять локальных учёток: два админа, сервисный пользователь мониторинга и две учётки веб-студии, которая когда-то дорабатывала портал. Любая из них читала конфиг 0644 и получала systemctl show по D-Bus.

Отдельно доставила ротация. Меняли пароль в базе — правили в трёх местах из четырёх. Про price-sync.sh забывали дважды. Второй раз это заметили не сразу: подгрузка прайсов поставщиков молча падала по аутентификации три дня, скрипт писал ошибку в свой лог и возвращал ноль, мониторинг видел «задание отработало». Три дня дизайнеры собирали сметы для заказчиков по устаревшим ценам на мебель и плитку. Две сметы пришлось пересогласовывать, и разговор с руководителем студии был содержательный.

Переделка заняла около полутора часов, из них почти час — на инвентаризацию, где вообще встречается пароль. Схема получилась такая:

# /etc/systemd/system/crm-app.service.d/10-credentials.conf
[Service]
LoadCredential=pgpass:/etc/credstore/crm-pgpass
Environment=PGPASSFILE=%d/pgpass

# /etc/systemd/system/price-sync.service.d/10-credentials.conf
[Service]
LoadCredential=pgpass:/etc/credstore/crm-pgpass
ExecStart=
ExecStart=/usr/local/bin/price-sync.sh
Environment=PGPASSFILE=%d/pgpass

Файл /etc/credstore/crm-pgpass — обычный pgpass-формат, режим 0400 root:root, каталог 0700 root:root. libpq требует, чтобы файл пароля был не шире 0600 — а credentials как раз отдаёт 0400, так что дополнительных плясок не понадобилось. Из /etc/crm/app.conf строку с паролем убрали совсем, экспортный скрипт перестал знать пароль в принципе — он теперь просто psql, который сам подхватывает PGPASSFILE. Из папки с документацией строку вычистили, вместо неё положили ссылку на регламент ротации.

Итог через год эксплуатации: ротация пароля — это правка одного файла и systemctl restart двух юнитов, время операции меньше минуты. Обновление портала больше не возвращает 0644 на файл с секретом, потому что секрета в этом файле нет. Учётки веб-студии, которые мы, разумеется, тоже вычистили, при всём желании не прочитали бы ни конфиг, ни окружение. И главное — исчез класс инцидентов «забыли поменять в одном из мест».

Начинайте не с юнитов, а с grep. Прежде чем внедрять credentials, найдите все копии секрета: grep -rIl 'пароль' /etc /usr/local/bin /opt /root плюс systemctl show '*' -p Environment. Пока старые копии живы, новый красивый механизм ничего не защищает.
Цифры и версии: Разбор: студия дизайна интерьера на 28 рабочих мест и один пароль в четырёх местах — схема
Цифры и версии: Разбор: студия дизайна интерьера на 28 рабочих мест и один пароль в четырёх местах. Открыть схему в полном размере

Шифрованные credentials: когда 0400 root:root — уже мало

Права 0400 root:root защищают файл на работающей машине. Они ничего не защищают, когда каталог /etc целиком уезжает в ночной бэкап на NAS, когда с виртуалки снимают снапшот, когда конфиги стенда лежат в git-репозитории «для истории» или когда диск отправляется в утиль. Для всего этого есть LoadCredentialEncrypted= и утилита systemd-creds — обе появились в systemd 250 вместе с SetCredentialEncrypted=.

Шифрование симметричное, AES256-GCM, то есть с аутентификацией: подменить шифротекст незаметно не выйдет. Ключ берётся из одного из трёх источников — секрет, выведенный из чипа TPM2; ключ из файла /var/lib/systemd/credential.secret, доступный только root; либо комбинация обоих. Комбинация — режим по умолчанию, если TPM2 есть, а /var/lib/systemd/ лежит на постоянном носителе. Выбирается ключ флагом --with-key=, значения: host, tpm2, host+tpm2, null, auto, auto-initrd; -H и -T — короткие синонимы для host и tpm2.

Практический сценарий целиком, от пароля до готового drop-in — прямо из мануала systemd-creds(1), я им пользуюсь как есть:

mkdir -p /etc/systemd/system/crm-app.service.d
systemd-ask-password -n | ( echo "[Service]" && \
  systemd-creds encrypt --name=pgpass -p - - ) \
  > /etc/systemd/system/crm-app.service.d/50-password.conf
systemctl daemon-reload
systemctl restart crm-app.service

На выходе в drop-in лежит строка SetCredentialEncrypted=pgpass: и дальше base64-блоб. Да, секрет физически находится в unit-файле — и это нормально ровно потому, что он зашифрован и без ключа с этой машины бесполезен. Именно в этом разница между SetCredential= и SetCredentialEncrypted=: первый хранит значение буквально и годится только для несекретных вещей вроде идентификаторов и публичных ключей, второй безопасен для паролей. Если секрет удобнее держать отдельным файлом, шифруете в /etc/credstore.encrypted/ и подключаете через LoadCredentialEncrypted=.

Теперь честно о граблях, потому что их тут больше, чем кажется. Имя credential вшивается в шифротекст и при расшифровке сверяется с ID из LoadCredentialEncrypted= (а если --name= при шифровании не задан, имя берётся из имени выходного файла). Переименовали файл или ID в юните — расшифровка откажет. Лечится явным --name=, но узнаёшь об этом обычно в момент, когда служба не стартует. Привязка к машине означает, что восстановление из бэкапа на другой хост, клонирование ВМ и переезд к другому провайдеру ломают расшифровку — ключ-то остался на старой машине; для host-ключа спасает копирование /var/lib/systemd/credential.secret, для TPM2 не спасает ничего, и это by design. Для служб пользовательского менеджера шифровать нужно с флагом --user (или --uid=, оба появились в systemd 256), а для системных — без него: мануал прямо говорит, что пользовательский менеджер не сможет расшифровать секреты, предназначенные системе или другим пользователям. Есть ещё --not-after= — вшивает срок годности, после которого расшифровка перестанет работать; штука полезная для временных доступов подрядчиков, но включать её на боевом сервисе без календарного напоминания я бы не стал.

Моя позиция такая: --with-key=host по умолчанию, TPM2 — только там, где TPM реально есть, машина физическая и её никто не собирается клонировать. Привязка к TPM2 на виртуалке, которую регулярно восстанавливают из бэкапа, приносит больше простоя, чем безопасности. Спорно? Да. Но за 15 лет я разбирал последствия неподнявшегося после восстановления сервиса чаще, чем последствия украденного бэкапа.

Перед тем как включать шифрование с привязкой к TPM2, ответьте себе на один вопрос: как эта машина будет подниматься из бэкапа. Если ответа нет — берите --with-key=host и положите /var/lib/systemd/credential.secret в тот же защищённый бэкап, что и остальные ключи.

Если приложение не умеет читать секрет из файла

Идеальный случай — когда софт сам умеет в *_FILE или в файл пароля: PostgreSQL с PGPASSFILE, MySQL с !include и отдельным cnf, куча современных демонов с суффиксом _FILE у переменных. Тогда всё сводится к Environment=SOMETHING_FILE=%d/имя, и статью можно было бы закончить на предыдущем разделе. Но регулярно попадается легаси, которое читает пароль либо из своего конфига, либо из переменной окружения, и никак иначе.

Вариант первый, самый частый: обёртка, которая подставляет секрет в окружение непосредственно перед exec. Секрет при этом попадает в environ процесса — и это компромисс, о котором надо говорить вслух. Плюс в том, что через D-Bus окружение с секретом уже не утечёт: systemctl show покажет только исходную строку юнита, где никакого пароля нет. Минус в том, что /proc/PID/environ прочитает сам пользователь службы и root — то есть от честного root вы не защитились, но от остальных пятерых учёток на машине защитились полностью.

# в drop-in
[Service]
LoadCredential=dbpass:/etc/credstore/dbpass
ExecStart=
ExecStart=/bin/sh -c 'export APP_DB_PASS="$(cat "$CREDENTIALS_DIRECTORY/dbpass")"; exec /usr/bin/legacy-app --config /etc/legacy/app.conf'

Вариант второй: генерировать конфиг при старте из шаблона. Приложение читает свой обычный файл, а файл этот создаётся заново на каждый запуск в RuntimeDirectory с режимом 0700 и умирает вместе со службой. Credentials отдаются процессам ExecStartPre= начиная с systemd 252 (так написано в NEWS), поэтому на Debian 12, Ubuntu 24.04 и RHEL 9 %d и $CREDENTIALS_DIRECTORY там доступны, а на Ubuntu 22.04 (249) — нет, и генерацию придётся переносить в обёртку ExecStart=. Учтите ещё, что sed сломается, если в пароле есть символ | — для таких паролей шаблон лучше собирать envsubst или маленьким скриптом:

[Service]
User=app
LoadCredential=dbpass:/etc/credstore/dbpass
RuntimeDirectory=legacy-app
RuntimeDirectoryMode=0700
ExecStartPre=/bin/sh -c 'sed "s|@@PASS@@|$(cat $CREDENTIALS_DIRECTORY/dbpass)|" /etc/legacy/app.conf.tmpl > /run/legacy-app/app.conf'
ExecStart=/usr/bin/legacy-app --config /run/legacy-app/app.conf

Вариант третий — для софта, который умеет читать пароль из указанного файла, но не понимает переменных: прописать в его конфиге прямой путь /run/credentials/имя.service/dbpass. Мануал такой способ явно разрешает «in cases where no interpolation is possible, e.g. configuration files of software that does not yet support credentials natively», но предупреждает, что основным интерфейсом остаётся $CREDENTIALS_DIRECTORY — он работает и для пользовательских служб, где путь другой.

Вариант четвёртый, экзотический, но очень красивый: LoadCredential= умеет читать не только из файла, но и из AF_UNIX stream-сокета. Менеджер подключается к сокету один раз при запуске службы и забирает данные из соединения. То есть можно поднять собственный маленький брокер секретов, который на каждый запрос отдаёт свежий пароль — а чтобы он понимал, кто именно спрашивает, systemd передаёт в имени абстрактного сокета имя юнита и идентификатор credential, вида \0RANDOM/unit/foobar.service/credx, читается через getpeername(2). Это готовая точка интеграции с любым хранилищем, без агента внутри каждой службы.

Чего делать нельзя ни в одном из вариантов: писать секрет во временный файл в /tmp, echo-ить его в ExecStartPre без перенаправления (уедет в журнал целиком) и оставлять сгенерированный конфиг в /etc после остановки службы. RuntimeDirectory= придуман ровно для того, чтобы такой файл гарантированно исчезал.

Где ломают из раза в раз

Ошибка первая и самая коварная: относительный путь вместо абсолютного. LoadCredential=dbpass:credstore/dbpass — строка «credstore/dbpass» не абсолютный путь и не допустимое имя credential. Мануал описывает только два нефатальных случая: путь опущен или указан валидный идентификатор — тогда systemd ищет среди полученных им системных credentials и в credstore, а если ничего не нашёл, служба стартует без секрета. Относительный путь со слешем документированного поведения не имеет; на практике systemd такую строку отбрасывает с предупреждением при загрузке юнита, и итог тот же: служба стартует, каталог %d пустой, приложение падает уже на коннекте к базе с невнятным сообщением. Поэтому пишите только абсолютные пути и смотрите вывод systemctl daemon-reload. Вторая: правка юнита прямо в /lib/systemd/system — переживёт ровно до ближайшего apt upgrade. Третья: секрет в SetCredential= вместо SetCredentialEncrypted=. Мануал не оставляет тут пространства для трактовок: «Do not use this option for data that is supposed to be secret, as it is accessible to unprivileged processes via IPC».

Четвёртая — ExecStartPre=/bin/chmod 0644 на файле с секретом, добавленный «на пять минут, чтобы проверить». Пять минут — это единица измерения, в которой такие строки живут годами. Пятая — переименование зашифрованного .cred-файла после systemd-creds encrypt: имя вшито внутрь, decrypt откажется, служба не поднимется, а сообщение об ошибке про имя вы прочитаете только если полезете в журнал внимательно. Шестая — упереться в лимит 1 МБ, обычно на связке сертификатов или keystore, и долго искать причину не там.

Седьмая, инфраструктурная: восстановили машину из бэкапа или склонировали ВМ, а /var/lib/systemd/credential.secret не приехал или приехал другой. Все зашифрованные host-ключом credentials мертвы, все зависящие от них службы не стартуют. Восьмая — версионная. LoadCredential= есть с systemd 247, но самой утилиты systemd-creds на Ubuntu 22.04 (systemd 249) нет — она пришла в 250, — а значит нет и практичного способа шифровать; поиска по имени в credstore (251) и credentials в ExecStartPre= (252) там тоже нет. ImportCredential= с его глобами появилась только в 254 — на RHEL 9 (systemd 252) её нет, и копипаста из свежих статей там просто не заведётся. Ubuntu 24.04 — 255, Debian 13 — 257, в апстриме на момент написания актуален 261.

Набор команд, которым я разбираю такие вещи на месте:

systemd-analyze verify /etc/systemd/system/app.service.d/10-credentials.conf
systemctl show app.service -p LoadCredential -p LoadCredentialEncrypted \
  -p SetCredential -p User -p Environment
systemctl status app.service
journalctl -u app.service -b --no-pager | grep -i credential
ls -la /run/credentials/app.service/          # только под root
systemd-run --wait --pipe --property=User=app \
  --property=LoadCredential=dbpass:/etc/credstore/dbpass \
  /bin/sh -c 'systemd-creds list'

Первая строка ловит опечатки и директивы не в той секции, вторая показывает, что менеджер реально прочитал из юнита, последняя даёт живой ответ на вопрос «дошёл ли секрет до службы и в каком он состоянии». Если systemd-creds list внутри пробного запуска показал имя, размер и secure — механизм работает, и дальше проблема уже в приложении, а не в systemd.

Проверяйте версию до того, как выбрали механизм: systemctl --version. Половина вопросов «почему у меня не работает как в статье» — это systemd старше, чем директива из примера.

С чего начать и на что можно спокойно забить

Приоритет первый, и он же самый дешёвый: вынести пароли из unit-файлов и из конфигов с правами 0644. Это полдня работы на типовом стенде из трёх-пяти машин и закрывает самый массовый способ утечки — локального пользователя, который просто прочитал файл или дёрнул systemctl show. Никакого шифрования на этом этапе не нужно: файл 0400 root:root в /etc/credstore/ плюс LoadCredential= в drop-in уже переводят ситуацию из «секрет знает вся машина» в «секрет знает root и одна служба».

Приоритет второй: ротация. Соберите список, где какой секрет используется, и добейтесь, чтобы у каждого было ровно одно место хранения. Практическая польза от credentials не столько в криптографии, сколько именно в этом: пароль перестаёт размножаться по скриптам, конфигам и вики. У «ИнтерьерГрада» из разбора выше главный выигрыш был не в защите от подрядчиков, а в том, что ротация перестала быть операцией с четырьмя шансами что-нибудь забыть.

Приоритет третий, необязательный: шифрование через systemd-creds для тех секретов, которые физически покидают машину — уезжают в бэкап, в репозиторий конфигов, в образ ВМ. Для секретов, которые никуда не уезжают, шифрование добавляет ровно одну новую точку отказа и ноль защиты от того, кто уже стал root на этой машине. Это не значит «не шифруйте» — это значит «шифруйте осознанно, а не по инерции».

А теперь о том, на что можно забить. Полноценное хранилище секретов — HashiCorp Vault, OpenBao и подобные — для десятка сервисов на трёх машинах не нужно. Вы получите ещё один кластер, который надо мониторить, бэкапить, распечатывать и хранить unseal-ключи, обновлять и объяснять сменщику. Порог, после которого разговор про хранилище становится осмысленным, я для себя определяю так: больше нескольких десятков сервисов, несколько команд с разными правами, требование аудита «кто и когда получал этот секрет» или регулярная автоматическая ротация. Пока этого нет, credentials в systemd закрывают задачу целиком и бесплатно.

И трезвый взгляд на границы механизма. Credentials не защищают от root — root на машине читает всё, включая /run/credentials и credential.secret. Они не ротируют секреты и не ведут аудит обращений. Они не помогут, если приложение само пишет пароль в свой лог на уровне DEBUG. Это транспорт секрета от менеджера служб к процессу, сделанный правильно, — ровно то, чего не хватало годами, и ровно столько, сколько заявлено.

Не начинайте с TPM2 и хранилища секретов. Начинайте с того, чтобы пароль перестал лежать в четырёх местах — по моей практике это закрывает 80 % реального риска за один рабочий день.
Порядок действий: С чего начать и на что можно спокойно забить — схема
Порядок действий: С чего начать и на что можно спокойно забить. Открыть схему в полном размере

Частые вопросы

Нужно ли менять права на исходном файле с паролем?

Нет, и в этом весь смысл. Файл читает менеджер служб под root ещё до того, как процесс переключится на User=app, поэтому 0400 root:root на источнике сохраняется. Служба получает не доступ к исходному файлу, а его копию в своём каталоге с режимом 0400 и владельцем из User=.

Можно ли увидеть пароль в ps или через systemctl show?

Через ps — нет, если вы не передаёте секрет аргументом командной строки (так делать не надо). Через systemctl show — нет, там видна только строка LoadCredential= с путём. А вот при Environment=PASSWORD=… пароль отдаётся непривилегированным клиентам по D-Bus, и его прочитает любой локальный пользователь: именно поэтому переменные окружения для секретов не годятся.

Работает ли это с DynamicUser=yes?

Работает, и это один из самых удобных сценариев. Файл-источник не обязан быть доступен процессам службы вообще: менеджер читает его сам и делает копию, доступную динамическому UID. Так привилегированные данные попадают процессу под заранее неизвестным пользователем без раздачи прав кому бы то ни было ещё.

Что происходит с каталогом credentials после остановки службы?

Он живёт ровно столько, сколько запущена служба: каталог монтируется в контекст юнита при старте и убирается при остановке. Данные по возможности лежат в неподкачиваемой памяти (ramfs), то есть на диск в своп не попадают. Проверить состояние можно командой systemd-creds list изнутри службы — она покажет secure, weak или insecure.

У меня Ubuntu 22.04, там это заведётся?

Частично. В systemd 249 директива LoadCredential= есть (она с версии 247), а утилиты systemd-creds нет — она появилась в 250, поэтому шифрованные credentials на этой системе недоступны. Не будет и поиска по имени в /etc/credstore/ (с 251), и передачи credentials в ExecStartPre= (с 252) — путь пишите абсолютным, а подготовку конфига переносите в ExecStart=. ImportCredential= с глобами появилась в 254, а флаг systemd-creds encrypt --user — в 256. Перед копированием примеров из статей всегда смотрите systemctl --version.

Не проще ли сразу поставить Vault?

Для десятка сервисов на двух-трёх машинах — нет. Вы получите отдельный кластер, который нужно мониторить, бэкапить, обновлять и распечатывать unseal-ключи, ради задачи, которую systemd решает штатно и бесплатно. Разговор про хранилище становится осмысленным, когда появляются десятки сервисов, разные команды с разными правами, требование аудита обращений к секретам или автоматическая ротация.

Чем LoadCredential= лучше Environment= с точки зрения /proc?

При Environment=PASSWORD=… значение попадает в /proc/<pid>/environ процесса службы и всех её потомков, а сама строка юнита видна через systemctl show любому локальному пользователю. При LoadCredential= в окружении есть только $CREDENTIALS_DIRECTORY — путь к каталогу, а файл внутри доступен лишь пользователю службы и root. Если же вы в обёртке сами делаете export пароля перед exec, выигрыш по D-Bus сохраняется, а по environ — теряется, и это стоит понимать.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи