DEV Community

Lyra
Lyra

Posted on

Stop Using .env for Linux Services: Safer Secrets with systemd Credentials

Stop Using .env for Linux Services: Safer Secrets with systemd Credentials

If your Linux service still loads API keys from Environment= or .env, you're carrying avoidable risk.

Environment variables are convenient, but they’re not designed as a secure secret-delivery mechanism. Linux exposes a process’s initial environment via /proc/<pid>/environ (subject to permissions), and environment values can spread through child processes.

systemd credentials give you a better pattern:

  • Secret material is delivered as files in a runtime credential directory
  • Access is scoped to the service
  • You can pass encrypted credentials with systemd-creds
  • Your unit no longer hardcodes cleartext secrets

This guide is a full, practical migration.


Why move away from .env for secrets?

From Linux proc_pid_environ(5), /proc/<pid>/environ contains the initial environment passed at exec time. That means secrets in env vars are easier to expose accidentally during debugging, process inspection, or inherited execution paths.

systemd credentials are explicitly designed for sensitive data delivery to services.


Prerequisites

  • A Linux host with systemd (check with systemctl --version)
  • systemd-creds available (usually packaged with systemd)
  • Root/sudo access

Check:

systemctl --version
systemd-creds --version
Enter fullscreen mode Exit fullscreen mode

Step 1) Create a demo service user + app directory

sudo useradd --system --home /opt/demo-secrets --shell /usr/sbin/nologin demo-secrets || true
sudo install -d -o demo-secrets -g demo-secrets /opt/demo-secrets
Enter fullscreen mode Exit fullscreen mode

Create a minimal script that reads a credential file path passed by systemd:

sudo tee /opt/demo-secrets/app.sh >/dev/null <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# systemd will place credential files under $CREDENTIALS_DIRECTORY
TOKEN_FILE="${CREDENTIALS_DIRECTORY:?missing}/api-token"

if [[ ! -f "$TOKEN_FILE" ]]; then
  echo "Token file missing: $TOKEN_FILE" >&2
  exit 1
fi

# Demo output: only show length, never print secret
TOKEN_LEN=$(wc -c < "$TOKEN_FILE")
echo "Credential loaded successfully (bytes=$TOKEN_LEN)"
EOF

sudo chown demo-secrets:demo-secrets /opt/demo-secrets/app.sh
sudo chmod 0750 /opt/demo-secrets/app.sh
Enter fullscreen mode Exit fullscreen mode

Step 2) Store secret outside the unit file

Create a plaintext secret file (for initial migration):

sudo install -d -m 0750 /etc/demo-secrets
printf '%s' 'replace-with-real-token' | sudo tee /etc/demo-secrets/api-token >/dev/null
sudo chmod 0640 /etc/demo-secrets/api-token
sudo chown root:root /etc/demo-secrets/api-token
Enter fullscreen mode Exit fullscreen mode

Step 3) Define service with LoadCredential=

sudo tee /etc/systemd/system/demo-secrets.service >/dev/null <<'EOF'
[Unit]
Description=Demo service using systemd credentials
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
User=demo-secrets
Group=demo-secrets
ExecStart=/opt/demo-secrets/app.sh

# credential-id:source-path
LoadCredential=api-token:/etc/demo-secrets/api-token

# Basic hardening
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes

[Install]
WantedBy=multi-user.target
EOF
Enter fullscreen mode Exit fullscreen mode

Reload + run:

sudo systemctl daemon-reload
sudo systemctl start demo-secrets.service
sudo systemctl status --no-pager demo-secrets.service
Enter fullscreen mode Exit fullscreen mode

Check logs:

journalctl -u demo-secrets.service --no-pager -n 20
Enter fullscreen mode Exit fullscreen mode

You should see Credential loaded successfully (...).


Step 4) Verify credential location and behavior

systemd exposes credentials via a runtime directory ($CREDENTIALS_DIRECTORY) for the service. Your app reads files from there (not environment variables).

To inspect within an interactive transient unit:

sudo systemd-run --wait --pipe \
  -p LoadCredential=api-token:/etc/demo-secrets/api-token \
  /bin/sh -lc 'echo "$CREDENTIALS_DIRECTORY"; ls -l "$CREDENTIALS_DIRECTORY"; wc -c "$CREDENTIALS_DIRECTORY/api-token"'
Enter fullscreen mode Exit fullscreen mode

Step 5) Encrypt credentials at rest with systemd-creds

Instead of keeping plaintext secret files, encrypt them for host-bound usage.

printf '%s' 'replace-with-real-token' | sudo systemd-creds encrypt - /etc/demo-secrets/api-token.cred
sudo chmod 0640 /etc/demo-secrets/api-token.cred
sudo chown root:root /etc/demo-secrets/api-token.cred
Enter fullscreen mode Exit fullscreen mode

Update the service to use encrypted input:

LoadCredentialEncrypted=api-token:/etc/demo-secrets/api-token.cred
Enter fullscreen mode Exit fullscreen mode

Apply change:

sudo systemctl daemon-reload
sudo systemctl restart demo-secrets.service
sudo systemctl status --no-pager demo-secrets.service
Enter fullscreen mode Exit fullscreen mode

Step 6) Rotate secret safely

When rotating, write new value, encrypt, and restart the unit:

printf '%s' 'new-rotated-token' | sudo systemd-creds encrypt - /etc/demo-secrets/api-token.cred
sudo systemctl restart demo-secrets.service
Enter fullscreen mode Exit fullscreen mode

For production rollouts, pair this with a maintenance window or health check + rollback flow.


Migration checklist (real services)

  • [ ] Remove secret values from Environment= and .env files
  • [ ] Move secret inputs to LoadCredential= / LoadCredentialEncrypted=
  • [ ] Update app code to read from $CREDENTIALS_DIRECTORY/<id>
  • [ ] Ensure logs never print secret values
  • [ ] Restrict service permissions (NoNewPrivileges, ProtectSystem, etc.)
  • [ ] Document rotation runbook

Common gotchas

  1. Wrong credential ID/file mismatch

    • LoadCredential=name:path must match app filename under $CREDENTIALS_DIRECTORY/name.
  2. App still expects env vars

    • Add a small startup shim that reads credential file and exports internally only if absolutely necessary.
  3. Permissions confusion

    • Source file readability is handled by systemd at start, then projected into credential dir for the service.
  4. Printing secrets while debugging

    • Never cat secret values in journals. Log hashes/lengths only.

Final thought

This is one of those upgrades that reduces risk without adding operational pain. Once you switch to systemd credentials, secret handling becomes explicit, auditable, and less fragile than .env-driven service configs.

If you’re already using systemd units in production, this is low-effort, high-impact hardening.


References

Top comments (0)