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.
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.
Operators managing local systemd services who want a safer first step than putting tokens directly into unit files or Environment= lines.
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.
Step 1: Check whether your host is a good fit
Start by checking the local systemd version:
systemctl --versionThen check whether the systemd-creds helper exists:
command -v systemd-credsYou 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-tokenThis 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.confUse a minimal drop-in like this:
[Service]
LoadCredential=api-token:/etc/myapp/creds/api-tokensystemd 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.shExample wrapper:
#!/usr/bin/env bash
set -euo pipefail
API_TOKEN="$(cat "${CREDENTIALS_DIRECTORY}/api-token")"
export API_TOKEN
exec /usr/local/bin/myappThen 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.serviceThen inspect logs carefully:
sudo journalctl -u myapp.service -bYou 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.serviceIf 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.
