Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

userborn: init at 0.1.0 #332719

Merged
merged 6 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions nixos/doc/manual/configuration/user-mgmt.chapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ modified using `usermod`. Unix groups can be managed using `groupadd`,

::: {.note}
This is experimental.

Please consider using [Userborn](#sec-userborn) over systemd-sysusers as it's
more feature complete.
:::

Instead of using a custom perl script to create users and groups, you can use
Expand All @@ -112,3 +115,43 @@ systemd-sysusers:
```

The primary benefit of this is to remove a dependency on perl.

## Manage users and groups with `userborn` {#sec-userborn}

::: {.note}
This is experimental.
:::

Like systemd-sysusers, Userborn adoesn't depend on Perl but offers some more
advantages over systemd-sysusers:

1. It can create "normal" users (with a GID >= 1000).
2. It can update some information about users. Most notably it can update their
passwords.
3. It will warn when users use an insecure or unsupported password hashing
scheme.

Userborn is the recommended way to manage users if you don't want to rely on
the Perl script. It aims to eventually replace the Perl script by default.

You can enable Userborn via:

```nix
services.userborn.enable = true;
```

You can configure Userborn to store the password files
(`/etc/{group,passwd,shadow}`) outside of `/etc` and symlink them from this
location to `/etc`:

```nix
services.userborn.passwordFilesLocation = "/persistent/etc";
```

This is useful when you store `/etc` on a `tmpfs` or if `/etc` is immutable
(e.g. when using `system.etc.overlay.mutable = false;`). In the latter case the
original files are by default stored in `/var/lib/nixos`.

Userborn implements immutable users by re-mounting the password files
read-only. This means that unlike when using the Perl script, trying to add a
new user (e.g. via `useradd`) will fail right away.
7 changes: 7 additions & 0 deletions nixos/doc/manual/release-notes/rl-2411.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@

- [Quickwit](https://quickwit.io), sub-second search & analytics engine on cloud storage. Available as [services.quickwit](options.html#opt-services.quickwit).

- [Userborn](https://github.com/nikstur/userborn), a service for declarative
user management. This can be used instead of the `update-users-groups.pl`
Perl script and instead of systemd-sysusers. To achieve a system without
Perl, this is the now recommended tool over systemd-sysusers because it can
alos create normal users and change passwords. Available as
[services.userborn](#opt-services.userborn.enable)

- [Flood](https://flood.js.org/), a beautiful WebUI for various torrent clients. Available as [services.flood](options.html#opt-services.flood).

- [QGroundControl], a ground station support and configuration manager for the PX4 and APM Flight Stacks. Available as [programs.qgroundcontrol](options.html#opt-programs.qgroundcontrol.enable).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1347,6 +1347,7 @@
./services/system/systembus-notify.nix
./services/system/systemd-lock-handler.nix
./services/system/uptimed.nix
./services/system/userborn.nix
./services/system/zram-generator.nix
./services/torrent/deluge.nix
./services/torrent/flexget.nix
Expand Down
2 changes: 1 addition & 1 deletion nixos/modules/profiles/perlless.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# Remove perl from activation
boot.initrd.systemd.enable = lib.mkDefault true;
system.etc.overlay.enable = lib.mkDefault true;
systemd.sysusers.enable = lib.mkDefault true;
services.userborn.enable = lib.mkDefault true;

# Random perl remnants
system.disableInstallerTools = lib.mkDefault true;
Expand Down
183 changes: 183 additions & 0 deletions nixos/modules/services/system/userborn.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
utils,
config,
lib,
pkgs,
...
}:

let

cfg = config.services.userborn;
userCfg = config.users;

userbornConfig = {
groups = lib.mapAttrsToList (username: opts: {
inherit (opts) name gid members;
}) config.users.groups;

users = lib.mapAttrsToList (username: opts: {
inherit (opts)
name
uid
group
description
home
password
hashedPassword
hashedPasswordFile
initialPassword
initialHashedPassword
;
isNormal = opts.isNormalUser;
shell = utils.toShellPath opts.shell;
}) config.users.users;
};

userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig);

immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
# The filenames created by userborn.
passwordFiles = [
"group"
"passwd"
"shadow"
];

in
{

options.services.userborn = {

enable = lib.mkEnableOption "userborn";

package = lib.mkPackageOption pkgs "userborn" { };

passwordFilesLocation = lib.mkOption {
type = lib.types.str;
default = if immutableEtc then "/var/lib/nixos" else "/etc";
defaultText = lib.literalExpression ''if immutableEtc then "/var/lib/nixos" else "/etc"'';
description = ''
The location of the original password files.

If this is not `/etc`, the files are symlinked from this location to `/etc`.

The primary motivation for this is an immutable `/etc`, where we cannot
write the files directly to `/etc`.

However this an also serve other use cases, e.g. when `/etc` is on a `tmpfs`.
'';
};

};

config = lib.mkIf cfg.enable {

assertions = [
{
assertion = !(config.systemd.sysusers.enable && cfg.enable);
message = "You cannot use systemd-sysusers and Userborn at the same time";
}
{
assertion = config.system.activationScripts.users == "";
message = "system.activationScripts.users has to be empty to use userborn";
}
{
assertion = immutableEtc -> (cfg.passwordFilesLocation != "/etc");
message = "When `system.etc.overlay.mutable = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`";
}
];

system.activationScripts.users = lib.mkForce "";
system.activationScripts.hashes = lib.mkForce "";

systemd = {

# Create home directories, do not create /var/empty even if that's a user's
# home.
tmpfiles.settings.home-directories = lib.mapAttrs' (
username: opts:
lib.nameValuePair opts.home {
d = {
mode = opts.homeMode;
user = username;
inherit (opts) group;
};
}
) (lib.filterAttrs (_username: opts: opts.home != "/var/empty") userCfg.users);

services.userborn = {
wantedBy = [ "sysinit.target" ];
requiredBy = [ "sysinit-reactivation.target" ];
after = [
"systemd-remount-fs.service"
"systemd-tmpfiles-setup-dev-early.service"
];
before = [
"systemd-tmpfiles-setup-dev.service"
"sysinit.target"
"shutdown.target"
"sysinit-reactivation.target"
];
conflicts = [ "shutdown.target" ];
restartTriggers = [
userbornConfigJson
cfg.passwordFilesLocation
];
# This way we don't have to re-declare all the dependencies to other
# services again.
aliases = [ "systemd-sysusers.service" ];

unitConfig = {
Description = "Manage Users and Groups";
DefaultDependencies = false;
};

serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
TimeoutSec = "90s";

ExecStart = "${lib.getExe cfg.package} ${userbornConfigJson} ${cfg.passwordFilesLocation}";

ExecStartPre = lib.mkMerge [
(lib.mkIf (!config.system.etc.overlay.mutable) [
"${pkgs.coreutils}/bin/mkdir -p ${cfg.passwordFilesLocation}"
])

# Make the source files writable before executing userborn.
(lib.mkIf (!userCfg.mutableUsers) (
lib.map (file: "-${pkgs.util-linux}/bin/umount ${cfg.passwordFilesLocation}/${file}") passwordFiles
))
];

# Make the source files read-only after userborn has finished.
ExecStartPost = lib.mkIf (!userCfg.mutableUsers) (
lib.map (
file:
"${pkgs.util-linux}/bin/mount --bind -o ro ${cfg.passwordFilesLocation}/${file} ${cfg.passwordFilesLocation}/${file}"
) passwordFiles
);
};
};
};

# Statically create the symlinks to passwordFilesLocation when they're not
# inside /etc because we will not be able to do it at runtime in case of an
# immutable /etc!
environment.etc = lib.mkIf (cfg.passwordFilesLocation != "/etc") (
lib.listToAttrs (
lib.map (
file:
lib.nameValuePair file {
source = "${cfg.passwordFilesLocation}/${file}";
mode = "direct-symlink";
}
) passwordFiles
)
);
};

meta.maintainers = with lib.maintainers; [ nikstur ];

}
4 changes: 2 additions & 2 deletions nixos/modules/system/etc/etc-activation.nix
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
message = "`system.etc.overlay.enable` requires `boot.initrd.systemd.enable`";
}
{
assertion = (!config.system.etc.overlay.mutable) -> config.systemd.sysusers.enable;
message = "`system.etc.overlay.mutable = false` requires `systemd.sysusers.enable`";
assertion = (!config.system.etc.overlay.mutable) -> (config.systemd.sysusers.enable || config.services.userborn.enable);
message = "`!system.etc.overlay.mutable` requires `systemd.sysusers.enable` or `services.userborn.enable`";
}
{
assertion = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.6";
Expand Down
5 changes: 5 additions & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1057,6 +1057,11 @@ in {
uptime-kuma = handleTest ./uptime-kuma.nix {};
urn-timer = handleTest ./urn-timer.nix {};
usbguard = handleTest ./usbguard.nix {};
userborn = runTest ./userborn.nix;
userborn-mutable-users = runTest ./userborn-mutable-users.nix;
userborn-immutable-users = runTest ./userborn-immutable-users.nix;
userborn-mutable-etc = runTest ./userborn-mutable-etc.nix;
userborn-immutable-etc = runTest ./userborn-immutable-etc.nix;
user-activation-scripts = handleTest ./user-activation-scripts.nix {};
user-expiry = runTest ./user-expiry.nix;
user-home-mode = handleTest ./user-home-mode.nix {};
Expand Down
70 changes: 70 additions & 0 deletions nixos/tests/userborn-immutable-etc.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{ lib, ... }:

let
normaloHashedPassword = "$y$j9T$IEWqhKtWg.r.8fVkSEF56.$iKNxdMC6hOAQRp6eBtYvBk4c7BGpONXeZMqc8I/LM46";

common = {
services.userborn.enable = true;
boot.initrd.systemd.enable = true;
system.etc.overlay = {
enable = true;
mutable = false;
};
};
in

{

name = "userborn-immutable-etc";

meta.maintainers = with lib.maintainers; [ nikstur ];

nodes.machine =
{ config, ... }:
{
imports = [ common ];

users = {
users = {
normalo = {
isNormalUser = true;
hashedPassword = normaloHashedPassword;
};
};
};

specialisation.new-generation = {
inheritParentConfig = false;
configuration = {
nixpkgs = {
inherit (config.nixpkgs) hostPlatform;
};
imports = [ common ];

users.users = {
new-normalo = {
isNormalUser = true;
};
};
};
};
};

testScript = ''
machine.wait_for_unit("userborn.service")

with subtest("normalo user is created"):
assert "${normaloHashedPassword}" in machine.succeed("getent shadow normalo"), "normalo user password is not correct"


machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")


with subtest("normalo user is disabled"):
print(machine.succeed("getent shadow normalo"))
assert "!*" in machine.succeed("getent shadow normalo"), "normalo user is not disabled"

with subtest("new-normalo user is created after switching to new generation"):
print(machine.succeed("getent passwd new-normalo"))
'';
}
Loading