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
      '';
    };
  };
}
2 Likes