Files
user-jails/modules/user-jails.nix

200 lines
6.4 KiB
Nix

{ 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/<uid>/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
};
}