Systemd services should allow running commands as root

The systemd.service keys like ExecStartPre etc accept special prefixes to, for example, run a command as root even though the service runs as a user.

Systemd service units are implemented here. As you can see, there’s no way to specify that the script commands run as root instead of the User (other than making your own script and going via serviceConfig).

→ What would be a good way of configuring that?

Also note that we don’t have a way to say that a unit should run as a certain user, apart from passing the username to serviceConfig.User, which is a bit of a hack IMHO.

→ Would it not be better to have a user config key that accepts a real config.users object, which means it can be checked for existence?

Just an opinion but… I think we should continue to not offer a convenient way to run commands as root for systemd services which don’t run as root. I don’t like the idea of promoting running preStart scripts as root when there are better ways to achieve the same results. In the few cases where there are no better alternatives having to use a special prefix seems like an acceptable cost.

1 Like

Point in case for me: I need to give read access to nginx for an uploads dir of an app hosted in a app-user-homedir. This means chmod a+x ~appuser but I often forget.

This is basic “activation script” work. How should I do this better?

Web servers sometimes fall into that “few cases” category so much that someone made a services.nginx.preStart where you could add your command and it would run as root. My guess, which could very well be wrong, is that this is often used to do things imperatively, which goes against the nix model.

Aside from that you might consider systemd.tmpfiles.rules to solve your problem in a declarative way.

Ah, thanks for the pointer, I’ll have a look.

I think the fact that services.nginx.preStart exists, means that there should have been a rootPreStart in systemd.services so that all servers can benefit…

I have a strong feeling, based on conversation with the author of that option, that the option is using being used to do very imperative things. I think as a community we need to consider when, why, and how often we want to promote doing things imperatively. It isn’t that we need to make life harder for people who want to do things imperatively, it is that we want to keep nixpkgs “pure” and promote best practices (ie. doing things declaratively). I don’t speak on behalf of anyone, so let me clarify at this point this is just my opinion :smile:

If we take the time to go through the specific case of services.nginx.preStart:

test -d ${cfg.stateDir}/logs || mkdir -m 750 -p ${cfg.stateDir}/logs
test `stat -c %a ${cfg.stateDir}` = "750" || chmod 750 ${cfg.stateDir}
test `stat -c %a ${cfg.stateDir}/logs` = "750" || chmod 750 ${cfg.stateDir}/logs
chown -R ${cfg.user}:${cfg.group} ${cfg.stateDir}

Roughly speaking, I would have added something like the following to the module instead of a preStart option:

systemd.tmpfiles.rules = [
  "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}"
  "d '${cfg.stateDir}/logs' 0750 - ${cfg.user} ${cfg.group}"
];
1 Like

And that’s a great alternative, and clearly superior (apart from even more tying to systemd). I didn’t really know about tmpfiles; it would be great if there was a nice declarative interface for it that doesn’t involve string interpolation.

Maybe a map of dirs with optional perms/user/group?

This has been discussed a few times so far. I can’t seem to recall why we haven’t done anything about that yet… :thinking:

It’s quite complex.

So those systemd.tmpfiles.rules are in the tmpfiles.d format, which has 32 types for the rules.

Furthermore, it’s not clear when these rules are executed in NixOS. There are “remove” types, but when are those removed? Ideally, there would be tmpfiles rules per service.

Furthermore, I can’t exactly specify what I want. In my case, I need to make sure that all parent directories of the uploads directory have at least execute permission. With tmpfiles, you can only specify absolute permissions, not relative.

So maybe it would be better if tmpfiles was replaced with some simplified rules that translate into ExecStartPre and ExecStopPost. For example, a way to configure that a directory should exist and be accessible by the service user.

Yes quite complex, and powerful.
The remove rules have a configurable time parameter. In general though every NixOS system rebuild calls the tmpfiles.d service, on top of already scheduled calls.

Interesting choice of words … “exactly”. You can’t exactly specify what you want, but what you want isn’t exact. Saying chmod +x usually (though not always) implies you have done something imperative somewhere. If it doesn’t imply an imperative action then it likely implies less service separation/sandboxing than may be desired. But I digress… you do have a fair point in that tmpfiles doesn’t handle the chmod +x scenario.

In your case what directory are you trying to create? As long as you operate on root owned base directories tmpfiles should automatically create the needed parent folders with acceptable access. I would really like to get to the bottom of your problem and either solve it with tmpfiles or acknowledge and address the shortcoming.

1 Like

In my case, I deploy applications in a separate user account with all the state. I use nginx to proxy the application and its assets. To get access to the assets, the user directory needs to have an ACL u:nginx:x so nginx can access the subdirectory in question.

For example, the assets could be in /home/awesomeapp/www/build/ and the awesomeapp dir needs +x for at least the nginx user.

Here is an example that does what you want. In that case I wanted to provide a secret token to the service, without having it in the /nix/store, without other services having access to it, and to complicate things without needing to allocate a fixed user.

  1. Set DynamicUser = true allocates a random UID/GID on service start.
  2. A RumtimeDirectory is provided to the service.
  3. The preStart script copies the token into the $RUNTIME_DIRECTORY.
  4. Because the service is running as a user, set PermissionsStartOnly = true to run the preStart script as root.

Here is the full script:

{
  systemd.services.github-actions-nixpkgs-fmt = {
    description = "Github Actions runner for nixpkgs-fmt";
    wantedBy = [ "multi-user.target" ];
    after = [ "network.target" ];

    preStart = ''
      cp \
        /run/keys/github-actions-nixpkgs-fmt-token \
        "$RUNTIME_DIRECTORY"/github-actions-nixpkgs-fmt-token
      chmod 644 "$RUNTIME_DIRECTORY"/github-actions-nixpkgs-fmt-token
    '';

    # https://github.com/actions/runner/blob/53d632706dc8316a1cf8bbaf98833e0912afcbbd/src/Runner.Listener/CommandSettings.cs
    serviceConfig = {
      PermissionsStartOnly = true;
      DynamicUser = true;
      RuntimeDirectory = "github-actions-nixpkgs-fmt";
      ExecStart = pkgs.writeShellScript "github-actions-nixpkgs-fmt" ''
        set -euo pipefail

        # Load registration token
        token=$(< $RUNTIME_DIRECTORY/github-actions-nixpkgs-fmt-token)
        rm $RUNTIME_DIRECTORY/github-actions-nixpkgs-fmt-token

        # Unpack archive
        cp --no-preserve=owner -R ${actions-runner}/* $RUNTIME_DIRECTORY"
        cd "$RUNTIME_DIRECTORY"

        # Register
        ./with-deps.sh ./config.sh \
          --url https://github.com/nix-community/nixpkgs-mt \
          --token "$token" \
          --unattended \
          --replace

        # Exec
        exec ./with-deps.sh ./run.sh
      '';
    };
  };
}
3 Likes

Hi ya’ll, I’m trying to run a service that executes a script as root. Having no luck.

I’m trying to make a test script to blink the builtin LED on a Raspberry Pi 4. Only root has access to these kinds of devices.

This is my sd-image.nix, root-ssh.nix just has some mDNS and SSH keys inside.

{ config, pkgs, ... }:{

  imports = [
    <nixpkgs/nixos/modules/installer/cd-dvd/sd-image-raspberrypi4.nix>
    ./root-ssh.nix
  ];

  config.networking.hostName = "mo";

  config.users.users.sprout = {
    isNormalUser = true;
    extraGroups = [ "wheel" ];
  };
  
  config.networking.wireless.enable = false;

  config.systemd.services.led-test = {
    wantedBy = [ "multi-user.target" ]; 
    after = [ "network.target.service" ];
    description = "Test LED";
    serviceConfig = {
      Type = "simple";
      User = "root";
      Group = "root";
      PermissionsStartOnly = true;
      DynamicUser = true;
      ExecStart = pkgs.writeScriptBin "blink" ''
        #!${pkgs.stdenv.shell}
        echo none > /sys/class/leds/led0/trigger
        while true; do
          echo 1 > /sys/class/leds/led0/brightness
          sleep 0.5
          echo 0 > /sys/class/leds/led0/brightness
          sleep 0.5
        done
      '';

    };
  };
}

Pretty sleep deprived, so I’ve just slapped a bunch of things into my service - though it sounds like there isn’t actually a way to do this? (I event tried to sneak in the ExecStart ‘+’ → ExecStart=+/nix/store/z7455b2injbwy3l6kjqq5kw00fpl56fw-blink)

Any thoughts? I just want some blinkin lights :frowning:

Perhaps leave of the dynamicuser? You want it to run as root after all

   config.systemd.services.led-test = {
     wantedBy = [ "multi-user.target" ];
     after = [ "network.target" ];
     description = "Test LED";
     script = ''
       echo none > /sys/class/leds/led0/trigger
       while true; do
         echo 1 > /sys/class/leds/led0/brightness
         sleep 0.5
         echo 0 > /sys/class/leds/led0/brightness
         sleep 0.5
       done
     '';
   };

Why do you need to order this after network.target though?

Ohhh I see exactly what was happening now. Thanks!

When including the script inside of ExecStart, the result is that the path of the derivation is passed back to ExecStart - but not the path to the script itself. By removing the serviceConfig and using script, the system will actually execute the script and not panic on trying to execute the directory.

There wasn’t a need to order the service after network.target, that was just an artifact from my memory trying to get couchdb set up at one point - I think.

Sorry for hijacking the thread, I thought there was a larger issue at hand :grimacing:

I just want to point out that this isn’t a long-term solution, because PermissionsStartOnly is being deprecated by systemd (see systemd's PermissionsStartOnly is deprecated · Issue #53852 · NixOS/nixpkgs · GitHub), and the feature the original post is asking for (special prefixes) is its intended replacement.

I’m trying to package a Django app, and need to make its /static/ subdir (which is in /var, not in the nix store) world-readable recursively so that nginx can read it (see also this question here).

It seems to me that systemd-tmpfiles cannot chmod recursively.

The docs say it supports “shell-style globs” but what exactly that means doesn’t seem to be explained.

Side note: I also don’t really like systemd-tempfiles for the following reasons:

  • It uses single-letter prefixes, making it impossible to see at a glance what a given tmpfiles declaration does. It’s like when you use a programming library that only has single-letter function names.
  • It uses positional arguments for everything, making you wonder what that - means in each place.

I don’t share this opinion: I think it’s fine to encourage and discourage uses of some feature, and to apply the policy that the declarative way should be preferred if it is possible to solve the problem that way. But we should not aim to make upstream features entirely impossible to use; there will be cases where they are necessary.

I don’t think that addresses all cases.

As a simple example, it’s possible for a service (e.g. my Django app) to restart without nginx restarting, and during restart the app might create more static/ files/subdirs that need to be made nginx-readable.

You’re looking for Z:

Recursively set the access mode, user and group ownership, and restore the SELinux security context of a file or directory if it exists, as well as of its subdirectories and the files contained therein (if applicable). Lines of this type accept shell-style globs in place of normal path names. Does not follow symlinks.

See tmpfiles.d for details.

You raise a good point. I’ll create a PR to add helper syntax to systemd.tmpfiles.rules and ping you.

We never made anything impossible. There are a many ways you can still achieve what you want, and NixOS is very flexible. If you want to give a specific example of what you want to happen I’ll see if I have any recommendations.