How to Encrypt and Pass Service Credentials in Linux Using systemd-creds

Managing database passwords, API access tokens, and private SSL keys in systemd service units traditionally forced administrators to store plain text secrets in world-readable unit files, insecure environment variable files, or complex third-party vault daemons. Introduced in systemd 250, the systemd-creds utility provides a native, cryptographic mechanism to encrypt, seal, and inject confidential credentials directly into systemd services. Credentials can be locked to the host’s specific machine identity, a symmetric key, or cryptographically bound to hardware TPM2 (Trusted Platform Module) chips.

In this guide, you will learn how to verify TPM2 support, encrypt plaintext secrets, configure LoadCredentialEncrypted= in systemd service files, and read credentials inside running daemons.

Understanding systemd Credentials Architecture

The systemd credentials framework operates on a clean separation of roles:

  • Encryption at Rest: Secrets are encrypted ahead of time into base64-encoded strings or standalone credential files.
  • Hardware Binding: Credentials can be sealed against TPM2 PCR (Platform Configuration Register) policies, ensuring that if a hard drive or cloud image is extracted and booted on another machine, the secret cannot be decrypted.
  • Runtime Injection: When systemd starts the service, the service manager decrypts the secret in memory and mounts it into a private, temporary RAM filesystem ($CREDENTIALS_DIRECTORY) accessible only by that service’s UID.
  • Automatic Purging: When the service stops, the RAM filesystem is unmounted and securely wiped, ensuring secrets never linger on persistent storage.

Checking TPM2 and Host Key Cryptographic Capabilities

Before sealing secrets, inspect your system’s cryptographic capabilities using systemd-creds has-tpm2:

systemd-creds has-tpm2

If your hardware or virtual machine includes an active TPM 2.0 cryptoprocessor, the command outputs yes. If the output is no, systemd-creds automatically falls back to host-specific symmetric keys derived from /etc/machine-id.

Encrypting a Plaintext Secret

You can encrypt a secret string directly from the terminal. For example, to encrypt a database password named db-password:

echo -n "MySuperSecretPassword123!" | systemd-creds encrypt --name=db-password - /etc/credstore.encrypted/db-password.cred

To seal the secret specifically to the hardware TPM2 chip so it cannot be decrypted on any other motherboard:

echo -n "MySuperSecretPassword123!" | systemd-creds encrypt --with-key=tpm2 --name=db-password - /etc/credstore.encrypted/db-password.cred

Inspect the generated credential file:

cat /etc/credstore.encrypted/db-password.cred

The file contains a standardized, base64-encoded ASCII payload containing encryption headers, TPM2 policies, and the encrypted ciphertext.

Verifying and Decrypting Credentials Interactively

To confirm that your host can successfully decrypt the sealed credential file, run systemd-creds decrypt:

systemd-creds decrypt /etc/credstore.encrypted/db-password.cred

The utility outputs the original plaintext secret to standard output. If you copy this file to another system or attempt decryption without the proper host key or TPM, systemd immediately rejects the operation with an authentication error.

Configuring a Systemd Service to Consume Encrypted Credentials

To pass the encrypted secret to a daemon, configure your service unit file using the LoadCredentialEncrypted= directive.

Create or edit your custom unit file (for example, /etc/systemd/system/myapp.service):

[Unit]
Description=Production API Microservice
After=network.target

[Service]
Type=simple
User=appuser
Group=appuser
ExecStart=/usr/local/bin/myapp-daemon

# Load and decrypt the credential automatically on startup
LoadCredentialEncrypted=db-password:/etc/credstore.encrypted/db-password.cred

# Security hardening
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes

[Install]
WantedBy=multi-user.target

Reload systemd manager configuration to register the unit:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service

Reading Credentials Inside the Application

When the service launches, systemd sets the $CREDENTIALS_DIRECTORY environment variable pointing to a private ramfs directory (usually /run/credentials/myapp.service/).

Inside the directory, systemd exposes each decrypted secret as a standard file named after the credential key:

# The decrypted secret path
$CREDENTIALS_DIRECTORY/db-password

In Python, reading the decrypted secret is straightforward:

import os
from pathlib import Path

cred_dir = os.environ.get("CREDENTIALS_DIRECTORY")
if cred_dir:
    db_password_file = Path(cred_dir) / "db-password"
    db_password = db_password_file.read_text().strip()
else:
    raise RuntimeError("Missing systemd credentials directory")

In shell scripts or other binaries, you can inspect the file directly:

DB_PASS=$(cat "$CREDENTIALS_DIRECTORY/db-password")

Because the credential exists only in transient kernel memory, it is never exposed in process listings (ps aux), shell history, or persistent logs.

Troubleshooting Credential Access Failures

If your service fails to start with a credential loading error:

  • Journalctl Inspection: Inspect systemd’s decrypt logs using journalctl:
    sudo journalctl -u myapp.service -e
    Look for Failed to load encrypted credential.
  • Filesystem Permissions: Ensure the user running systemd (PID 1 / root) has read access to the encrypted file in /etc/credstore.encrypted/.
    sudo chmod 644 /etc/credstore.encrypted/db-password.cred
  • TPM PCR State Drift: If you sealed credentials against specific PCR registers and performed a firmware update or kernel upgrade, the PCR values change, preventing automatic unsealing. Reseal the credential after verified system updates.

Get the best tech tips delivered straight to your inbox.

Join thousands of readers mastering Apple, Google, Microsoft, and Linux.