nix: configure target users via nixos
This makes patching home-manager no longer necessary. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
parent
b5bb7654da
commit
134247b57d
@ -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)
|
||||
}
|
@ -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
2
dist/install.sh
vendored
@ -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"
|
||||
|
86
nixos.nix
86
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;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
13
options.nix
13
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;
|
||||
|
96
test.nix
96
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")
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user