From 134247b57d2ff93c7b70815158474d94bed1074b Mon Sep 17 00:00:00 2001 From: Ophestra Date: Thu, 23 Jan 2025 17:04:19 +0900 Subject: [PATCH] nix: configure target users via nixos This makes patching home-manager no longer necessary. Signed-off-by: Ophestra --- cmd/fuserdb/main.go | 69 ------------------------------ cmd/fuserdb/payload.go | 64 ---------------------------- dist/install.sh | 2 - nixos.nix | 86 +++++++++++++++++++++++++++---------- options.nix | 13 ++++++ test.nix | 96 +++++++++++++++++++++++++++++------------- 6 files changed, 143 insertions(+), 187 deletions(-) delete mode 100644 cmd/fuserdb/main.go delete mode 100644 cmd/fuserdb/payload.go diff --git a/cmd/fuserdb/main.go b/cmd/fuserdb/main.go deleted file mode 100644 index 2c3dd24..0000000 --- a/cmd/fuserdb/main.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "flag" - "fmt" - "os" - "path" - "strconv" - - "git.gensokyo.uk/security/fortify/internal/fmsg" -) - -func main() { - fmsg.SetPrefix("fuserdb") - - const varEmpty = "/var/empty" - - out := flag.String("o", "userdb", "output directory") - homeDir := flag.String("d", varEmpty, "parent of home directories") - shell := flag.String("s", "/sbin/nologin", "absolute path to subordinate user shell") - flag.Parse() - - type user struct { - name string - fid int - } - - users := make([]user, len(flag.Args())) - for i, s := range flag.Args() { - f := bytes.SplitN([]byte(s), []byte{':'}, 2) - if len(f) != 2 { - fmsg.Fatalf("invalid entry at index %d", i) - } - users[i].name = string(f[0]) - if fid, err := strconv.Atoi(string(f[1])); err != nil { - fmsg.Fatal(err.Error()) - } else { - users[i].fid = fid - } - } - - if err := os.MkdirAll(*out, 0755); err != nil && !errors.Is(err, os.ErrExist) { - fmsg.Fatalf("cannot create output: %v", err) - } - - for _, u := range users { - fidString := strconv.Itoa(u.fid) - for aid := 0; aid < 10000; aid++ { - userName := fmt.Sprintf("u%d_a%d", u.fid, aid) - uid := 1000000 + u.fid*10000 + aid - us := strconv.Itoa(uid) - realName := fmt.Sprintf("Fortify subordinate user %d (%s)", aid, u.name) - var homeDirectory string - if *homeDir != varEmpty { - homeDirectory = path.Join(*homeDir, "u"+fidString, "a"+strconv.Itoa(aid)) - } else { - homeDirectory = varEmpty - } - - writeUser(userName, uid, us, realName, homeDirectory, *shell, *out) - writeGroup(userName, uid, us, nil, *out) - } - } - - fmsg.Printf("created %d entries", len(users)*2*10000) - fmsg.Exit(0) -} diff --git a/cmd/fuserdb/payload.go b/cmd/fuserdb/payload.go deleted file mode 100644 index dc8a072..0000000 --- a/cmd/fuserdb/payload.go +++ /dev/null @@ -1,64 +0,0 @@ -package main - -import ( - "encoding/json" - "os" - "path" - - "git.gensokyo.uk/security/fortify/internal/fmsg" -) - -type payloadU struct { - UserName string `json:"userName"` - Uid int `json:"uid"` - Gid int `json:"gid"` - MemberOf []string `json:"memberOf,omitempty"` - RealName string `json:"realName"` - HomeDirectory string `json:"homeDirectory"` - Shell string `json:"shell"` -} - -func writeUser(userName string, uid int, us string, realName, homeDirectory, shell string, out string) { - userFileName := userName + ".user" - if f, err := os.OpenFile(path.Join(out, userFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { - fmsg.Fatalf("cannot create %s: %v", userName, err) - } else if err = json.NewEncoder(f).Encode(&payloadU{ - UserName: userName, - Uid: uid, - Gid: uid, - RealName: realName, - HomeDirectory: homeDirectory, - Shell: shell, - }); err != nil { - fmsg.Fatalf("cannot serialise %s: %v", userName, err) - } else if err = f.Close(); err != nil { - fmsg.Printf("cannot close %s: %v", userName, err) - } - if err := os.Symlink(userFileName, path.Join(out, us+".user")); err != nil { - fmsg.Fatalf("cannot link %s: %v", userName, err) - } -} - -type payloadG struct { - GroupName string `json:"groupName"` - Gid int `json:"gid"` - Members []string `json:"members,omitempty"` -} - -func writeGroup(groupName string, gid int, gs string, members []string, out string) { - groupFileName := groupName + ".group" - if f, err := os.OpenFile(path.Join(out, groupFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err != nil { - fmsg.Fatalf("cannot create %s: %v", groupName, err) - } else if err = json.NewEncoder(f).Encode(&payloadG{ - GroupName: groupName, - Gid: gid, - Members: members, - }); err != nil { - fmsg.Fatalf("cannot serialise %s: %v", groupName, err) - } else if err = f.Close(); err != nil { - fmsg.Printf("cannot close %s: %v", groupName, err) - } - if err := os.Symlink(groupFileName, path.Join(out, gs+".group")); err != nil { - fmsg.Fatalf("cannot link %s: %v", groupName, err) - } -} diff --git a/dist/install.sh b/dist/install.sh index 0641226..bd1e8a2 100755 --- a/dist/install.sh +++ b/dist/install.sh @@ -4,8 +4,6 @@ cd "$(dirname -- "$0")" || exit 1 install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify" install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg" -install -vDm0755 "bin/fuserdb" "${FORTIFY_INSTALL_PREFIX}/usr/libexec/fortify/fuserdb" - install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu" if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" diff --git a/nixos.nix b/nixos.nix index cd4cf98..84e58c7 100644 --- a/nixos.nix +++ b/nixos.nix @@ -7,6 +7,7 @@ let inherit (lib) + mkMerge mkIf mkDefault mapAttrs @@ -19,6 +20,10 @@ let ; cfg = config.environment.fortify; + + getsubuid = fid: aid: 1000000 + fid * 10000 + aid; + getsubname = fid: aid: "u${toString fid}_a${toString aid}"; + getsubhome = fid: aid: "${cfg.stateDir}/u${toString fid}/a${toString aid}"; in { @@ -33,23 +38,12 @@ in group = "root"; }; - environment.etc = { - fsurc = { - mode = "0400"; - text = foldlAttrs ( - acc: username: fid: - "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc - ) "" cfg.users; - }; - - userdb.source = pkgs.runCommand "fortify-userdb" { } '' - ${cfg.package}/libexec/fuserdb -o $out ${ - foldlAttrs ( - acc: username: fid: - acc + " ${username}:${toString fid}" - ) "-s /run/current-system/sw/bin/nologin -d ${cfg.stateDir}" cfg.users - } - ''; + environment.etc.fsurc = { + mode = "0400"; + text = foldlAttrs ( + acc: username: fid: + "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc + ) "" cfg.users; }; systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ]; @@ -114,8 +108,8 @@ in confinement = { app_id = aid; inherit (app) groups; - username = "u${toString fid}_a${toString aid}"; - home = "${cfg.stateDir}/u${toString fid}/a${toString aid}"; + username = getsubname fid aid; + home = getsubhome fid aid; sandbox = { inherit (app) userns @@ -173,7 +167,9 @@ in }; in pkgs.writeShellScriptBin app.name '' - exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@ + exec fortify${ + if app.verbose then " -v" else "" + } app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@ '' ) cfg.apps; in @@ -208,13 +204,57 @@ in mergeAttrsList ( # aid 0 is reserved imap1 (aid: app: { - "u${toString fid}_a${toString aid}" = app.extraConfig // { - home.packages = app.packages; - }; + ${getsubname fid aid} = mkMerge [ + (cfg.home-manager (getsubname fid aid) (getsubuid fid aid)) + app.extraConfig + { home.packages = app.packages; } + ]; }) cfg.apps ) // acc ) privPackages cfg.users; }; + + users = + let + getuser = fid: aid: { + isSystemUser = true; + createHome = true; + description = "Fortify subordinate user ${toString aid} (u${toString fid})"; + group = getsubname fid aid; + home = getsubhome fid aid; + uid = getsubuid fid aid; + }; + getgroup = fid: aid: { gid = getsubuid fid aid; }; + in + { + users = foldlAttrs ( + acc: _: fid: + mkMerge [ + (mergeAttrsList ( + # aid 0 is reserved + imap1 (aid: _: { + ${getsubname fid aid} = getuser fid aid; + }) cfg.apps + )) + { ${getsubname fid 0} = getuser fid 0; } + acc + ] + ) { } cfg.users; + + groups = foldlAttrs ( + acc: _: fid: + mkMerge [ + (mergeAttrsList ( + # aid 0 is reserved + imap1 (aid: _: { + ${getsubname fid aid} = getgroup fid aid; + }) cfg.apps + )) + { ${getsubname fid 0} = getgroup fid 0; } + acc + ] + ) { } cfg.users; + }; }; } diff --git a/options.nix b/options.nix index 616b24c..578c7fd 100644 --- a/options.nix +++ b/options.nix @@ -26,6 +26,17 @@ in ''; }; + home-manager = mkOption { + type = + let + inherit (types) functionTo attrsOf anything; + in + functionTo (functionTo (attrsOf anything)); + description = '' + Target user shared home-manager configuration. + ''; + }; + apps = mkOption { type = let @@ -50,6 +61,8 @@ in ''; }; + verbose = mkEnableOption "launchers with verbose output"; + id = mkOption { type = nullOr str; default = null; diff --git a/test.nix b/test.nix index 47f7985..e40ae72 100644 --- a/test.nix +++ b/test.nix @@ -44,7 +44,6 @@ nixosTest { # For glinfo and wayland-info: mesa-demos wayland-utils - alacritty # For D-Bus tests: libnotify @@ -111,6 +110,43 @@ nixosTest { enable = true; stateDir = "/var/lib/fortify"; users.alice = 0; + + home-manager = _: _: { home.stateVersion = "23.05"; }; + + apps = [ + { + name = "ne-foot"; + verbose = true; + share = pkgs.foot; + packages = [ pkgs.foot ]; + command = "foot"; + capability = { + dbus = false; + pulse = false; + }; + } + { + name = "pa-foot"; + verbose = true; + share = pkgs.foot; + packages = [ pkgs.foot ]; + command = "foot"; + capability.dbus = false; + } + { + name = "x11-alacritty"; + verbose = true; + share = pkgs.alacritty; + packages = [ pkgs.alacritty ]; + command = "alacritty"; + capability = { + wayland = false; + x11 = true; + dbus = false; + pulse = false; + }; + } + ]; }; imports = [ @@ -176,16 +212,18 @@ nixosTest { machine.screenshot(name) - def check_state(command, enablements): + def check_state(name, enablements): instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps")) if len(instances) != 1: raise Exception(f"unexpected state length {len(instances)}") instance = next(iter(instances.values())) - if instance['config']['command'] != command: + config = instance['config'] + + if len(config['command']) != 1 or not(config['command'][0].startswith("/nix/store/")) or not(config['command'][0].endswith(f"{name}-start")): raise Exception(f"unexpected command {instance['config']['command']}") - if instance['config']['confinement']['enablements'] != enablements: + if config['confinement']['enablements'] != enablements: raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}") @@ -212,60 +250,60 @@ nixosTest { # Create fortify uid 0 state directory: machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0") - # Start fortify outside Wayland session: + # Start fortify permissive defaults outside Wayland session: print(machine.succeed("sudo -u alice -i fortify -v run -a 0 touch /tmp/success-bare")) machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-bare") - # Start fortify within Wayland session: + # Start fortify permissive defaults within Wayland session: fortify('-v run --wayland --dbus notify-send -a "NixOS Tests" "Test notification" "Notification from within sandbox." && touch /tmp/dbus-done') machine.wait_for_file("/tmp/dbus-done") collect_state_ui("dbus_notify_exited") machine.succeed("pkill -9 mako") - # Start a terminal (foot) within fortify: - fortify("run --wayland foot") - wait_for_window("u0_a0@machine") + # Start app (foot) with Wayland enablement: + swaymsg("exec ne-foot") + wait_for_window("u0_a1@machine") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") - machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client") - collect_state_ui("foot_wayland_permissive") - check_state(["foot"], 1) + machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client") + collect_state_ui("foot_wayland") + check_state("ne-foot", 1) # Verify acl on XDG_RUNTIME_DIR: - print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000")) + print(machine.succeed("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001")) machine.send_chars("exit\n") machine.wait_until_fails("pgrep foot") # Verify acl cleanup on XDG_RUNTIME_DIR: - machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000000") + machine.wait_until_fails("getfacl --absolute-names --omit-header --numeric /run/user/1000 | grep 1000001") - # Start a terminal (foot) within fortify from a terminal: - swaymsg("exec foot $SHELL -c '(fortify run --wayland foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'") - wait_for_window("u0_a0@machine") + # Start app (foot) with Wayland enablement from a terminal: + swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'") + wait_for_window("u0_a1@machine") machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n") - machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-term") + machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client-term") machine.wait_for_file("/tmp/ps-show-ok") - collect_state_ui("foot_wayland_permissive_term") - check_state(["foot"], 1) + collect_state_ui("foot_wayland_term") + check_state("ne-foot", 1) machine.send_chars("exit\n") wait_for_window("foot") machine.send_key("ctrl-c") machine.wait_until_fails("pgrep foot") # Test PulseAudio (fortify does not support PipeWire yet): - fortify("run --wayland --pulse foot") - wait_for_window("u0_a0@machine") + swaymsg("exec pa-foot") + wait_for_window("u0_a2@machine") machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n") - machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-pulse") + machine.wait_for_file("/tmp/fortify.1000/tmpdir/2/success-pulse") collect_state_ui("pulse_wayland") - check_state(["foot"], 9) + check_state("pa-foot", 9) machine.send_chars("exit\n") machine.wait_until_fails("pgrep foot") # Test XWayland (foot does not support X): - fortify("run -X alacritty") - wait_for_window("u0_a0@machine") + swaymsg("exec x11-alacritty") + wait_for_window("u0_a3@machine") machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n") - machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11") - collect_state_ui("alacritty_x11_permissive") - check_state(["alacritty"], 2) + machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11") + collect_state_ui("alacritty_x11") + check_state("x11-alacritty", 2) machine.send_chars("exit\n") machine.wait_until_fails("pgrep alacritty")