nix: configure target users via nixos
All checks were successful
Build / Create distribution (push) Successful in 2m0s
Test / Run NixOS test (push) Successful in 3m46s

This makes patching home-manager no longer necessary.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-01-23 17:04:19 +09:00
parent b5bb7654da
commit 134247b57d
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
6 changed files with 143 additions and 187 deletions

View File

@ -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)
}

View File

@ -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)
}
}

2
dist/install.sh vendored
View File

@ -4,8 +4,6 @@ cd "$(dirname -- "$0")" || exit 1
install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify" install -vDm0755 "bin/fortify" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fortify"
install -vDm0755 "bin/fpkg" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fpkg" 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" install -vDm6511 "bin/fsu" "${FORTIFY_INSTALL_PREFIX}/usr/bin/fsu"
if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then if [ ! -f "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" ]; then
install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc" install -vDm0400 "fsurc.default" "${FORTIFY_INSTALL_PREFIX}/etc/fsurc"

View File

@ -7,6 +7,7 @@
let let
inherit (lib) inherit (lib)
mkMerge
mkIf mkIf
mkDefault mkDefault
mapAttrs mapAttrs
@ -19,6 +20,10 @@ let
; ;
cfg = config.environment.fortify; 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 in
{ {
@ -33,8 +38,7 @@ in
group = "root"; group = "root";
}; };
environment.etc = { environment.etc.fsurc = {
fsurc = {
mode = "0400"; mode = "0400";
text = foldlAttrs ( text = foldlAttrs (
acc: username: fid: acc: username: fid:
@ -42,16 +46,6 @@ in
) "" cfg.users; ) "" 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
}
'';
};
systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ]; systemd.services.nix-daemon.unitConfig.RequiresMountsFor = [ "/etc/userdb" ];
services.userdbd.enable = mkDefault true; services.userdbd.enable = mkDefault true;
@ -114,8 +108,8 @@ in
confinement = { confinement = {
app_id = aid; app_id = aid;
inherit (app) groups; inherit (app) groups;
username = "u${toString fid}_a${toString aid}"; username = getsubname fid aid;
home = "${cfg.stateDir}/u${toString fid}/a${toString aid}"; home = getsubhome fid aid;
sandbox = { sandbox = {
inherit (app) inherit (app)
userns userns
@ -173,7 +167,9 @@ in
}; };
in in
pkgs.writeShellScriptBin app.name '' 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; ) cfg.apps;
in in
@ -208,13 +204,57 @@ in
mergeAttrsList ( mergeAttrsList (
# aid 0 is reserved # aid 0 is reserved
imap1 (aid: app: { imap1 (aid: app: {
"u${toString fid}_a${toString aid}" = app.extraConfig // { ${getsubname fid aid} = mkMerge [
home.packages = app.packages; (cfg.home-manager (getsubname fid aid) (getsubuid fid aid))
}; app.extraConfig
{ home.packages = app.packages; }
];
}) cfg.apps }) cfg.apps
) )
// acc // acc
) privPackages cfg.users; ) 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;
};
}; };
} }

View File

@ -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 { apps = mkOption {
type = type =
let let
@ -50,6 +61,8 @@ in
''; '';
}; };
verbose = mkEnableOption "launchers with verbose output";
id = mkOption { id = mkOption {
type = nullOr str; type = nullOr str;
default = null; default = null;

View File

@ -44,7 +44,6 @@ nixosTest {
# For glinfo and wayland-info: # For glinfo and wayland-info:
mesa-demos mesa-demos
wayland-utils wayland-utils
alacritty
# For D-Bus tests: # For D-Bus tests:
libnotify libnotify
@ -111,6 +110,43 @@ nixosTest {
enable = true; enable = true;
stateDir = "/var/lib/fortify"; stateDir = "/var/lib/fortify";
users.alice = 0; 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 = [ imports = [
@ -176,16 +212,18 @@ nixosTest {
machine.screenshot(name) 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")) instances = json.loads(machine.succeed("sudo -u alice -i XDG_RUNTIME_DIR=/run/user/1000 fortify --json ps"))
if len(instances) != 1: if len(instances) != 1:
raise Exception(f"unexpected state length {len(instances)}") raise Exception(f"unexpected state length {len(instances)}")
instance = next(iter(instances.values())) 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']}") 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']}") raise Exception(f"unexpected enablements {instance['config']['confinement']['enablements']}")
@ -212,60 +250,60 @@ nixosTest {
# Create fortify uid 0 state directory: # Create fortify uid 0 state directory:
machine.succeed("install -dm 0755 -o u0_a0 -g users /var/lib/fortify/u0") 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")) 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") 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') 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") machine.wait_for_file("/tmp/dbus-done")
collect_state_ui("dbus_notify_exited") collect_state_ui("dbus_notify_exited")
machine.succeed("pkill -9 mako") machine.succeed("pkill -9 mako")
# Start a terminal (foot) within fortify: # Start app (foot) with Wayland enablement:
fortify("run --wayland foot") swaymsg("exec ne-foot")
wait_for_window("u0_a0@machine") wait_for_window("u0_a1@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client\n") machine.send_chars("clear; wayland-info && touch /tmp/success-client\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client") machine.wait_for_file("/tmp/fortify.1000/tmpdir/1/success-client")
collect_state_ui("foot_wayland_permissive") collect_state_ui("foot_wayland")
check_state(["foot"], 1) check_state("ne-foot", 1)
# Verify acl on XDG_RUNTIME_DIR: # 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.send_chars("exit\n")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Verify acl cleanup on XDG_RUNTIME_DIR: # 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: # Start app (foot) with Wayland enablement 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'") swaymsg("exec foot $SHELL -c '(ne-foot) & sleep 1 && fortify show $(fortify ps --short) && touch /tmp/ps-show-ok && cat'")
wait_for_window("u0_a0@machine") wait_for_window("u0_a1@machine")
machine.send_chars("clear; wayland-info && touch /tmp/success-client-term\n") 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") machine.wait_for_file("/tmp/ps-show-ok")
collect_state_ui("foot_wayland_permissive_term") collect_state_ui("foot_wayland_term")
check_state(["foot"], 1) check_state("ne-foot", 1)
machine.send_chars("exit\n") machine.send_chars("exit\n")
wait_for_window("foot") wait_for_window("foot")
machine.send_key("ctrl-c") machine.send_key("ctrl-c")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Test PulseAudio (fortify does not support PipeWire yet): # Test PulseAudio (fortify does not support PipeWire yet):
fortify("run --wayland --pulse foot") swaymsg("exec pa-foot")
wait_for_window("u0_a0@machine") wait_for_window("u0_a2@machine")
machine.send_chars("clear; pactl info && touch /tmp/success-pulse\n") 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") collect_state_ui("pulse_wayland")
check_state(["foot"], 9) check_state("pa-foot", 9)
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep foot") machine.wait_until_fails("pgrep foot")
# Test XWayland (foot does not support X): # Test XWayland (foot does not support X):
fortify("run -X alacritty") swaymsg("exec x11-alacritty")
wait_for_window("u0_a0@machine") wait_for_window("u0_a3@machine")
machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n") machine.send_chars("clear; glinfo && touch /tmp/success-client-x11\n")
machine.wait_for_file("/tmp/fortify.1000/tmpdir/0/success-client-x11") machine.wait_for_file("/tmp/fortify.1000/tmpdir/3/success-client-x11")
collect_state_ui("alacritty_x11_permissive") collect_state_ui("alacritty_x11")
check_state(["alacritty"], 2) check_state("x11-alacritty", 2)
machine.send_chars("exit\n") machine.send_chars("exit\n")
machine.wait_until_fails("pgrep alacritty") machine.wait_until_fails("pgrep alacritty")