Skip to content

feat(plugins/modules): add systemd_credential - #521

Merged
NavidSassan merged 1 commit into
mainfrom
feat/systemd-credential
Oct 28, 2026
Merged

NavidSassan merged 1 commit into
mainfrom
feat/systemd-credential

Conversation

@NavidSassan

Copy link
Copy Markdown
Member

Adds linuxfabrik.lfops.systemd_credential, a module that stores a secret as a systemd credential, as groundwork for keeping secrets that LFOps deploys out of plain configuration files. No role uses it yet; mariadb_server will be the first consumer in a follow-up.

  • systemd >= 251 (RHEL 9/10, Debian 12/13, Ubuntu 24.04/26.04): encrypted with systemd-creds into /etc/credstore.encrypted/<name>, where LoadCredentialEncrypted=<name> finds it without a path (systemd NEWS 251).
  • Older systemd (RHEL 8: 239, Ubuntu 22.04: 249, which has no systemd-creds): plain file in /etc/credstore/<name>. Consumers read ${CREDENTIALS_DIRECTORY:-/etc/credstore}/<name> and work with both.
  • The version of the running service manager decides (systemctl show --property=Version), not the installed client.
  • Idempotent: systemd-creds encrypt produces a different file on every call, so the module decrypts the existing credential on the host and compares it in memory. It re-encrypts only on a difference or when decryption fails (lost host key, VM restored without /var/lib/systemd).
  • Switching stores (e.g. RHEL 8 -> 9) removes the copy in the other store; since systemd 252 the encrypted store is searched first.
  • community.general.systemd_creds_encrypt / _decrypt were not used: they need community.general >= 10.2.0 (LFOps pins < 9.0.0 for Python 3.6 on RHEL 8), and they are stateless, so a role would round-trip the plaintext to the controller and need four tasks per secret.

Security (reviewed by a separate read of the code, all findings fixed):

  • The secret goes to systemd-creds on stdin, never into argv; the module never returns it (content is no_log). No occurrence in -vvv output.
  • Store directories are forced to root:root 0700, credentials to root:root 0600, also when the content already matches. A symlink or non-directory in place of the store fails the module; a symlink, FIFO or other non-regular file in place of the credential is replaced, not followed.
  • Writes go through a temporary file in the store, fsynced, renamed and the directory fsynced, so a crash cannot leave an empty credential. The ciphertext comes back on stdout and is written by the module, instead of systemd-creds' own temp file with mode 0666 & ~umask; os.rename() instead of atomic_move(), which gives new files 0666 & ~umask.
  • Names are restricted to [A-Za-z0-9_.-], no leading dot, full match (no trailing newline). Empty content and non-root runs are rejected.
  • Documented: with a TPM2 chip the credential is also bound to PCR 7, so after a Secure Boot update the service fails to start until the module re-encrypts.

Verified:

  • Unit tests (25, incl. idempotence, check mode, migration, symlinks, FIFO, wrong modes, encrypt failure keeping the old credential) on Python 3.6 (UBI 8, py36-target) and 3.12 / ansible-core 2.18; test_plugin_docs passes.
  • Containers via the podman connection: Rocky 8 (systemd 239, plain), Rocky 9 (252, encrypted, read back by a unit with LoadCredentialEncrypted=), Ubuntu 22.04 (249, plain): create, idempotence, check mode, content change, modes, lost host key (re-encrypts, then idempotent), plain-to-encrypted migration, absent, invalid name.

@NavidSassan
NavidSassan force-pushed the feat/systemd-credential branch from 1d398ea to 0c8c658 Compare October 28, 2026 10:40
@NavidSassan
NavidSassan merged commit 500bf37 into main Oct 28, 2026
13 checks passed
@NavidSassan
NavidSassan deleted the feat/systemd-credential branch October 28, 2026 10:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant