28

I would like to start a service using a systemd unit file. This service requires a password to start. I don't want to store the password in plaintext in the systemd unit file, because it is world-readable. I also don't want to provide this password interactively.

If I were writing a normal script for this, I would store the credentials in a file owned by root with restricted permissions (400 or 600), and then read the file as part of the script. Is there any particular systemd-style way to do this, or should I just follow the same process as I would in a regular shell script?

SauceCode
  • 2,275
  • 4
  • 21
  • 32
  • Related - https://serverfault.com/questions/413397/how-to-set-environment-variable-in-systemd-service – slm Aug 02 '18 at 15:02

5 Answers5

18

There are two possible approaches here, depending on your requirements. If you do not want to be prompted for the password when the service is activated, use the EnvironmentFile directive. From man systemd.exec:

Similar to Environment= but reads the environment variables from a text file. The text file should contain new-line-separated variable assignments.

If you do want to be prompted, you would use one of the systemd-ask-password directives. From man systemd-ask-password:

systemd-ask-password may be used to query a system password or passphrase from the user, using a question message specified on the command line. When run from a TTY it will query a password on the TTY and print it to standard output. When run with no TTY or with --no-tty it will use the system-wide query mechanism, which allows active users to respond via several agents

jasonwryan
  • 71,734
  • 34
  • 193
  • 226
  • 7
    Note that there are shortcomings to passing passwords in environment variables (even if the file where the variables are set is protected), since it's usually possible to snoop into environment variables of processes belonging to other users (e.g. using `ps ajxewww`) so someone might be able to pick it up from there. – filbranden Aug 02 '18 at 15:09
  • 1
    Thanks! `EnvironmentFile` entry with an absolute path to a file with 400 permissions works well. – levibostian Dec 09 '19 at 19:06
  • 4
    @filbranden You cannot read environment variables of processes belonging to other users. – woky Feb 13 '20 at 12:22
7

According to the systemd manual (systemd.exec, etc.), plain environment variables shouldn't be used for credential management. Systemd has an entire manual page describing its options, such as LoadCredential, LoadCredentialEncrypted, SetCredential, SetCredentialEncrypted. See https://systemd.io/CREDENTIALS/ and systemd.exec manual page for a full description of the proper way to do this in systemd.

Using environment variables means processes which don't need the credentials might still inherit or have access to the variables anyway, and could cause secrets to be exposed unintentionally. I wanted to have a systemd service integrate with Bitwarden CLI, and I used SetCredentialEncrypted following the instructions in https://systemd.io/CREDENTIALS/. After a couple of tweaks (bw expects the HOME environment variable to be set), my Bitwarden master password is encrypted at rest on disk, and only my service unit has access to it to unlock my Bitwarden account.

This is how I did it. First, I wrote my Bitwarden master password to a regular file, let's call it /tmp/bwmaster.pass. On retrospect it would have been better to write this on a ramdisk, instead of Btrfs, possibly choosing /run/ (all of these commands require root access). Next, I created an encrypted copy of my master password:

# systemd-creds encrypt --pretty --name=bwmp /tmp/bwmaster.pass -

This will pretty print a SetCredentialEncrypted=bwmp blurb which you can add to the [Service] section of your systemd service unit file (directly or as an override). Here's what my output looked like:

SetCredentialEncrypted=bwmp: \
        k6iUCUh0RJCQyvL8k8q1UyAAAAABAAAADAAAABAAAADCNQBSGbmpFUpRgngAAAAAgAAAA \
        AAAAAALACMA0AAAACAAAAAAfgAgCE+qUDoEpxT3155oWkncltAG6Wv+IQAEm7EwIhahpw \
        gAEB8dLosK741kyXWOWXQwLRfvhx6vDf7Na6JNGNW3PQkF6TZmJsUNYsWGXLgyIbgtjkd \
        0TkS0LfVOo/e6PQ32p/xfEj0b+ZGCXgtjC0Tx6sDedhU0fIfT2b+IlwBOAAgACwAAABIA \
        ID8H3RbsT7rIBH02CIgm/Gv1ukSXO3DMHmVQkDG0wEciABAAINgs66YvSM+PW+qPTnGE/ \
        8Yq67HHaX5XMmymUojmugUkPwfdFuxPusgEfTYIiCb8a/W6RJc7cMweZVCQMbTARyIAAA \
        AAWUZXjxS/vfx0BJq8RQAAyeXdSsf7ccsz+yEcMvoUm4WYZupWzt1FW93FDsUpdyi+QID \
        pqczUQ24ODMC+HP9vP7nvLHPF4uNk+iPkR5fF7Ypl5rHdcFyfm1Bqp70g

After I stored this in my service unit file, I securely deleted the temporary plaintext file:

# shred -uz /tmp/bwmaster.pass

See man shred for more details on this. When I was doing this, I was using Bitwarden to store my Borg encrypted backup credentials. I wrote the following service file:

[Unit]
Wants=network.target
After=network-online.target
[Service]
Type=oneshot
SetCredentialEncrypted=bwmp: \
        k6iUCUh0RJCQyvL8k8q1UyAAAAABAAAADAAAABAAAADCNQBSGbmpFUpRgngAAAAAgAAAA \
        AAAAAALACMA0AAAACAAAAAAfgAgCE+qUDoEpxT3155oWkncltAG6Wv+IQAEm7EwIhahpw \
        gAEB8dLosK741kyXWOWXQwLRfvhx6vDf7Na6JNGNW3PQkF6TZmJsUNYsWGXLgyIbgtjkd \
        0TkS0LfVOo/e6PQ32p/xfEj0b+ZGCXgtjC0Tx6sDedhU0fIfT2b+IlwBOAAgACwAAABIA \
        ID8H3RbsT7rIBH02CIgm/Gv1ukSXO3DMHmVQkDG0wEciABAAINgs66YvSM+PW+qPTnGE/ \
        8Yq67HHaX5XMmymUojmugUkPwfdFuxPusgEfTYIiCb8a/W6RJc7cMweZVCQMbTARyIAAA \
        AAWUZXjxS/vfx0BJq8RQAAyeXdSsf7ccsz+yEcMvoUm4WYZupWzt1FW93FDsUpdyi+QID \
        pqczUQ24ODMC+HP9vP7nvLHPF4uNk+iPkR5fF7Ypl5rHdcFyfm1Bqp70g
Environment=BWMPATH=%d/bwmp
ExecStart=/usr/local/sbin/borg.sh

There's a little more to my borg.service file, but I only included the important parts for this topic. According to the manual, there's supposed to be a $CREDENTIAL_PATH/bwmp file that can be referenced within the script, but I couldn't get it working until I also defined the BWMPATH environment variable as I did above. That may have been because the bw command (Bitwarden CLI) tool expects the HOME variable to be set, so I set it in my script (only root will run borg.sh).

Please see the systemd.exec and https://systemd.io/CREDENTIALS manuals for the specifics on how this works. The systemd-creds command will automatically use a TPMv2 chip if it is available in your system, along with the key file. I'm not sure if my system has a TPMv2 chip, it's something I will explore later.

This worked for me on Arch Linux, systemd 251.3-1 (which hasn't been updated in a week or so). If you're using a different version this may not exist.

Trey Blancher
  • 81
  • 1
  • 3
  • Do I understand correctly that it requires a service to be able to read the credentials file instead of a value being set from env? In that case, most services are not ready for this and require ENV to be set. Does anyone have a good workaround? Or is the only solution, wrapping all startup scripts to read the cred file and set ENV vars? – oglop May 31 '23 at 04:52
  • In my script, _/usr/local/sbin/borg.sh_, I have the following statement: ```if BW_SESSION=$(bw unlock --passwordfile "$BWPATH" --raw); then``` BWPATH is not defined inside this script, it is inherited from the environment set by the systemd service unit. Any scripts which rely on this credential need to be modified to look for the credential in this path. – Trey Blancher Jun 05 '23 at 21:22
  • Can this work with user defined services? – James Schinner Aug 27 '23 at 09:17
  • I don't see why it can't be used with user-defined services, they're just unit files. If your system has a TPM2 chip, it may not be able to use it without root access (I admit I don't know a whole lot about TPM2). It looks like `systemd-creds` can be run as a regular user, so it should work. – Trey Blancher Aug 29 '23 at 01:17
6

There's a third alternative to this as well as the 2 suggestion by @jasonwryan.

excerpt from Michael Hampton's answer at ServerFault - How to set environment variable in systemd service?

The current best way to do this is to run systemctl edit myservice, which will create an override file for you or let you edit an existing one.

In normal installations this will create a directory /etc/systemd/system/myservice.service.d, and inside that directory create a file whose name ends in .conf (typically, override.conf), and in this file you can add to or override any part of the unit shipped by the distribution.

For instance, in a file /etc/systemd/system/myservice.service.d/myenv.conf:

[Service]
Environment="SECRET=pGNqduRFkB4K9C2vijOmUDa2kPtUhArN"
Environment="ANOTHER_SECRET=JP8YLOc2bsNlrGuD6LVTq7L36obpjzxd"

Also note that if the directory exists and is empty, your service will be disabled! If you don't intend to put something in the directory, ensure that it does not exist.

Jeff Schaller
  • 66,199
  • 35
  • 114
  • 250
slm
  • 363,520
  • 117
  • 767
  • 871
  • 3
    Note that the content of drop-in config files `/etc/systemd/system/myservice.service.d/*.conf` is visible for every user (see `systemctl show myservice.service | grep Environment=`) – canochordo Sep 25 '21 at 21:44
6

Just to mention this, systemd 274 has support for special in memory credential files, see the LoadCredential= and LoadCredentialEncrypted= settings in the systemd.exec(5) manual page which applies also to .service units.

Toby Speight
  • 8,460
  • 3
  • 26
  • 50
eckes
  • 226
  • 2
  • 8
  • Just a BTW, sssd had a credential store which was bound to uid, so that would be a fine tool for this, but it is removed in later versions. – eckes Mar 01 '22 at 16:44
3

I can offer an additional alternative that may suit your needs but it requires several preconditions to be met:

The next steps are as follow:

Use secret-tool from libsecret to store your password. For example:

$ secret-tool store --label=myProgram myService password
Password: <type it here>

If your executable supports reading the password from an environmental variable, it's better:

[Service]
ExecStart=/usr/bin/sh -c 'env SECRET=$(secret-tool lookup myService password) /usr/bin/script'

If your executable accepts the password as an argument, you can still use secret-tool like so:

[Service]
ExecStart=/usr/bin/sh -c '/usr/bin/script --secret=$(secret-tool lookup myService password)'

Caution: the password will be plain visible when you will run systemctl --user status myUnit.service since it shows the argument as being run on the command line. This means this will also be visible for users running top or ps -aux.

Doron Behar
  • 673
  • 8
  • 25
  • Can this ensure a running user process can no longer request the credential after service start? It would be better if the secret tool can only be used once per user/session/cred – eckes Apr 01 '21 at 21:18
  • Note that this alternative is not appropriate for systemd units that need to run as root, as my answer above indicates. As for limiting the credential to just when the unit is starting, you could probably post an empty `Environment=` just before an `ExecStartPost=/path/to/some/no_op.script`, but that's just a guess. – Trey Blancher Jun 05 '23 at 21:26