nixos/userborn: introduce config.services.userborn.static mode
For embedded appliance-style applications, we would like to avoid the ~100ms boot time hit that comes from running userborn or sysusers by pre-baking the final password files directly into the system closure. Turns out, userborn is trivial to run at build time instead of boot time, so let's introduce a new "baked" static mode that directly places the files into an immutable /etc.
This commit is contained in:
@@ -35,6 +35,9 @@ let
|
||||
};
|
||||
|
||||
userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig);
|
||||
userbornStaticFiles =
|
||||
pkgs.runCommand "static-userborn" { }
|
||||
"mkdir -p $out; ${lib.getExe cfg.package} ${userbornConfigJson} $out";
|
||||
|
||||
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
|
||||
# The filenames created by userborn.
|
||||
@@ -51,12 +54,29 @@ in
|
||||
|
||||
enable = lib.mkEnableOption "userborn";
|
||||
|
||||
static = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to generate the password files at build time and store them directly
|
||||
in the system closure, without requiring any services at boot time.
|
||||
|
||||
This is STRICTLY intended for embedded appliance images that only have system
|
||||
users with manually managed static user IDs, and CANNOT be used with generation
|
||||
updates.
|
||||
|
||||
WARNING: In this mode, you MUST statically manage user IDs yourself, carefully.
|
||||
Beware, UID reuse is a serious security issue and it's your responsibility
|
||||
to avoid it over the entire lifetime of the system.
|
||||
'';
|
||||
};
|
||||
|
||||
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"'';
|
||||
default = if immutableEtc && !cfg.static then "/var/lib/nixos" else "/etc";
|
||||
defaultText = lib.literalExpression ''if immutableEtc && !config.services.userborn.static then "/var/lib/nixos" else "/etc"'';
|
||||
description = ''
|
||||
The location of the original password files.
|
||||
|
||||
@@ -83,8 +103,12 @@ in
|
||||
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`";
|
||||
assertion = (immutableEtc && !cfg.static) -> (cfg.passwordFilesLocation != "/etc");
|
||||
message = "When `system.etc.overlay.mutable = false` and `services.userborn.static = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`";
|
||||
}
|
||||
{
|
||||
assertion = !(cfg.static && config.system.switch.enable);
|
||||
message = "You cannot use `services.userborn.static = true` with switchable configurations, it is ONLY indended for appliance images with fully static user IDs";
|
||||
}
|
||||
];
|
||||
|
||||
@@ -110,7 +134,7 @@ in
|
||||
) userCfg.users
|
||||
);
|
||||
|
||||
services.userborn = {
|
||||
services.userborn = lib.mkIf (!cfg.static) {
|
||||
wantedBy = [ "sysinit.target" ];
|
||||
requiredBy = [ "sysinit-reactivation.target" ];
|
||||
after = [
|
||||
@@ -166,20 +190,35 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# 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
|
||||
)
|
||||
);
|
||||
environment.etc = lib.mkMerge [
|
||||
(lib.mkIf cfg.static (
|
||||
# In static mode, statically drop the files into an immutable /etc.
|
||||
lib.listToAttrs (
|
||||
lib.map (
|
||||
file:
|
||||
lib.nameValuePair file {
|
||||
source = "${userbornStaticFiles}/${file}";
|
||||
mode = if file == "shadow" then "0000" else "0644";
|
||||
}
|
||||
) passwordFiles
|
||||
)
|
||||
))
|
||||
|
||||
(lib.mkIf (!cfg.static && cfg.passwordFilesLocation != "/etc") (
|
||||
# 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 a
|
||||
# (non-static) immutable /etc!
|
||||
lib.listToAttrs (
|
||||
lib.map (
|
||||
file:
|
||||
lib.nameValuePair file {
|
||||
source = "${cfg.passwordFilesLocation}/${file}";
|
||||
mode = "direct-symlink";
|
||||
}
|
||||
) passwordFiles
|
||||
)
|
||||
))
|
||||
];
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
@@ -1647,6 +1647,7 @@ in
|
||||
userborn-immutable-users = runTest ./userborn-immutable-users.nix;
|
||||
userborn-mutable-etc = runTest ./userborn-mutable-etc.nix;
|
||||
userborn-mutable-users = runTest ./userborn-mutable-users.nix;
|
||||
userborn-static = runTest ./userborn-static.nix;
|
||||
ustreamer = runTest ./ustreamer.nix;
|
||||
uwsgi = runTest ./uwsgi.nix;
|
||||
v2ray = runTest ./v2ray.nix;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
{ lib, ... }:
|
||||
|
||||
let
|
||||
sysuserPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639";
|
||||
|
||||
common = {
|
||||
services.userborn = {
|
||||
enable = true;
|
||||
static = true;
|
||||
};
|
||||
boot.initrd.systemd.enable = true;
|
||||
networking.useNetworkd = true;
|
||||
system.etc.overlay = {
|
||||
enable = true;
|
||||
mutable = false;
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "userborn-static";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [
|
||||
mic92
|
||||
valpackett
|
||||
];
|
||||
|
||||
nodes.machine =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [ common ];
|
||||
|
||||
users.users.sysuser = {
|
||||
uid = 1337;
|
||||
isSystemUser = true;
|
||||
group = "wheel";
|
||||
home = "/var/empty";
|
||||
initialHashedPassword = sysuserPassword;
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
with subtest("Correct mode on the password files"):
|
||||
assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n"
|
||||
assert machine.succeed("stat -c '%a' /etc/group") == "644\n"
|
||||
assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n"
|
||||
|
||||
with subtest("Check files"):
|
||||
print(machine.succeed("grpck -r"))
|
||||
print(machine.succeed("pwck -r"))
|
||||
|
||||
with subtest("sysuser user is created"):
|
||||
print(machine.succeed("getent passwd sysuser"))
|
||||
assert "${sysuserPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not correct"
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user