{ config, pkgs, lib, ... }: let extraUserOpts = { options.jail = { enable = lib.mkEnableOption ""; # TODO: it's not possible to configure this with the current container module # chrootPath = lib.mkOption { # type = with lib.types; nullOr path; # default = null; # example = "/run/users//root-mnt"; # description = '' # ''; # }; # Bind global nix store instead of a separate isolated one. bindGlobalNixStore = lib.mkOption { type = lib.types.bool; description = '' Whether to bindmount the global nix store into the user jail. Note that this will expose any content in the nix store downloaded by other users as well. ''; default = false; example = true; }; allowNetworking = lib.mkOption { type = lib.types.bool; description = '' Whether to bridge the user's private network namespace to the main namespace so the user can establish network connections outwards from the host machine. ''; default = true; example = false; }; useGlobalNetworking = lib.mkOption { type = lib.types.bool; description = '' Whether to let the user be a part of the main network namespace. ''; default = false; example = true; }; # TODO: not sure if systemd exposes this as an option? In either case, the # nix socket is bindmounted in, so I think the IPC namespace is global # by default # useGlobalIPC = lib.mkOption { # type = lib.types.bool; # description = '' # Whether to let the user be a part of the main IPC namespace. # ''; # default = false; # example = true; # }; useGlobalUsers = lib.mkOption { type = lib.types.bool; description = '' Whether to let the user be a part of the main user namespace. ''; default = false; example = true; }; # NOTE: I believe this is inherently disabled # useGlobalPIDs = lib.mkOption { # type = lib.types.bool; # description = '' # Whether to let the user be a part of the main pid namespace. # ''; # default = false; # example = true; # }; # TODO: Let the user configure their own timezone # useGlobalUTS = lib.mkOption { # type = lib.types.bool; # description = '' # Whether to let the user be a part of the main UTS namespace. # ''; # default = false; # example = true; # }; extraBindPaths = lib.mkOption { type = with lib.types; listOf str; description = '' Additional paths to bindmount into the user's jail. ''; default = [ ]; example = [ ]; }; extraBindReadOnlyPaths = lib.mkOption { type = with lib.types; listOf str; description = '' Additional paths to bindmount as readonly into the user's jail. ''; default = [ ]; example = [ ]; }; }; }; in { options = { users.users' = lib.mkOption { default = { }; type = with lib.types; attrsOf (submodule extraUserOpts); }; }; config = { assertions = [{ assertion = config.boot.enableContainers; message = "Machine needs to be able to run containers in order to create user jails"; }] # NOTE: needed for uid mapping to work correctly? ++ lib.mapAttrsToList (k: _: { assertion = config.users.users.${k}.uid != null; message = "All jailed users need a deterministic uid, missing for user '${k}'"; }) (lib.filterAttrs (_: v: v.jail.enable) config.users.users'); users.users = lib.pipe config.users.users' [ (lib.filterAttrs (_: v: v.jail.enable)) (lib.mapAttrs' (k: v: { name = k; value.group = k; })) ]; users.groups = lib.pipe config.users.users' [ (lib.filterAttrs (_: v: v.jail.enable)) (lib.mapAttrs' (k: v: { name = k; value = { gid = config.users.users.${k}.uid; }; })) ]; containers = lib.mapAttrs' (k: v: { name = "user-jail-${k}"; value = { # TODO: don't linger unless users.users.linger = true; autoStart = true; ephemeral = true; privateNetwork = !v.jail.useGlobalNetworking; privateUsers = if v.jail.useGlobalUsers then "no" else "pick"; extraFlags = [ # TODO: add support for bindmount arguments instead of hacking it in here # "--bind=${config.users.users.${k}.home}:${config.users.users.${k}.home}:rbind,idmap" "--bind=${config.users.users.${k}.home}:${config.users.users.${k}.home}" # TODO: add support for bind-user in nixos-container module # "--bind-user=${k}" ]; config = { system.stateVersion = config.system.stateVersion; networking.hostName = config.networking.hostName; # NOTE: seemingly not needed with --bind-user. users.users.${k} = config.users.users.${k}; users.groups.${config.users.users.${k}.group} = config.users.groups.${config.users.users.${k}.group}; }; }; }) (lib.filterAttrs (_: v: v.jail.enable) config.users.users'); systemd.services = lib.mapAttrs' (k: v: { name = "container@user-jail-${k}"; value = { # unitConfig.RequiresMountsFor = lib.mapAttrsToList (k: v: if v.hostPath != null then v.hostPath else k) config.containers."user-jail-${k}".bindMounts; }; }) (lib.filterAttrs (_: v: v.jail.enable) config.users.users'); services.openssh.extraConfig = lib.pipe config.users.users' [ (lib.filterAttrs (_: v: v.jail.enable)) (lib.mapAttrsToList (k: v: '' Match User ${k} ForceCommand 'machinectl' --quiet shell '${k}@user-jail-${k}' $SSH_ORIGINAL_COMMAND '')) lib.concatStrings ]; security.polkit.enable = true; security.polkit.extraConfig = '' polkit.addRule(function(action, subject) { if ( action.id === "org.freedesktop.machine1.shell" && action.lookup("user") === subject.user && action.lookup("machine") === "user-jail-" + subject.user ) { return polkit.Result.YES; } }); ''; # TODO: use pam module to maybe stop the container upon closing the connection }; }