diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 00a380dcc11b..51fbf36889a6 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -203,6 +203,8 @@ - [Pareto Security](https://paretosecurity.com/) is an alternative to corporate compliance solutions for companies that care about security but know it doesn't have to be invasive. Available as [services.paretosecurity](#opt-services.paretosecurity.enable) +- [GNU Rush](https://gnu.org/software/rush/) is a Restricted User Shell, designed for systems providing limited remote access to their resources. Available as [programs.rush](#opt-programs.rush.enable). + - [ipfs-cluster](https://ipfscluster.io/), Pinset orchestration for IPFS. Available as [services.ipfs-cluster](#opt-services.ipfs-cluster.enable) - [bitbox-bridge](https://github.com/BitBoxSwiss/bitbox-bridge), a bridge software that connects BitBox hardware wallets to computers & web wallets like [Rabby](https://rabby.io/). Allows one to interact & transact with smart contracts, Web3 websites & financial services without storing private keys anywhere other than the hardware wallet. Available as [services.bitbox-bridge](#opt-services.bitbox-bridge.enable). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ca4148d81ba8..a8c538bdcc2f 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -290,6 +290,7 @@ ./programs/quark-goldleaf.nix ./programs/regreet.nix ./programs/rog-control-center.nix + ./programs/rush.nix ./programs/rust-motd.nix ./programs/ryzen-monitor-ng.nix ./programs/screen.nix diff --git a/nixos/modules/programs/rush.nix b/nixos/modules/programs/rush.nix new file mode 100644 index 000000000000..782e6da2c2a3 --- /dev/null +++ b/nixos/modules/programs/rush.nix @@ -0,0 +1,109 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.programs.rush; + + indent = + lines: + lib.pipe lines [ + (lib.splitString "\n") + (builtins.filter (line: line != "")) + (map (line: " " + line)) + (builtins.concatStringsSep "\n") + ]; +in +{ + meta.maintainers = pkgs.rush.meta.maintainers; + + options.programs.rush = with lib.types; { + enable = lib.mkEnableOption "Restricted User Shell."; + + package = lib.mkPackageOption pkgs "rush" { } // { + type = shellPackage; + }; + + global = lib.mkOption { + type = lines; + description = "The `global` statement defines global settings."; + default = ""; + }; + + rules = lib.mkOption { + type = attrsOf lines; + default = { }; + + description = '' + The rule statement configures a GNU Rush rule. This is a block statement, which means that all + statements located between it and the next rule statement (or end of file, whichever occurs first) + modify the definition of that rule. + ''; + }; + + shell = lib.mkOption { + readOnly = true; + type = either shellPackage path; + + description = '' + The resolved shell path that users can inherit to set `rush` as their login shell. + This is a convenience option for use in user definitions. Example: + `users.users.alice = { inherit (config.programs.rush) shell; ... };` + ''; + }; + + wrap = lib.mkOption { + type = bool; + default = config.security.enableWrappers; + defaultText = lib.literalExpression "config.security.enableWrappers"; + + description = '' + Whether to wrap the `rush` binary with a SUID-enabled wrapper. + This is required if {option}`security.enableWrappers` is enabled in your configuration. + ''; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + (lib.mkIf cfg.wrap { + security.wrappers.rush = lib.mkDefault { + group = "root"; + owner = "root"; + permissions = "u+rx,g+x,o+x"; + setgid = false; + setuid = true; + source = lib.getExe cfg.package; + }; + }) + + { + programs.rush.shell = if cfg.wrap then config.security.wrapperDir + "/rush" else cfg.package; + + environment = { + shells = [ cfg.shell ]; + systemPackages = [ cfg.package ]; + + etc."rush.rc".text = + lib.pipe + [ + "# This file was created by the module `programs.rush`;" + "rush 2.0" + (lib.optionalString (cfg.global != "") "global\n${indent cfg.global}") + (lib.optionals (cfg.rules != { }) ( + lib.mapAttrsToList (name: content: "rule ${name}\n${indent content}") cfg.rules + )) + ] + [ + (lib.flatten) + (builtins.filter (line: line != "")) + (builtins.concatStringsSep "\n\n") + (lib.mkDefault) + ]; + }; + } + ] + ); +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 3ab1413cecc8..1a398990dcf1 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -1169,6 +1169,7 @@ in rsyslogd = handleTest ./rsyslogd.nix { }; rtkit = runTest ./rtkit.nix; rtorrent = handleTest ./rtorrent.nix { }; + rush = runTest ./rush.nix; rustls-libssl = handleTest ./rustls-libssl.nix { }; rxe = handleTest ./rxe.nix { }; sabnzbd = handleTest ./sabnzbd.nix { }; diff --git a/nixos/tests/rush.nix b/nixos/tests/rush.nix new file mode 100644 index 000000000000..0fcd87d15f58 --- /dev/null +++ b/nixos/tests/rush.nix @@ -0,0 +1,88 @@ +{ pkgs, ... }: +let + inherit (import ./ssh-keys.nix pkgs) snakeOilEd25519PrivateKey snakeOilEd25519PublicKey; + username = "nix-remote-builder"; +in +{ + name = "rush"; + meta = { inherit (pkgs.rush.meta) maintainers platforms; }; + + nodes = { + client = + { ... }: + { + nix.settings.extra-experimental-features = [ "nix-command" ]; + }; + + server = + { config, ... }: + { + nix.settings.trusted-users = [ "${username}" ]; + + programs.rush = { + enable = true; + global = "debug 1"; + + rules = { + daemon = '' + match $# == 2 + match $0 == "nix-daemon" + match $1 == "--stdio" + match $user == "${username}" + chdir "${config.nix.package}/bin" + ''; + + whoami = '' + match $# == 1 + match $0 == "whoami" + match $user == "${username}" + chdir "${dirOf config.environment.usrbinenv}" + ''; + }; + }; + + services.openssh = { + enable = true; + + extraConfig = '' + Match User ${username} + AllowAgentForwarding no + AllowTcpForwarding no + PermitTTY no + PermitTunnel no + X11Forwarding no + Match All + ''; + }; + + users = { + groups."${username}" = { }; + + users."${username}" = { + inherit (config.programs.rush) shell; + group = "${username}"; + isSystemUser = true; + openssh.authorizedKeys.keys = [ snakeOilEd25519PublicKey ]; + }; + }; + }; + }; + + testScript = '' + start_all() + + client.succeed("mkdir -m 700 /root/.ssh") + client.succeed("cat '${snakeOilEd25519PrivateKey}' | tee /root/.ssh/id_ed25519") + client.succeed("chmod 600 /root/.ssh/id_ed25519") + + server.wait_for_unit("sshd") + + client.succeed("ssh-keyscan -H server | tee -a /root/.ssh/known_hosts") + + client.succeed("ssh ${username}@server -- whoami") + client.succeed("nix store info --store 'ssh-ng://${username}@server'") + + client.fail("ssh ${username}@server -- date") + client.fail("nix store info --store 'ssh://${username}@server'") + ''; +} diff --git a/pkgs/by-name/ru/rush/package.nix b/pkgs/by-name/ru/rush/package.nix index 3e94ac44c970..cca8e1429a14 100644 --- a/pkgs/by-name/ru/rush/package.nix +++ b/pkgs/by-name/ru/rush/package.nix @@ -4,6 +4,7 @@ stdenv, bash, perl, + nixosTests, }: stdenv.mkDerivation rec { @@ -61,5 +62,6 @@ stdenv.mkDerivation rec { passthru = { shellPath = "/bin/rush"; + tests = { inherit (nixosTests) rush; }; }; }