Allowing remote rebuild commands in sudoers

I’m trying to lock down security on my NixOS hosts. Currently I rebuild from my development host using a dedicated deploy user on the target hosts, using pam for passwordless sudo like so:

    # Enable PAM authentication with SSH keys
    security.pam = {
      services.sudo = {
        sshAgentAuth = true;
        logFailures = true;
      };
      sshAgentAuth = {
        enable = true;
        authorizedKeysFiles = [ "/etc/ssh/authorized_keys.d/%u" ];
      };
    };

Ideally, I’d like to lock down the deploy user to only having passwordless sudo on the commands needed to rebuild as in this thread however looking in the logs it seems like the commands are wrapped in /bin/sh -c:

COMMAND=/bin/sh -c 'exec /usr/bin/env -i PATH="${PATH-}" "$@"' sh nix-env -p /nix/var/nix/profiles/system --set /nix/store/NIXOS_SYSTEM_STORE_PATH
...
COMMAND=/bin/sh -c 'exec /usr/bin/env -i PATH="${PATH-}" LOCALE_ARCHIVE="${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK="${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER=0 "$@"' sh systemd-run -E LOCALE_ARCHIVE -E NIXOS_INSTALL_BOOTLOADER -E NIXOS_NO_CHECK --collect --no-ask-password --pipe --quiet --service-type=exec --unit=nixos-rebuild-switch-to-configuration /nix/store/NIXOS_SYSTEM_STORE_PATH/bin/switch-to-configuration switch

which upsets my sudoers approach without giving the user passwordless sudo access to /bin/sh, which would seem to defeat the whole point of locking it down. I’d love to know if anyone has got this working, thanks!

Since the deployed configuration can change the sudoers configuration or run anything it wants as root in an activation script or service, is locking this down accomplishing anything?

1 Like

Consider instead making the deploy user be an alt-root account (that is, an account with uid 0 but whose name isn’t root), and using ssh’s forced command mechanism to control what commands it can run. Then you don’t need sudo at all.

I might be misunderstanding what you’re saying, but my aim was to restrict as much as possible the user which can deploy configuration, so a malicious configuration like you describe wouldn’t be possible. I made sudo capabilities only available over SSH so even if the deploy user on the host is compromised, the configuration can’t be changed locally.

Hey I hadn’t thought of this, thanks :grinning_face:. I haven’t head of ssh’s forced command mechanism or know it was possible to have multiple accounts share a UID. Since I’m rebuilding the configuration remotely (nixos-rebuild switch –-target-host=…), the arguments change slightly for each rebuild, do you think this method would still work?

For anyone else looking, I ended up basically adding sudoers rules for the whole thing, /bin/sh -c and all:

    security.sudo.extraRules =
      let
        storePrefix = "/nix/store/*";
        systemName = "nixos-system-${config.networking.hostName}-*";
        envVariants = [
          "/usr/bin/env"
          "env"
        ];
      in
      [
        {
          users = [ "deploy" ];
          runAs = "root";
          commands =
            (map (env: {
              command = ''/bin/sh -c exec ${env} -i PATH\="''${PATH-}" "$@" sh nix-env -p /nix/var/nix/profiles/system --set ${storePrefix}-${systemName}'';
            }) envVariants)
            ++ (map (env: {
              command = ''/bin/sh -c exec ${env} -i PATH\="''${PATH-}" LOCALE_ARCHIVE\="''${LOCALE_ARCHIVE-}" NIXOS_NO_CHECK\="''${NIXOS_NO_CHECK-}" NIXOS_INSTALL_BOOTLOADER\=[01] "$@" sh systemd-run -E LOCALE_ARCHIVE -E NIXOS_INSTALL_BOOTLOADER -E NIXOS_NO_CHECK --collect --no-ask-password --pipe --quiet --service-type\=exec --unit\=nixos-rebuild-switch-to-configuration ${storePrefix}-${systemName}/bin/switch-to-configuration *'';
            }) envVariants);
        }
      ];

The function is due to nixpkgs and nixpkgs-unstable using different invocations of env.

Yes, you will need to write a wrapper script but you can have the forced command interpret the “command originally specified by the client”. See the “AUTHORIZED KEYS FILE FORMAT” section of the sshd(8) manpage for how to go about it.

[EDIT: I found a generic wrapper script someone already wrote: The Only Way For SSH Forced Commands I have not tried to use it myself, and it looks old enough that it might require a bunch of tweaking, but it’ll at least give you somewhere to start. ]