200 lines
6.4 KiB
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
|
|
};
|
|
}
|