Skip to content

Commit 66285bc

Browse files
committed
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.
1 parent 9cfae7b commit 66285bc

File tree

3 files changed

+111
-19
lines changed

3 files changed

+111
-19
lines changed

nixos/modules/services/system/userborn.nix

Lines changed: 56 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ let
3535
};
3636

3737
userbornConfigJson = pkgs.writeText "userborn.json" (builtins.toJSON userbornConfig);
38+
userbornStaticFiles =
39+
pkgs.runCommand "static-userborn" { }
40+
"mkdir -p $out; ${lib.getExe cfg.package} ${userbornConfigJson} $out";
3841

3942
immutableEtc = config.system.etc.overlay.enable && !config.system.etc.overlay.mutable;
4043
# The filenames created by userborn.
@@ -51,12 +54,29 @@ in
5154

5255
enable = lib.mkEnableOption "userborn";
5356

57+
static = lib.mkOption {
58+
type = lib.types.bool;
59+
default = false;
60+
description = ''
61+
Whether to generate the password files at build time and store them directly
62+
in the system closure, without requiring any services at boot time.
63+
64+
This is STRICTLY intended for embedded appliance images that only have system
65+
users with manually managed static user IDs, and CANNOT be used with generation
66+
updates.
67+
68+
WARNING: In this mode, you MUST statically manage user IDs yourself, carefully.
69+
Beware, UID reuse is a serious security issue and it's your responsibility
70+
to avoid it over the entire lifetime of the system.
71+
'';
72+
};
73+
5474
package = lib.mkPackageOption pkgs "userborn" { };
5575

5676
passwordFilesLocation = lib.mkOption {
5777
type = lib.types.str;
58-
default = if immutableEtc then "/var/lib/nixos" else "/etc";
59-
defaultText = lib.literalExpression ''if immutableEtc then "/var/lib/nixos" else "/etc"'';
78+
default = if immutableEtc && !cfg.static then "/var/lib/nixos" else "/etc";
79+
defaultText = lib.literalExpression ''if immutableEtc && !config.services.userborn.static then "/var/lib/nixos" else "/etc"'';
6080
description = ''
6181
The location of the original password files.
6282
@@ -83,8 +103,12 @@ in
83103
message = "system.activationScripts.users has to be empty to use userborn";
84104
}
85105
{
86-
assertion = immutableEtc -> (cfg.passwordFilesLocation != "/etc");
87-
message = "When `system.etc.overlay.mutable = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`";
106+
assertion = (immutableEtc && !cfg.static) -> (cfg.passwordFilesLocation != "/etc");
107+
message = "When `system.etc.overlay.mutable = false` and `services.userborn.static = false`, `services.userborn.passwordFilesLocation` cannot be set to `/etc`";
108+
}
109+
{
110+
assertion = !(cfg.static && config.system.switch.enable);
111+
message = "You cannot use `services.userborn.static = true` with switchable configurations, it is ONLY indended for appliance images with fully static user IDs";
88112
}
89113
];
90114

@@ -110,7 +134,7 @@ in
110134
) userCfg.users
111135
);
112136

113-
services.userborn = {
137+
services.userborn = lib.mkIf (!cfg.static) {
114138
wantedBy = [ "sysinit.target" ];
115139
requiredBy = [ "sysinit-reactivation.target" ];
116140
after = [
@@ -166,20 +190,33 @@ in
166190
};
167191
};
168192

169-
# Statically create the symlinks to passwordFilesLocation when they're not
170-
# inside /etc because we will not be able to do it at runtime in case of an
171-
# immutable /etc!
172-
environment.etc = lib.mkIf (cfg.passwordFilesLocation != "/etc") (
173-
lib.listToAttrs (
174-
lib.map (
175-
file:
176-
lib.nameValuePair file {
177-
source = "${cfg.passwordFilesLocation}/${file}";
178-
mode = "direct-symlink";
179-
}
180-
) passwordFiles
181-
)
182-
);
193+
environment.etc =
194+
if cfg.static then
195+
# In static mode, statically drop the files into an immutable /etc.
196+
lib.listToAttrs (
197+
lib.map (
198+
file:
199+
lib.nameValuePair file {
200+
source = "${userbornStaticFiles}/${file}";
201+
mode = if file == "shadow" then "0000" else "0644";
202+
}
203+
) passwordFiles
204+
)
205+
else if cfg.passwordFilesLocation != "/etc" then
206+
# Statically create the symlinks to passwordFilesLocation when they're not
207+
# inside /etc because we will not be able to do it at runtime in case of a
208+
# (non-static) immutable /etc!
209+
lib.listToAttrs (
210+
lib.map (
211+
file:
212+
lib.nameValuePair file {
213+
source = "${cfg.passwordFilesLocation}/${file}";
214+
mode = "direct-symlink";
215+
}
216+
) passwordFiles
217+
)
218+
else
219+
{ };
183220
};
184221

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

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,7 @@ in
16161616
userborn-immutable-users = runTest ./userborn-immutable-users.nix;
16171617
userborn-mutable-etc = runTest ./userborn-mutable-etc.nix;
16181618
userborn-mutable-users = runTest ./userborn-mutable-users.nix;
1619+
userborn-static = runTest ./userborn-static.nix;
16191620
ustreamer = runTest ./ustreamer.nix;
16201621
uwsgi = runTest ./uwsgi.nix;
16211622
v2ray = runTest ./v2ray.nix;

nixos/tests/userborn-static.nix

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
{ lib, ... }:
2+
3+
let
4+
sysuserPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639";
5+
6+
common = {
7+
services.userborn = {
8+
enable = true;
9+
static = true;
10+
};
11+
boot.initrd.systemd.enable = true;
12+
networking.useNetworkd = true;
13+
system.etc.overlay = {
14+
enable = true;
15+
mutable = false;
16+
};
17+
};
18+
in
19+
20+
{
21+
22+
name = "userborn-static";
23+
24+
meta.maintainers = with lib.maintainers; [ nikstur ];
25+
26+
nodes.machine =
27+
{ ... }:
28+
{
29+
imports = [ common ];
30+
31+
users.users.sysuser = {
32+
uid = 1337;
33+
isSystemUser = true;
34+
group = "wheel";
35+
home = "/var/empty";
36+
initialHashedPassword = sysuserPassword;
37+
};
38+
};
39+
40+
testScript = ''
41+
with subtest("Correct mode on the password files"):
42+
assert machine.succeed("stat -c '%a' /etc/passwd") == "644\n"
43+
assert machine.succeed("stat -c '%a' /etc/group") == "644\n"
44+
assert machine.succeed("stat -c '%a' /etc/shadow") == "0\n"
45+
46+
with subtest("Check files"):
47+
print(machine.succeed("grpck -r"))
48+
print(machine.succeed("pwck -r"))
49+
50+
with subtest("sysuser user is created"):
51+
print(machine.succeed("getent passwd sysuser"))
52+
assert "${sysuserPassword}" in machine.succeed("getent shadow sysuser"), "sysuser user password is not correct"
53+
'';
54+
}

0 commit comments

Comments
 (0)