Pass Secrets to systemd Services with Credentials Instead of Env Vars

Use systemd's credential mechanism for local services so secrets do not need to live directly in unit files or ordinary environment variables.

systemd credentialsLinux secretsService hygiene
Illustrated guide cover for Pass Secrets to systemd Services with Credentials Instead of Env Vars
systemd credentials • Linux secrets • Service hygiene
What you learn

How to check for credential support, load a secret into a service with LoadCredential=, read it from the service at runtime, and keep the first pattern minimal.

Best for

Operators managing local systemd services who want a safer first step than putting tokens directly into unit files or Environment= lines.

Risk to watch

The sloppy mistake is implying every distro and every app already supports this workflow the same way. You need a modern enough systemd and a service that can read the credential file.

Before you begin

  • A Linux host using systemd for the service you want to improve.
  • Root access to create secret files and service drop-ins.
  • A service you control closely enough to change how it reads the secret.
  • A willingness to keep the first example simple and version-aware.

systemd's credentials feature exists so units can receive small secret objects in a way that is more deliberate than baking them into ordinary environment variables. The systemd docs describe credentials as limited-size binary or textual objects that can be passed to units and accessed from a credential directory at runtime.

Version note: This guide assumes a reasonably modern systemd with credential support. Check your actual host before you promise this pattern to production. If your distro is far behind, use a different secret-delivery method instead of forcing an unsupported feature.

Step 1: Check whether your host is a good fit

Start by checking the local systemd version:

systemctl --version

Then check whether the systemd-creds helper exists:

command -v systemd-creds

You do not need to use encrypted credentials for the first pass, but the presence of the tooling is a good signal that the host is in the right era of systemd features. The important unit settings to know are LoadCredential=, SetCredential=, and their encrypted variants. This guide stays with LoadCredential= because it is easier to explain and audit.

Step 2: Place the secret on disk with tight permissions

Create a directory for the service's local credentials and write the secret file there:

sudo mkdir -p /etc/myapp/creds
sudo chmod 700 /etc/myapp/creds
printf '%s\n' 'replace-with-real-token' | sudo tee /etc/myapp/creds/api-token > /dev/null
sudo chmod 600 /etc/myapp/creds/api-token

This is not perfect secret management, but it is already better than copying the token into an Environment= line that may get echoed, templated, or handled more casually later.

Step 3: Add a credential-aware systemd drop-in

Create a drop-in for the service instead of editing the base unit directly:

sudo mkdir -p /etc/systemd/system/myapp.service.d
sudo nano /etc/systemd/system/myapp.service.d/credentials.conf

Use a minimal drop-in like this:

[Service]
LoadCredential=api-token:/etc/myapp/creds/api-token

systemd will make the credential available to the service under the credential directory for that unit. The key operational detail is that the service should read the file at runtime instead of expecting a plain environment variable to exist already.

Step 4: Read the credential in the service process

If the application can read a file path directly, that is the cleanest option. If not, use a tiny wrapper script you control:

sudo install -d -m 755 /usr/local/lib/myapp
sudo nano /usr/local/lib/myapp/start-with-credential.sh

Example wrapper:

#!/usr/bin/env bash
set -euo pipefail
API_TOKEN="$(cat "${CREDENTIALS_DIRECTORY}/api-token")"
export API_TOKEN
exec /usr/local/bin/myapp

Then point the service's ExecStart= at the wrapper if needed. Keep the wrapper small and avoid echoing the token or writing it into logs.

This is also the right place to stay honest about what credentials do not solve. If the app itself prints secrets in debug logs, this pattern does not save you from that.

Step 5: Verify behavior without leaking the secret

Reload and restart the unit:

sudo systemctl daemon-reload
sudo systemctl restart myapp.service
sudo systemctl status myapp.service

Then inspect logs carefully:

sudo journalctl -u myapp.service -b

You want to confirm the service started and can use the secret, while also confirming that the secret itself did not get printed. If you are testing inside a purpose-built service wrapper, you can also use systemd-creds from inside that context later, but the first pass should stay focused on service success and non-leakage.

Rollback and recovery notes

If the service fails after the change, remove the drop-in first and return to the old startup path:

sudo rm /etc/systemd/system/myapp.service.d/credentials.conf
sudo systemctl daemon-reload
sudo systemctl restart myapp.service

If the wrapper script caused confusion, point ExecStart= back to the original binary and debug the credential path offline. Do not keep a half-working secret loader in production.

Troubleshooting common credential mistakes

The unit restarts immediately after adding LoadCredential=.
The file path may be wrong, unreadable, or the app may not know how to consume the credential at runtime.

The app still expects an env var.
Use a small wrapper that reads from ${CREDENTIALS_DIRECTORY} and exports only what the app truly needs.

The distro seems too old for this workflow.
Stop and verify the local systemd feature level. This guide is only honest on hosts with suitable credential support.

You are tempted to treat this as a full secret manager.
Do not. This is a safer local-service delivery mechanism, not a complete secret-lifecycle platform.

What to do next: If you want to keep the service itself cleaner too, the next strong Linux-native step is hardening the unit sandbox. Pair this pattern with Harden a systemd Service with systemd-analyze security and Sandboxing.