Initial commit

This commit is contained in:
2025-11-19 13:25:07 +09:00
commit 5afee7e394
5 changed files with 345 additions and 0 deletions

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
result
result-*
*.qcow2
target

26
flake.lock generated Normal file
View File

@@ -0,0 +1,26 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1763283776,
"narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-unstable",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

115
flake.nix Normal file
View File

@@ -0,0 +1,115 @@
{
inputs.nixpkgs.url = "nixpkgs/nixos-unstable";
outputs = { self, nixpkgs }: let
inherit (nixpkgs) lib;
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system nixpkgs.legacyPackages.${system});
in {
apps = forAllSystems (system: pkgs: {
default = self.apps.${system}.vm;
vm = {
type = "app";
program = "${lib.getExe self.nixosConfigurations."vm-${system}".config.system.build.vm}";
};
});
nixosModules.default = ./module.nix;
nixosConfigurations = lib.mapAttrs' (n: v: lib.nameValuePair "vm-${n}" v) (forAllSystems (system: pkgs:
lib.nixosSystem {
inherit system pkgs;
modules = [
"${nixpkgs}/nixos/modules/virtualisation/qemu-vm.nix"
self.nixosModules.default
({ config, ... }: {
system.stateVersion = config.system.nixos.release;
virtualisation.graphics = false;
services.getty.autologinUser = "root";
users.motd = ''
==================================
Welcome to the user-jails test vm!
Try logging in as a user:
ssh user1@localhost
ssh user2@localhost
user1: default jail
- private networking
- global users (private users doesn't work atm)
- allow outside network access
user2: permissive jail
- global networking
- global users
- allow outside network access
All users have password 'foobar'
To exit, press Ctrl+A, then X
==================================
'';
users.users.user1 = {
uid = 1000;
isNormalUser = true;
createHome = true;
password = "foobar";
};
users.users'.user1.jail = {
enable = true;
# Private users doesn't work inside VM for now
# See https://github.com/NixOS/nixpkgs/issues/451167
useGlobalUsers = true;
};
users.users.user2 = {
uid = 1001;
isNormalUser = true;
createHome = true;
password = "foobar";
};
users.users'.user2.jail = {
enable = true;
# bindGlobalNixStore = true; # doesn't do anything for now
useGlobalNetworking = true;
useGlobalUsers = true;
};
# users.users.user3 = {
# uid = 1002;
# isNormalUser = true;
# createHome = true;
# password = "foobar";
# };
# users.users'.user3.jail = {
# enable = true;
# allowNetworking = false;
# };
# MOTD description:
# user3: strict jail
# - private networking
# - private users
# - deny outside network access
services.openssh.enable = true;
programs.vim.enable = true;
})
];
}
));
};
}

199
module.nix Normal file
View File

@@ -0,0 +1,199 @@
{ 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
};
}