Управление учётными данными в Systemd
В версии 247 systemd получил систему управления учётными данными. Она позволяет передавать чувствительную информацию от системы в юнит. Основной сценарий использования — передача конфигурации, паролей, приватных ключей и прочего подобного. Подробнее эта функциональность описана в systemd.exec(5).
В Release Note нововведение было описано следующим образом:
Появилась поддержка шифрованных и аутентифицированных учётных данных.
Она расширяет логику, появившуюся в v247, добавляя возможность
неинтерактивного симметричного шифрования и проверки подлинности.
Для этого используется ключ, который хранится в /var/ или в чипе
TPM2 (если он есть), или сразу оба источника (по умолчанию, если
TPM2 присутствует, применяется комбинированный вариант, иначе —
только ключ в /var/). Учётные данные автоматически расшифровываются
в момент запуска сервиса и предоставляются ему уже в открытом виде.
Новый инструмент `systemd-creds` позволяет заранее зашифровать данные
для такого использования, а новые параметры в файле сервиса —
LoadCredentialEncrypted= и SetCredentialEncrypted= — настраивают работу
с такими учётными данными.
Эта возможность полезна для того, чтобы хранить чувствительные вещи,
вроде SSL-сертификатов, паролей и подобного, в зашифрованном виде
«на покое», расшифровывая их только при необходимости — и так, чтобы
всё это было привязано к конкретной установке ОС или оборудованию.
Выглядит как минимум любопытно. Давайте посмотрим, как это работает на практике!
Как сказано в релизнотах, systemd-creds будет использовать чип TPM2, если он есть. Если его нет, используется секрет из /var/lib/systemd/credential.secret. Так что сначала проверим, включён ли у нас TPM. Это можно сделать, посмотрев содержимое файла:
cat /sys/class/tpm/tpm0/tpm_version_major
Если доступен TPM 2.0, вывод команды будет 2.
Сначала нужно зашифровать наши старые учётные данные. Это делается через systemd-creds. Команда принимает входной и выходной файл. Входной файл — наш старый, незашифрованный файл с секретом, выходной — уже зашифрованный:
systemd-creds encrypt secrets/old/token_unencrypted secrets/token
После этого остаётся только поменять настройку в нашем .service-файле (это пример сервиса для бота, который я гоняю на своём сервере):
[Unit] Description=My Service After=network-online.target [Service] ExecStart=/usr/bin/important-service Type=exec user=foo WorkingDirectory=/home/foo # Hardening PrivateDevices=true ProtectControlGroups=true ProtectSystem=full ProtectKernelTunables=true RestrictSUIDSGID=true PrivateTmp=true PrivateUsers=true ProtectClock=true ProtectHome=true ProtectKernelLogs=true ProtectKernelModules=true ProtectProc=noaccess # Everything is RO, we can only write to ReadWritePaths ProtectSystem=strict RemoveIPC=true UMask=0077 ProtectHostname=true NoNewPrivileges=true CapabilityBoundingSet= RestrictNamespaces=true RestrictAddressFamilies=AF_INET AF_INET6 # Secrets LoadCredentialEncrypted=token:/home/foo/secrets/token [Install] WantedBy=multi-user.target
Важно: ID токена (имя перед двоеточием) должен совпадать с именем файла секрета.
После этого вроде бы всё должно заработать, верно? Не совсем. Когда я впервые запустил юнит, он сразу же упал. Сможете угадать, в чём была проблема? Я даже приведу вам логи ошибки из юнита:
Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tcti-device.c:442:Tss2_Tcti_Device_Init() Failed to open specified TCTI device file /dev/tpmrm0: Operation not permitted Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tctildr-dl.c:154:tcti_from_file() Could not initialize TCTI file: libtss2-tcti-device.so.0 Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tcti-device.c:442:Tss2_Tcti_Device_Init() Failed to open specified TCTI device file /dev/tpm0: Operation not permitted Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tctildr-dl.c:154:tcti_from_file() Could not initialize TCTI file: libtss2-tcti-device.so.0 Jan 11 14:19:57 examplehost service[13245]: WARNING:tcti:src/util/io.c:252:socket_connect() Failed to connect to host 127.0.0.1, port 2321: errno 111: Connection refused Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tcti-swtpm.c:591:Tss2_Tcti_Swtpm_Init() Cannot connect to swtpm TPM socket Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tctildr-dl.c:154:tcti_from_file() Could not initialize TCTI file: libtss2-tcti-swtpm.so.0 Jan 11 14:19:57 examplehost service[13245]: WARNING:tcti:src/util/io.c:252:socket_connect() Failed to connect to host 127.0.0.1, port 2321: errno 111: Connection refused Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tctildr-dl.c:154:tcti_from_file() Could not initialize TCTI file: libtss2-tcti-mssim.so.0 Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tctildr-dl.c:254:tctildr_get_default() No standard TCTI could be loaded Jan 11 14:19:57 examplehost service[13245]: ERROR:tcti:src/tss2-tcti/tctildr.c:428:Tss2_TctiLdr_Initialize_Ex() Failed to instantiate TCTI Jan 11 14:19:57 examplehost service[13245]: ERROR:esys:src/tss2-esys/esys_context.c:69:Esys_Initialize() Initialize default tcti. ErrorCode (0x000a000a) Jan 11 14:19:57 examplehost systemd[13245]: Failed to initialize TPM context: tcti:IO failure Jan 11 14:19:57 examplehost systemd[13244]: sergeantbot-staging.service: Failed to set up credentials: Protocol error Jan 11 14:19:57 examplehost systemd[13244]: sergeantbot-staging.service: Failed at step CREDENTIALS spawning /srv/bot/prod/service: Protocol error
Проблема была в одном из параметров hardening. Конкретно — в PrivateDevices=true. Если эта опция включена, systemd даёт доступ только к очень ограниченному набору псевдоустройств (null, zero, pty). Так что делать это нужно по-другому. Один вариант — просто отключить эту опцию. Но мне такой подход не очень нравится. Другой вариант — использовать опцию DevicePolicy. В ней можно задать политику доступа к устройствам. Доступно два режима: strict и closed. closed разрешает доступ к псевдоустройствам, а strict полностью запрещает доступ ко всем устройствам. Поскольку доступ к определённым псевдоустройствам нас не беспокоит, поставим пока closed. Затем мы можем разрешить конкретные устройства через DeviceAllow. Просто найдите в логах устройство, на которое ругается сервис (в моём случае это tpm0 и tpmrm0), и явно разрешите доступ:
DevicePolicy=closed DeviceAllow=/dev/tpm0 DeviceAllow=/dev/tpmrm0
После этого перезапустите сервис, и всё должно заработать.
На этом наше небольшое погружение в systemd credentials можно закончить. Мне действительно нравится эта возможность, и я считаю, что это очень хороший способ передавать секреты в сервис (точно лучше, чем использовать переменные окружения!).