nix: integrate dynamic users
test / test (push) Successful in 21s Details

Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
Ophestra Umiker 2024-11-18 02:49:48 +09:00
parent 05b7dbf066
commit 8f3f0c7bbf
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
1 changed files with 271 additions and 269 deletions

540
nixos.nix
View File

@ -11,8 +11,12 @@ let
mkOption mkOption
mkEnableOption mkEnableOption
mkIf mkIf
mkDefault
mapAttrs mapAttrs
mapAttrsToList mapAttrsToList
mergeAttrsList
imap1
foldr
foldlAttrs foldlAttrs
optional optional
optionals optionals
@ -26,8 +30,24 @@ in
environment.fortify = { environment.fortify = {
enable = mkEnableOption "fortify"; enable = mkEnableOption "fortify";
target = mkOption { package = mkOption {
default = { }; type = types.package;
default = pkgs.callPackage ./package.nix { };
description = "Package providing fortify.";
};
users = mkOption {
type =
let
inherit (types) attrsOf ints;
in
attrsOf (ints.between 0 99);
description = ''
Users allowed to spawn fortify apps, as well as their fortify ID value.
'';
};
apps = mkOption {
type = type =
let let
inherit (types) inherit (types)
@ -43,8 +63,23 @@ in
functionTo functionTo
; ;
in in
attrsOf (submodule { listOf (submodule {
options = { options = {
name = mkOption {
type = str;
description = ''
App name, typically command.
'';
};
id = mkOption {
type = nullOr str;
default = null;
description = ''
Freedesktop application ID.
'';
};
packages = mkOption { packages = mkOption {
type = listOf package; type = listOf package;
default = [ ]; default = [ ];
@ -53,200 +88,153 @@ in
''; '';
}; };
launchers = mkOption {
type = attrsOf (submodule {
options = {
id = mkOption {
type = nullOr str;
default = null;
description = ''
Freedesktop application ID.
'';
};
script = mkOption {
type = nullOr str;
default = null;
description = ''
Application launch script.
'';
};
command = mkOption {
type = nullOr str;
default = null;
description = ''
Command to run as the target user.
Setting this to null will default command to wrapper name.
Has no effect when script is set.
'';
};
method = mkOption {
type = enum [
"simple"
"sudo"
"systemd"
];
default = "systemd";
description = ''
Launch method for the sandboxed program.
'';
};
dbus = {
session = mkOption {
type = nullOr (functionTo anything);
default = null;
description = ''
D-Bus session bus custom configuration.
Setting this to null will enable built-in defaults.
'';
};
system = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus system bus custom configuration.
Setting this to null will disable the system bus proxy.
'';
};
};
env = mkOption {
type = nullOr (attrsOf str);
default = null;
description = ''
Environment variables to set for the initial process in the sandbox.
'';
};
nix = mkEnableOption ''
Whether to allow nix daemon connections from within sandbox.
'';
userns = mkEnableOption ''
Whether to allow userns within sandbox.
'';
mapRealUid = mkEnableOption ''
Whether to map to fortify's real UID within the sandbox.
'';
net =
mkEnableOption ''
Whether to allow network access within sandbox.
''
// {
default = true;
};
gpu = mkOption {
type = nullOr bool;
default = null;
description = ''
Target process GPU and driver access.
Setting this to null will enable GPU whenever X or Wayland is enabled.
'';
};
dev = mkEnableOption ''
Whether to allow access to all devices within sandbox.
'';
extraPaths = mkOption {
type = listOf anything;
default = [ ];
description = ''
Extra paths to make available inside the sandbox.
'';
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
default = null;
description = ''
Package containing share files.
Setting this to null will default package name to wrapper name.
'';
};
};
});
default = { };
};
persistence = mkOption {
type = submodule {
options = {
directories = mkOption {
type = listOf anything;
default = [ ];
};
files = mkOption {
type = listOf anything;
default = [ ];
};
};
};
description = ''
Per-user state passed to github:nix-community/impermanence.
'';
};
extraConfig = mkOption { extraConfig = mkOption {
type = anything; type = anything;
default = { }; default = { };
description = "Extra home-manager configuration."; description = "Extra home-manager configuration.";
}; };
script = mkOption {
type = nullOr str;
default = null;
description = ''
Application launch script.
'';
};
command = mkOption {
type = nullOr str;
default = null;
description = ''
Command to run as the target user.
Setting this to null will default command to wrapper name.
Has no effect when script is set.
'';
};
groups = mkOption {
type = listOf str;
default = [ ];
description = ''
List of groups to inherit from the privileged user.
'';
};
dbus = {
session = mkOption {
type = nullOr (functionTo anything);
default = null;
description = ''
D-Bus session bus custom configuration.
Setting this to null will enable built-in defaults.
'';
};
system = mkOption {
type = nullOr anything;
default = null;
description = ''
D-Bus system bus custom configuration.
Setting this to null will disable the system bus proxy.
'';
};
};
env = mkOption {
type = nullOr (attrsOf str);
default = null;
description = ''
Environment variables to set for the initial process in the sandbox.
'';
};
nix = mkEnableOption ''
Whether to allow nix daemon connections from within sandbox.
'';
userns = mkEnableOption ''
Whether to allow userns within sandbox.
'';
mapRealUid = mkEnableOption ''
Whether to map to fortify's real UID within the sandbox.
'';
net =
mkEnableOption ''
Whether to allow network access within sandbox.
''
// {
default = true;
};
gpu = mkOption {
type = nullOr bool;
default = null;
description = ''
Target process GPU and driver access.
Setting this to null will enable GPU whenever X or Wayland is enabled.
'';
};
dev = mkEnableOption ''
Whether to allow access to all devices within sandbox.
'';
extraPaths = mkOption {
type = listOf anything;
default = [ ];
description = ''
Extra paths to make available inside the sandbox.
'';
};
capability = {
wayland = mkOption {
type = bool;
default = true;
description = ''
Whether to share the Wayland socket.
'';
};
x11 = mkOption {
type = bool;
default = false;
description = ''
Whether to share the X11 socket and allow connection.
'';
};
dbus = mkOption {
type = bool;
default = true;
description = ''
Whether to proxy D-Bus.
'';
};
pulse = mkOption {
type = bool;
default = true;
description = ''
Whether to share the PulseAudio socket and cookie.
'';
};
};
share = mkOption {
type = nullOr package;
default = null;
description = ''
Package containing share files.
Setting this to null will default package name to wrapper name.
'';
};
}; };
}); });
}; default = [ ];
description = "Applications managed by fortify.";
package = mkOption {
type = types.package;
default = pkgs.callPackage ./package.nix { };
description = "Package providing fortify.";
};
user = mkOption {
type = types.str;
description = "Privileged user account.";
}; };
stateDir = mkOption { stateDir = mkOption {
@ -259,25 +247,50 @@ in
}; };
config = mkIf cfg.enable { config = mkIf cfg.enable {
environment.persistence.${cfg.stateDir}.users = mapAttrs (_: target: target.persistence) cfg.target; security.wrappers.fsu = {
source = "${cfg.package}/libexec/fsu";
setuid = true;
owner = "root";
setgid = true;
group = "root";
};
home-manager.users = environment.etc = {
mapAttrs (_: target: target.extraConfig // { home.packages = target.packages; }) cfg.target fsurc = {
// { mode = "0400";
${cfg.user}.home.packages = text = foldlAttrs (
let acc: username: fid:
wrap = "${toString config.users.users.${username}.uid} ${toString fid}\n" + acc
user: launchers: ) "" cfg.users;
mapAttrsToList ( };
name: launcher:
with launcher.capability; userdb.source = pkgs.runCommand "generate-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
}
'';
};
services.userdbd.enable = mkDefault true;
home-manager =
let
privPackages = mapAttrs (username: fid: {
home.packages =
let
# aid 0 is reserved
wrappers = imap1 (
aid: app:
let let
extendDBusDefault = id: ext: { extendDBusDefault = id: ext: {
filter = true; filter = true;
talk = [ "org.freedesktop.Notifications" ] ++ ext.talk; talk = [ "org.freedesktop.Notifications" ] ++ ext.talk;
own = own =
(optionals (launcher.id != null) [ (optionals (app.id != null) [
"${id}.*" "${id}.*"
"org.mpris.MediaPlayer2.${id}.*" "org.mpris.MediaPlayer2.${id}.*"
]) ])
@ -296,37 +309,41 @@ in
in in
{ {
session_bus = session_bus =
if launcher.dbus.session != null then if app.dbus.session != null then
(launcher.dbus.session (extendDBusDefault launcher.id)) (app.dbus.session (extendDBusDefault app.id))
else else
(extendDBusDefault launcher.id default); (extendDBusDefault app.id default);
system_bus = launcher.dbus.system; system_bus = app.dbus.system;
}; };
command = if launcher.command == null then name else launcher.command; command = if app.command == null then app.name else app.command;
script = if launcher.script == null then ("exec " + command + " $@") else launcher.script; script = if app.script == null then ("exec " + command + " $@") else app.script;
enablements = enablements =
with app.capability;
(if wayland then 1 else 0) (if wayland then 1 else 0)
+ (if x11 then 2 else 0) + (if x11 then 2 else 0)
+ (if dbus then 4 else 0) + (if dbus then 4 else 0)
+ (if pulse then 8 else 0); + (if pulse then 8 else 0);
conf = { conf = {
inherit (launcher) id method; inherit (app) id;
inherit user;
command = [ command = [
(pkgs.writeScript "${name}-start" '' (pkgs.writeScript "${app.name}-start" ''
#!${pkgs.zsh}${pkgs.zsh.shellPath} #!${pkgs.zsh}${pkgs.zsh.shellPath}
${script} ${script}
'') '')
]; ];
confinement = { confinement = {
app_id = aid;
inherit (app) groups;
username = "u${toString fid}_a${toString aid}";
home = "${cfg.stateDir}/${toString fid}/${toString aid}";
sandbox = { sandbox = {
inherit (launcher) inherit (app)
userns userns
net net
dev dev
env env
; ;
map_real_uid = launcher.mapRealUid; map_real_uid = app.mapRealUid;
filesystem = filesystem =
[ [
{ src = "/bin"; } { src = "/bin"; }
@ -353,24 +370,19 @@ in
src = "/sys/devices"; src = "/sys/devices";
require = false; require = false;
} }
{
src = "/home/${user}";
write = true;
require = true;
}
] ]
++ optionals launcher.nix [ ++ optionals app.nix [
{ src = "/nix/var"; } { src = "/nix/var"; }
{ src = "/var/db/nix-channels"; } { src = "/var/db/nix-channels"; }
] ]
++ optionals (if launcher.gpu != null then launcher.gpu else wayland || x11) [ ++ optionals (if app.gpu != null then app.gpu else app.capability.wayland || app.capability.x11) [
{ src = "/run/opengl-driver"; } { src = "/run/opengl-driver"; }
{ {
src = "/dev/dri"; src = "/dev/dri";
dev = true; dev = true;
} }
] ]
++ launcher.extraPaths; ++ app.extraPaths;
auto_etc = true; auto_etc = true;
override = [ "/var/run/nscd" ]; override = [ "/var/run/nscd" ];
}; };
@ -379,58 +391,48 @@ in
}; };
}; };
in in
pkgs.writeShellScriptBin name ( pkgs.writeShellScriptBin app.name ''
if launcher.method == "simple" then exec fortify app ${pkgs.writeText "fortify-${app.name}.json" (builtins.toJSON conf)} $@
'' ''
exec sudo -u ${user} -i ${command} $@ ) cfg.apps;
'' in
else foldr (
'' app: acc:
exec fortify app ${pkgs.writeText "fortify-${name}.json" (builtins.toJSON conf)} $@
''
)
) launchers;
in
foldlAttrs (
acc: user: target:
acc
++ (foldlAttrs (
shares: name: launcher:
let let
pkg = if launcher.share != null then launcher.share else pkgs.${name}; pkg = if app.share != null then app.share else pkgs.${app.name};
copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true"; copy = source: "[ -d '${source}' ] && cp -Lrv '${source}' $out/share || true";
in in
shares optional (app.capability.wayland || app.capability.x11) (
++ pkgs.runCommand "${app.name}-share" { } ''
optional (launcher.method != "simple" && (launcher.capability.wayland || launcher.capability.x11)) mkdir -p $out/share
( ${copy "${pkg}/share/applications"}
pkgs.runCommand "${name}-share" { } '' ${copy "${pkg}/share/icons"}
mkdir -p $out/share ${copy "${pkg}/share/man"}
${copy "${pkg}/share/applications"}
${copy "${pkg}/share/icons"}
${copy "${pkg}/share/man"}
substituteInPlace $out/share/applications/* \ substituteInPlace $out/share/applications/* \
--replace-warn '${pkg}/bin/' "" \ --replace-warn '${pkg}/bin/' "" \
--replace-warn '${pkg}/libexec/' "" --replace-warn '${pkg}/libexec/' ""
'' ''
) )
) (wrap user target.launchers) target.launchers) ++ acc
) [ cfg.package ] cfg.target; ) (wrappers ++ [ cfg.package ]) cfg.apps;
}; }) cfg.users;
security.polkit.extraConfig =
let
allowList = builtins.toJSON (mapAttrsToList (name: _: name) cfg.target);
in in
'' {
polkit.addRule(function(action, subject) { useUserPackages = false; # prevent users.users entries from being added
if (action.id == "org.freedesktop.machine1.host-shell" &&
${allowList}.indexOf(action.lookup("user")) > -1 && users = foldlAttrs (
subject.user == "${cfg.user}") { acc: _: fid:
return polkit.Result.YES; mergeAttrsList (
} # aid 0 is reserved
}); imap1 (aid: app: {
''; "u${toString fid}_a${toString aid}" = app.extraConfig // {
home.packages = app.packages;
};
}) cfg.apps
)
// acc
) privPackages cfg.users;
};
}; };
} }