Merge pull request #270727 from nikstur/nixos-perlless-activation
Perlless Activation
This commit is contained in:
commit
18e5176621
23 changed files with 964 additions and 17 deletions
|
@ -29,6 +29,7 @@ profiles/graphical.section.md
|
|||
profiles/hardened.section.md
|
||||
profiles/headless.section.md
|
||||
profiles/installation-device.section.md
|
||||
profiles/perlless.section.md
|
||||
profiles/minimal.section.md
|
||||
profiles/qemu-guest.section.md
|
||||
```
|
||||
|
|
11
nixos/doc/manual/configuration/profiles/perlless.section.md
Normal file
11
nixos/doc/manual/configuration/profiles/perlless.section.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Perlless {#sec-perlless}
|
||||
|
||||
::: {.warning}
|
||||
If you enable this profile, you will NOT be able to switch to a new
|
||||
configuration and thus you will not be able to rebuild your system with
|
||||
nixos-rebuild!
|
||||
:::
|
||||
|
||||
Render your system completely perlless (i.e. without the perl interpreter). This
|
||||
includes a mechanism so that your build fails if it contains a Nix store path
|
||||
that references the string "perl".
|
|
@ -89,3 +89,18 @@ A user can be deleted using `userdel`:
|
|||
The flag `-r` deletes the user's home directory. Accounts can be
|
||||
modified using `usermod`. Unix groups can be managed using `groupadd`,
|
||||
`groupmod` and `groupdel`.
|
||||
|
||||
## Create users and groups with `systemd-sysusers` {#sec-systemd-sysusers}
|
||||
|
||||
::: {.note}
|
||||
This is experimental.
|
||||
:::
|
||||
|
||||
Instead of using a custom perl script to create users and groups, you can use
|
||||
systemd-sysusers:
|
||||
|
||||
```nix
|
||||
systemd.sysusers.enable = true;
|
||||
```
|
||||
|
||||
The primary benefit of this is to remove a dependency on perl.
|
||||
|
|
36
nixos/doc/manual/development/etc-overlay.section.md
Normal file
36
nixos/doc/manual/development/etc-overlay.section.md
Normal file
|
@ -0,0 +1,36 @@
|
|||
# `/etc` via overlay filesystem {#sec-etc-overlay}
|
||||
|
||||
::: {.note}
|
||||
This is experimental and requires a kernel version >= 6.6 because it uses
|
||||
new overlay features and relies on the new mount API.
|
||||
:::
|
||||
|
||||
Instead of using a custom perl script to activate `/etc`, you activate it via an
|
||||
overlay filesystem:
|
||||
|
||||
```nix
|
||||
system.etc.overlay.enable = true;
|
||||
```
|
||||
|
||||
Using an overlay has two benefits:
|
||||
|
||||
1. it removes a dependency on perl
|
||||
2. it makes activation faster (up to a few seconds)
|
||||
|
||||
By default, the `/etc` overlay is mounted writable (i.e. there is a writable
|
||||
upper layer). However, you can also mount `/etc` immutably (i.e. read-only) by
|
||||
setting:
|
||||
|
||||
```nix
|
||||
system.etc.overlay.mutable = false;
|
||||
```
|
||||
|
||||
The overlay is atomically replaced during system switch. However, files that
|
||||
have been modified will NOT be overwritten. This is the biggest change compared
|
||||
to the perl-based system.
|
||||
|
||||
If you manually make changes to `/etc` on your system and then switch to a new
|
||||
configuration where `system.etc.overlay.mutable = false;`, you will not be able
|
||||
to see the previously made changes in `/etc` anymore. However the changes are
|
||||
not completely gone, they are still in the upperdir of the previous overlay in
|
||||
`/.rw-etc/upper`.
|
|
@ -56,4 +56,5 @@ explained in the next sections.
|
|||
unit-handling.section.md
|
||||
activation-script.section.md
|
||||
non-switchable-systems.section.md
|
||||
etc-overlay.section.md
|
||||
```
|
||||
|
|
|
@ -18,6 +18,22 @@ In addition to numerous new and upgraded packages, this release has the followin
|
|||
|
||||
- Julia environments can now be built with arbitrary packages from the ecosystem using the `.withPackages` function. For example: `julia.withPackages ["Plots"]`.
|
||||
|
||||
- A new option `systemd.sysusers.enable` was added. If enabled, users and
|
||||
groups are created with systemd-sysusers instead of with a custom perl script.
|
||||
|
||||
- A new option `system.etc.overlay.enable` was added. If enabled, `/etc` is
|
||||
mounted via an overlayfs instead of being created by a custom perl script.
|
||||
|
||||
- It is now possible to have a completely perlless system (i.e. a system
|
||||
without perl). Previously, the NixOS activation depended on two perl scripts
|
||||
which can now be replaced via an opt-in mechanism. To make your system
|
||||
perlless, you can use the new perlless profile:
|
||||
```
|
||||
{ modulesPath, ... }: {
|
||||
imports = [ "${modulesPath}/profiles/perlless.nix" ];
|
||||
}
|
||||
```
|
||||
|
||||
## New Services {#sec-release-24.05-new-services}
|
||||
|
||||
<!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
|
||||
|
|
|
@ -685,7 +685,7 @@ in {
|
|||
shadow.gid = ids.gids.shadow;
|
||||
};
|
||||
|
||||
system.activationScripts.users = {
|
||||
system.activationScripts.users = if !config.systemd.sysusers.enable then {
|
||||
supportsDryActivation = true;
|
||||
text = ''
|
||||
install -m 0700 -d /root
|
||||
|
@ -694,7 +694,7 @@ in {
|
|||
${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \
|
||||
-w ${./update-users-groups.pl} ${spec}
|
||||
'';
|
||||
};
|
||||
} else ""; # keep around for backwards compatibility
|
||||
|
||||
system.activationScripts.update-lingering = let
|
||||
lingerDir = "/var/lib/systemd/linger";
|
||||
|
@ -711,7 +711,9 @@ in {
|
|||
'';
|
||||
|
||||
# Warn about user accounts with deprecated password hashing schemes
|
||||
system.activationScripts.hashes = {
|
||||
# This does not work when the users and groups are created by
|
||||
# systemd-sysusers because the users are created too late then.
|
||||
system.activationScripts.hashes = if !config.systemd.sysusers.enable then {
|
||||
deps = [ "users" ];
|
||||
text = ''
|
||||
users=()
|
||||
|
@ -729,7 +731,7 @@ in {
|
|||
printf ' - %s\n' "''${users[@]}"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
} else ""; # keep around for backwards compatibility
|
||||
|
||||
# for backwards compatibility
|
||||
system.activationScripts.groups = stringAfter [ "users" ] "";
|
||||
|
|
|
@ -1488,6 +1488,7 @@
|
|||
./system/boot/systemd/repart.nix
|
||||
./system/boot/systemd/shutdown.nix
|
||||
./system/boot/systemd/sysupdate.nix
|
||||
./system/boot/systemd/sysusers.nix
|
||||
./system/boot/systemd/tmpfiles.nix
|
||||
./system/boot/systemd/user.nix
|
||||
./system/boot/systemd/userdbd.nix
|
||||
|
|
31
nixos/modules/profiles/perlless.nix
Normal file
31
nixos/modules/profiles/perlless.nix
Normal file
|
@ -0,0 +1,31 @@
|
|||
# WARNING: If you enable this profile, you will NOT be able to switch to a new
|
||||
# configuration and thus you will not be able to rebuild your system with
|
||||
# nixos-rebuild!
|
||||
|
||||
{ lib, ... }:
|
||||
|
||||
{
|
||||
|
||||
# Disable switching to a new configuration. This is not a necessary
|
||||
# limitation of a perlless system but just a current one. In the future,
|
||||
# perlless switching might be possible.
|
||||
system.switch.enable = lib.mkDefault false;
|
||||
|
||||
# Remove perl from activation
|
||||
boot.initrd.systemd.enable = lib.mkDefault true;
|
||||
system.etc.overlay.enable = lib.mkDefault true;
|
||||
systemd.sysusers.enable = lib.mkDefault true;
|
||||
|
||||
# Random perl remnants
|
||||
system.disableInstallerTools = lib.mkDefault true;
|
||||
programs.less.lessopen = lib.mkDefault null;
|
||||
programs.command-not-found.enable = lib.mkDefault false;
|
||||
boot.enableContainers = lib.mkDefault false;
|
||||
environment.defaultPackages = lib.mkDefault [ ];
|
||||
documentation.info.enable = lib.mkDefault false;
|
||||
|
||||
# Check that the system does not contain a Nix store path that contains the
|
||||
# string "perl".
|
||||
system.forbiddenDependenciesRegex = "perl";
|
||||
|
||||
}
|
|
@ -95,6 +95,7 @@ in
|
|||
uid = config.ids.uids.messagebus;
|
||||
description = "D-Bus system message bus daemon user";
|
||||
home = homeDir;
|
||||
homeMode = "0755";
|
||||
group = "messagebus";
|
||||
};
|
||||
|
||||
|
|
169
nixos/modules/system/boot/systemd/sysusers.nix
Normal file
169
nixos/modules/system/boot/systemd/sysusers.nix
Normal file
|
@ -0,0 +1,169 @@
|
|||
{ config, lib, pkgs, utils, ... }:
|
||||
|
||||
let
|
||||
|
||||
cfg = config.systemd.sysusers;
|
||||
userCfg = config.users;
|
||||
|
||||
sysusersConfig = pkgs.writeTextDir "00-nixos.conf" ''
|
||||
# Type Name ID GECOS Home directory Shell
|
||||
|
||||
# Users
|
||||
${lib.concatLines (lib.mapAttrsToList
|
||||
(username: opts:
|
||||
let
|
||||
uid = if opts.uid == null then "-" else toString opts.uid;
|
||||
in
|
||||
''u ${username} ${uid}:${opts.group} "${opts.description}" ${opts.home} ${utils.toShellPath opts.shell}''
|
||||
)
|
||||
userCfg.users)
|
||||
}
|
||||
|
||||
# Groups
|
||||
${lib.concatLines (lib.mapAttrsToList
|
||||
(groupname: opts: ''g ${groupname} ${if opts.gid == null then "-" else toString opts.gid}'') userCfg.groups)
|
||||
}
|
||||
|
||||
# Group membership
|
||||
${lib.concatStrings (lib.mapAttrsToList
|
||||
(groupname: opts: (lib.concatMapStrings (username: "m ${username} ${groupname}\n")) opts.members ) userCfg.groups)
|
||||
}
|
||||
'';
|
||||
|
||||
staticSysusersCredentials = pkgs.runCommand "static-sysusers-credentials" { } ''
|
||||
mkdir $out; cd $out
|
||||
${lib.concatLines (
|
||||
(lib.mapAttrsToList
|
||||
(username: opts: "echo -n '${opts.initialHashedPassword}' > 'passwd.hashed-password.${username}'")
|
||||
(lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users))
|
||||
++
|
||||
(lib.mapAttrsToList
|
||||
(username: opts: "echo -n '${opts.initialPassword}' > 'passwd.plaintext-password.${username}'")
|
||||
(lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users))
|
||||
++
|
||||
(lib.mapAttrsToList
|
||||
(username: opts: "cat '${opts.hashedPasswordFile}' > 'passwd.hashed-password.${username}'")
|
||||
(lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users))
|
||||
)
|
||||
}
|
||||
'';
|
||||
|
||||
staticSysusers = pkgs.runCommand "static-sysusers"
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.systemd ];
|
||||
} ''
|
||||
mkdir $out
|
||||
export CREDENTIALS_DIRECTORY=${staticSysusersCredentials}
|
||||
systemd-sysusers --root $out ${sysusersConfig}/00-nixos.conf
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
options = {
|
||||
|
||||
# This module doesn't set it's own user options but reuses the ones from
|
||||
# users-groups.nix
|
||||
|
||||
systemd.sysusers = {
|
||||
enable = lib.mkEnableOption (lib.mdDoc "systemd-sysusers") // {
|
||||
description = lib.mdDoc ''
|
||||
If enabled, users are created with systemd-sysusers instead of with
|
||||
the custom `update-users-groups.pl` script.
|
||||
|
||||
Note: This is experimental.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.system.activationScripts.users == "";
|
||||
message = "system.activationScripts.users has to be empty to use systemd-sysusers";
|
||||
}
|
||||
{
|
||||
assertion = config.users.mutableUsers -> config.system.etc.overlay.enable;
|
||||
message = "config.users.mutableUsers requires config.system.etc.overlay.enable.";
|
||||
}
|
||||
];
|
||||
|
||||
systemd = lib.mkMerge [
|
||||
({
|
||||
|
||||
# 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;
|
||||
group = opts.group;
|
||||
};
|
||||
})
|
||||
(lib.filterAttrs (_username: opts: opts.home != "/var/empty") userCfg.users);
|
||||
})
|
||||
|
||||
(lib.mkIf config.users.mutableUsers {
|
||||
additionalUpstreamSystemUnits = [
|
||||
"systemd-sysusers.service"
|
||||
];
|
||||
|
||||
services.systemd-sysusers = {
|
||||
# Enable switch-to-configuration to restart the service.
|
||||
unitConfig.ConditionNeedsUpdate = [ "" ];
|
||||
requiredBy = [ "sysinit-reactivation.target" ];
|
||||
before = [ "sysinit-reactivation.target" ];
|
||||
restartTriggers = [ "${config.environment.etc."sysusers.d".source}" ];
|
||||
|
||||
serviceConfig = {
|
||||
LoadCredential = lib.mapAttrsToList
|
||||
(username: opts: "passwd.hashed-password.${username}:${opts.hashedPasswordFile}")
|
||||
(lib.filterAttrs (_username: opts: opts.hashedPasswordFile != null) userCfg.users);
|
||||
SetCredential = (lib.mapAttrsToList
|
||||
(username: opts: "passwd.hashed-password.${username}:${opts.initialHashedPassword}")
|
||||
(lib.filterAttrs (_username: opts: opts.initialHashedPassword != null) userCfg.users))
|
||||
++
|
||||
(lib.mapAttrsToList
|
||||
(username: opts: "passwd.plaintext-password.${username}:${opts.initialPassword}")
|
||||
(lib.filterAttrs (_username: opts: opts.initialPassword != null) userCfg.users))
|
||||
;
|
||||
};
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
environment.etc = lib.mkMerge [
|
||||
(lib.mkIf (!userCfg.mutableUsers) {
|
||||
"passwd" = {
|
||||
source = "${staticSysusers}/etc/passwd";
|
||||
mode = "0644";
|
||||
};
|
||||
"group" = {
|
||||
source = "${staticSysusers}/etc/group";
|
||||
mode = "0644";
|
||||
};
|
||||
"shadow" = {
|
||||
source = "${staticSysusers}/etc/shadow";
|
||||
mode = "0000";
|
||||
};
|
||||
"gshadow" = {
|
||||
source = "${staticSysusers}/etc/gshadow";
|
||||
mode = "0000";
|
||||
};
|
||||
})
|
||||
|
||||
(lib.mkIf userCfg.mutableUsers {
|
||||
"sysusers.d".source = sysusersConfig;
|
||||
})
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
}
|
209
nixos/modules/system/etc/build-composefs-dump.py
Normal file
209
nixos/modules/system/etc/build-composefs-dump.py
Normal file
|
@ -0,0 +1,209 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
"""Build a composefs dump from a Json config
|
||||
|
||||
See the man page of composefs-dump for details about the format:
|
||||
https://github.com/containers/composefs/blob/main/man/composefs-dump.md
|
||||
|
||||
Ensure to check the file with the check script when you make changes to it:
|
||||
|
||||
./check-build-composefs-dump.sh ./build-composefs_dump.py
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
Attrs = dict[str, Any]
|
||||
|
||||
|
||||
class FileType(Enum):
|
||||
"""The filetype as defined by the `st_mode` stat field in octal
|
||||
|
||||
You can check the st_mode stat field of a path in Python with
|
||||
`oct(os.stat("/path/").st_mode)`
|
||||
"""
|
||||
|
||||
directory = "4"
|
||||
file = "10"
|
||||
symlink = "12"
|
||||
|
||||
|
||||
class ComposefsPath:
|
||||
path: str
|
||||
size: int
|
||||
filetype: FileType
|
||||
mode: str
|
||||
uid: str
|
||||
gid: str
|
||||
payload: str
|
||||
rdev: str = "0"
|
||||
nlink: int = 1
|
||||
mtime: str = "1.0"
|
||||
content: str = "-"
|
||||
digest: str = "-"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
attrs: Attrs,
|
||||
size: int,
|
||||
filetype: FileType,
|
||||
mode: str,
|
||||
payload: str,
|
||||
path: str | None = None,
|
||||
):
|
||||
if path is None:
|
||||
path = attrs["target"]
|
||||
self.path = "/" + path
|
||||
self.size = size
|
||||
self.filetype = filetype
|
||||
self.mode = mode
|
||||
self.uid = attrs["uid"]
|
||||
self.gid = attrs["gid"]
|
||||
self.payload = payload
|
||||
|
||||
def write_line(self) -> str:
|
||||
line_list = [
|
||||
str(self.path),
|
||||
str(self.size),
|
||||
f"{self.filetype.value}{self.mode}",
|
||||
str(self.nlink),
|
||||
str(self.uid),
|
||||
str(self.gid),
|
||||
str(self.rdev),
|
||||
str(self.mtime),
|
||||
str(self.payload),
|
||||
str(self.content),
|
||||
str(self.digest),
|
||||
]
|
||||
return " ".join(line_list)
|
||||
|
||||
|
||||
def eprint(*args, **kwargs) -> None:
|
||||
print(args, **kwargs, file=sys.stderr)
|
||||
|
||||
|
||||
def leading_directories(path: str) -> list[str]:
|
||||
"""Return the leading directories of path
|
||||
|
||||
Given the path "alsa/conf.d/50-pipewire.conf", for example, this function
|
||||
returns `[ "alsa", "alsa/conf.d" ]`.
|
||||
"""
|
||||
parents = list(Path(path).parents)
|
||||
parents.reverse()
|
||||
# remove the implicit `.` from the start of a relative path or `/` from an
|
||||
# absolute path
|
||||
del parents[0]
|
||||
return [str(i) for i in parents]
|
||||
|
||||
|
||||
def add_leading_directories(
|
||||
target: str, attrs: Attrs, paths: dict[str, ComposefsPath]
|
||||
) -> None:
|
||||
"""Add the leading directories of a target path to the composefs paths
|
||||
|
||||
mkcomposefs expects that all leading directories are explicitly listed in
|
||||
the dump file. Given the path "alsa/conf.d/50-pipewire.conf", for example,
|
||||
this function adds "alsa" and "alsa/conf.d" to the composefs paths.
|
||||
"""
|
||||
path_components = leading_directories(target)
|
||||
for component in path_components:
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
path=component,
|
||||
size=4096,
|
||||
filetype=FileType.directory,
|
||||
mode="0755",
|
||||
payload="-",
|
||||
)
|
||||
paths[component] = composefs_path
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Build a composefs dump from a Json config
|
||||
|
||||
This config describes the files that the final composefs image is supposed
|
||||
to contain.
|
||||
"""
|
||||
config_file = sys.argv[1]
|
||||
if not config_file:
|
||||
eprint("No config file was supplied.")
|
||||
sys.exit(1)
|
||||
|
||||
with open(config_file, "rb") as f:
|
||||
config = json.load(f)
|
||||
|
||||
if not config:
|
||||
eprint("Config is empty.")
|
||||
sys.exit(1)
|
||||
|
||||
eprint("Building composefs dump...")
|
||||
|
||||
paths: dict[str, ComposefsPath] = {}
|
||||
for attrs in config:
|
||||
target = attrs["target"]
|
||||
source = attrs["source"]
|
||||
mode = attrs["mode"]
|
||||
|
||||
if "*" in source: # Path with globbing
|
||||
glob_sources = glob.glob(source)
|
||||
for glob_source in glob_sources:
|
||||
basename = os.path.basename(glob_source)
|
||||
glob_target = f"{target}/{basename}"
|
||||
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
path=glob_target,
|
||||
size=100,
|
||||
filetype=FileType.symlink,
|
||||
mode="0777",
|
||||
payload=glob_source,
|
||||
)
|
||||
|
||||
paths[glob_target] = composefs_path
|
||||
add_leading_directories(glob_target, attrs, paths)
|
||||
else: # Without globbing
|
||||
if mode == "symlink":
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
# A high approximation of the size of a symlink
|
||||
size=100,
|
||||
filetype=FileType.symlink,
|
||||
mode="0777",
|
||||
payload=source,
|
||||
)
|
||||
else:
|
||||
if os.path.isdir(source):
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
size=4096,
|
||||
filetype=FileType.directory,
|
||||
mode=mode,
|
||||
payload=source,
|
||||
)
|
||||
else:
|
||||
composefs_path = ComposefsPath(
|
||||
attrs,
|
||||
size=os.stat(source).st_size,
|
||||
filetype=FileType.file,
|
||||
mode=mode,
|
||||
payload=target,
|
||||
)
|
||||
paths[target] = composefs_path
|
||||
add_leading_directories(target, attrs, paths)
|
||||
|
||||
composefs_dump = ["/ 4096 40755 1 0 0 0 0.0 - - -"] # Root directory
|
||||
for key in sorted(paths):
|
||||
composefs_path = paths[key]
|
||||
eprint(composefs_path.path)
|
||||
composefs_dump.append(composefs_path.write_line())
|
||||
|
||||
print("\n".join(composefs_dump))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
8
nixos/modules/system/etc/check-build-composefs-dump.sh
Executable file
8
nixos/modules/system/etc/check-build-composefs-dump.sh
Executable file
|
@ -0,0 +1,8 @@
|
|||
#! /usr/bin/env nix-shell
|
||||
#! nix-shell -i bash -p black ruff mypy
|
||||
|
||||
file=$1
|
||||
|
||||
black --check --diff $file
|
||||
ruff --line-length 88 $file
|
||||
mypy --strict $file
|
|
@ -1,12 +1,96 @@
|
|||
{ config, lib, ... }:
|
||||
let
|
||||
inherit (lib) stringAfter;
|
||||
in {
|
||||
|
||||
{
|
||||
|
||||
imports = [ ./etc.nix ];
|
||||
|
||||
config = {
|
||||
system.activationScripts.etc =
|
||||
stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands;
|
||||
};
|
||||
config = lib.mkMerge [
|
||||
|
||||
{
|
||||
system.activationScripts.etc =
|
||||
lib.stringAfter [ "users" "groups" ] config.system.build.etcActivationCommands;
|
||||
}
|
||||
|
||||
(lib.mkIf config.system.etc.overlay.enable {
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.boot.initrd.systemd.enable;
|
||||
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 = lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.6";
|
||||
message = "`system.etc.overlay.enable requires a newer kernel, at least version 6.6";
|
||||
}
|
||||
{
|
||||
assertion = config.systemd.sysusers.enable -> (config.users.mutableUsers == config.system.etc.overlay.mutable);
|
||||
message = ''
|
||||
When using systemd-sysusers and mounting `/etc` via an overlay, users
|
||||
can only be mutable when `/etc` is mutable and vice versa.
|
||||
'';
|
||||
}
|
||||
];
|
||||
|
||||
boot.initrd.availableKernelModules = [ "loop" "erofs" "overlay" ];
|
||||
|
||||
boot.initrd.systemd = {
|
||||
mounts = [
|
||||
{
|
||||
where = "/run/etc-metadata";
|
||||
what = "/sysroot${config.system.build.etcMetadataImage}";
|
||||
type = "erofs";
|
||||
options = "loop";
|
||||
unitConfig.RequiresMountsFor = [
|
||||
"/sysroot/nix/store"
|
||||
];
|
||||
}
|
||||
{
|
||||
where = "/sysroot/etc";
|
||||
what = "overlay";
|
||||
type = "overlay";
|
||||
options = lib.concatStringsSep "," ([
|
||||
"relatime"
|
||||
"redirect_dir=on"
|
||||
"metacopy=on"
|
||||
"lowerdir=/run/etc-metadata::/sysroot${config.system.build.etcBasedir}"
|
||||
] ++ lib.optionals config.system.etc.overlay.mutable [
|
||||
"rw"
|
||||
"upperdir=/sysroot/.rw-etc/upper"
|
||||
"workdir=/sysroot/.rw-etc/work"
|
||||
] ++ lib.optionals (!config.system.etc.overlay.mutable) [
|
||||
"ro"
|
||||
]);
|
||||
wantedBy = [ "initrd-fs.target" ];
|
||||
before = [ "initrd-fs.target" ];
|
||||
requires = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ];
|
||||
after = lib.mkIf config.system.etc.overlay.mutable [ "rw-etc.service" ];
|
||||
unitConfig.RequiresMountsFor = [
|
||||
"/sysroot/nix/store"
|
||||
"/run/etc-metadata"
|
||||
];
|
||||
}
|
||||
];
|
||||
services = lib.mkIf config.system.etc.overlay.mutable {
|
||||
rw-etc = {
|
||||
unitConfig = {
|
||||
DefaultDependencies = false;
|
||||
RequiresMountsFor = "/sysroot";
|
||||
};
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = ''
|
||||
/bin/mkdir -p -m 0755 /sysroot/.rw-etc/upper /sysroot/.rw-etc/work
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
})
|
||||
|
||||
];
|
||||
}
|
||||
|
|
|
@ -62,6 +62,16 @@ let
|
|||
]) etc'}
|
||||
'';
|
||||
|
||||
etcHardlinks = filter (f: f.mode != "symlink") etc';
|
||||
|
||||
build-composefs-dump = pkgs.runCommand "build-composefs-dump.py"
|
||||
{
|
||||
buildInputs = [ pkgs.python3 ];
|
||||
} ''
|
||||
install ${./build-composefs-dump.py} $out
|
||||
patchShebangs --host $out
|
||||
'';
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
@ -72,6 +82,30 @@ in
|
|||
|
||||
options = {
|
||||
|
||||
system.etc.overlay = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = lib.mdDoc ''
|
||||
Mount `/etc` as an overlayfs instead of generating it via a perl script.
|
||||
|
||||
Note: This is currently experimental. Only enable this option if you're
|
||||
confident that you can recover your system if it breaks.
|
||||
'';
|
||||
};
|
||||
|
||||
mutable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = lib.mdDoc ''
|
||||
Whether to mount `/etc` mutably (i.e. read-write) or immutably (i.e. read-only).
|
||||
|
||||
If this is false, only the immutable lowerdir is mounted. If it is
|
||||
true, a writable upperdir is mounted on top.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
environment.etc = mkOption {
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
|
@ -190,12 +224,84 @@ in
|
|||
config = {
|
||||
|
||||
system.build.etc = etc;
|
||||
system.build.etcActivationCommands =
|
||||
''
|
||||
# Set up the statically computed bits of /etc.
|
||||
echo "setting up /etc..."
|
||||
${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
|
||||
system.build.etcActivationCommands = let
|
||||
etcOverlayOptions = lib.concatStringsSep "," ([
|
||||
"relatime"
|
||||
"redirect_dir=on"
|
||||
"metacopy=on"
|
||||
] ++ lib.optionals config.system.etc.overlay.mutable [
|
||||
"upperdir=/.rw-etc/upper"
|
||||
"workdir=/.rw-etc/work"
|
||||
]);
|
||||
in if config.system.etc.overlay.enable then ''
|
||||
# This script atomically remounts /etc when switching configuration. On a (re-)boot
|
||||
# this should not run because /etc is mounted via a systemd mount unit
|
||||
# instead. To a large extent this mimics what composefs does. Because
|
||||
# it's relatively simple, however, we avoid the composefs dependency.
|
||||
if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]]; then
|
||||
echo "remounting /etc..."
|
||||
|
||||
tmpMetadataMount=$(mktemp --directory)
|
||||
mount --type erofs ${config.system.build.etcMetadataImage} $tmpMetadataMount
|
||||
|
||||
# Mount the new /etc overlay to a temporary private mount.
|
||||
# This needs the indirection via a private bind mount because you
|
||||
# cannot move shared mounts.
|
||||
tmpEtcMount=$(mktemp --directory)
|
||||
mount --bind --make-private $tmpEtcMount $tmpEtcMount
|
||||
mount --type overlay overlay \
|
||||
--options lowerdir=$tmpMetadataMount::${config.system.build.etcBasedir},${etcOverlayOptions} \
|
||||
$tmpEtcMount
|
||||
|
||||
# Move the new temporary /etc mount underneath the current /etc mount.
|
||||
#
|
||||
# This should eventually use util-linux to perform this move beneath,
|
||||
# however, this functionality is not yet in util-linux. See this
|
||||
# tracking issue: https://github.com/util-linux/util-linux/issues/2604
|
||||
${pkgs.move-mount-beneath}/bin/move-mount --move --beneath $tmpEtcMount /etc
|
||||
|
||||
# Unmount the top /etc mount to atomically reveal the new mount.
|
||||
umount /etc
|
||||
|
||||
fi
|
||||
'' else ''
|
||||
# Set up the statically computed bits of /etc.
|
||||
echo "setting up /etc..."
|
||||
${pkgs.perl.withPackages (p: [ p.FileSlurp ])}/bin/perl ${./setup-etc.pl} ${etc}/etc
|
||||
'';
|
||||
|
||||
system.build.etcBasedir = pkgs.runCommandLocal "etc-lowerdir" { } ''
|
||||
set -euo pipefail
|
||||
|
||||
makeEtcEntry() {
|
||||
src="$1"
|
||||
target="$2"
|
||||
|
||||
mkdir -p "$out/$(dirname "$target")"
|
||||
cp "$src" "$out/$target"
|
||||
}
|
||||
|
||||
mkdir -p "$out"
|
||||
${concatMapStringsSep "\n" (etcEntry: escapeShellArgs [
|
||||
"makeEtcEntry"
|
||||
# Force local source paths to be added to the store
|
||||
"${etcEntry.source}"
|
||||
etcEntry.target
|
||||
]) etcHardlinks}
|
||||
'';
|
||||
|
||||
system.build.etcMetadataImage =
|
||||
let
|
||||
etcJson = pkgs.writeText "etc-json" (builtins.toJSON etc');
|
||||
etcDump = pkgs.runCommand "etc-dump" { } "${build-composefs-dump} ${etcJson} > $out";
|
||||
in
|
||||
pkgs.runCommand "etc-metadata.erofs" {
|
||||
nativeBuildInputs = [ pkgs.composefs pkgs.erofs-utils ];
|
||||
} ''
|
||||
mkcomposefs --from-file ${etcDump} $out
|
||||
fsck.erofs $out
|
||||
'';
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -207,7 +207,10 @@ in
|
|||
networking.usePredictableInterfaceNames = false;
|
||||
|
||||
# Make it easy to log in as root when running the test interactively.
|
||||
users.users.root.initialHashedPassword = mkOverride 150 "";
|
||||
# This needs to be a file because of a quirk in systemd credentials,
|
||||
# where you cannot specify an empty string as a value. systemd-sysusers
|
||||
# uses credentials to set passwords on users.
|
||||
users.users.root.hashedPasswordFile = mkOverride 150 "${pkgs.writeText "hashed-password.root" ""}";
|
||||
|
||||
services.xserver.displayManager.job.logToJournal = true;
|
||||
|
||||
|
|
30
nixos/tests/activation/etc-overlay-immutable.nix
Normal file
30
nixos/tests/activation/etc-overlay-immutable.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{ lib, ... }: {
|
||||
|
||||
name = "activation-etc-overlay-immutable";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine = { pkgs, ... }: {
|
||||
system.etc.overlay.enable = true;
|
||||
system.etc.overlay.mutable = false;
|
||||
|
||||
# Prerequisites
|
||||
systemd.sysusers.enable = true;
|
||||
users.mutableUsers = false;
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
specialisation.new-generation.configuration = {
|
||||
environment.etc."newgen".text = "newgen";
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.succeed("findmnt --kernel --type overlay /etc")
|
||||
machine.fail("stat /etc/newgen")
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
assert machine.succeed("cat /etc/newgen") == "newgen"
|
||||
'';
|
||||
}
|
30
nixos/tests/activation/etc-overlay-mutable.nix
Normal file
30
nixos/tests/activation/etc-overlay-mutable.nix
Normal file
|
@ -0,0 +1,30 @@
|
|||
{ lib, ... }: {
|
||||
|
||||
name = "activation-etc-overlay-mutable";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine = { pkgs, ... }: {
|
||||
system.etc.overlay.enable = true;
|
||||
system.etc.overlay.mutable = true;
|
||||
|
||||
# Prerequisites
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
specialisation.new-generation.configuration = {
|
||||
environment.etc."newgen".text = "newgen";
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.succeed("findmnt --kernel --type overlay /etc")
|
||||
machine.fail("stat /etc/newgen")
|
||||
machine.succeed("echo -n 'mutable' > /etc/mutable")
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
assert machine.succeed("cat /etc/newgen") == "newgen"
|
||||
assert machine.succeed("cat /etc/mutable") == "mutable"
|
||||
'';
|
||||
}
|
24
nixos/tests/activation/perlless.nix
Normal file
24
nixos/tests/activation/perlless.nix
Normal file
|
@ -0,0 +1,24 @@
|
|||
{ lib, ... }:
|
||||
|
||||
{
|
||||
|
||||
name = "activation-perlless";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine = { pkgs, modulesPath, ... }: {
|
||||
imports = [ "${modulesPath}/profiles/perlless.nix" ];
|
||||
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
virtualisation.mountHostNixStore = false;
|
||||
virtualisation.useNixStoreImage = true;
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
perl_store_paths = machine.succeed("ls /nix/store | grep perl || true")
|
||||
print(perl_store_paths)
|
||||
assert len(perl_store_paths) == 0
|
||||
'';
|
||||
|
||||
}
|
|
@ -285,6 +285,9 @@ in {
|
|||
activation = pkgs.callPackage ../modules/system/activation/test.nix { };
|
||||
activation-var = runTest ./activation/var.nix;
|
||||
activation-nix-channel = runTest ./activation/nix-channel.nix;
|
||||
activation-etc-overlay-mutable = runTest ./activation/etc-overlay-mutable.nix;
|
||||
activation-etc-overlay-immutable = runTest ./activation/etc-overlay-immutable.nix;
|
||||
activation-perlless = runTest ./activation/perlless.nix;
|
||||
etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
|
||||
etcd-cluster = handleTestOn ["x86_64-linux"] ./etcd-cluster.nix {};
|
||||
etebase-server = handleTest ./etebase-server.nix {};
|
||||
|
@ -866,6 +869,8 @@ in {
|
|||
systemd-repart = handleTest ./systemd-repart.nix {};
|
||||
systemd-shutdown = handleTest ./systemd-shutdown.nix {};
|
||||
systemd-sysupdate = runTest ./systemd-sysupdate.nix;
|
||||
systemd-sysusers-mutable = runTest ./systemd-sysusers-mutable.nix;
|
||||
systemd-sysusers-immutable = runTest ./systemd-sysusers-immutable.nix;
|
||||
systemd-timesyncd = handleTest ./systemd-timesyncd.nix {};
|
||||
systemd-timesyncd-nscd-dnssec = handleTest ./systemd-timesyncd-nscd-dnssec.nix {};
|
||||
systemd-user-tmpfiles-rules = handleTest ./systemd-user-tmpfiles-rules.nix {};
|
||||
|
|
64
nixos/tests/systemd-sysusers-immutable.nix
Normal file
64
nixos/tests/systemd-sysusers-immutable.nix
Normal file
|
@ -0,0 +1,64 @@
|
|||
{ lib, ... }:
|
||||
|
||||
let
|
||||
rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
|
||||
normaloPassword = "$y$j9T$3aiOV/8CADAK22OK2QT3/0$67OKd50Z4qTaZ8c/eRWHLIM.o3ujtC1.n9ysmJfv639";
|
||||
newNormaloPassword = "mellow";
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "activation-sysusers-immutable";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine = {
|
||||
systemd.sysusers.enable = true;
|
||||
users.mutableUsers = false;
|
||||
|
||||
# Override the empty root password set by the test instrumentation
|
||||
users.users.root.hashedPasswordFile = lib.mkForce null;
|
||||
users.users.root.initialHashedPassword = rootPassword;
|
||||
users.users.normalo = {
|
||||
isNormalUser = true;
|
||||
initialHashedPassword = normaloPassword;
|
||||
};
|
||||
|
||||
specialisation.new-generation.configuration = {
|
||||
users.users.new-normalo = {
|
||||
isNormalUser = true;
|
||||
initialPassword = newNormaloPassword;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
with subtest("Users are not created with systemd-sysusers"):
|
||||
machine.fail("systemctl status systemd-sysusers.service")
|
||||
machine.fail("ls /etc/sysusers.d")
|
||||
|
||||
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"
|
||||
assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n"
|
||||
|
||||
with subtest("root user has correct password"):
|
||||
print(machine.succeed("getent passwd root"))
|
||||
assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct"
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
print(machine.succeed("getent passwd normalo"))
|
||||
assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n"
|
||||
assert "${normaloPassword}" 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("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
print(machine.succeed("getent shadow new-normalo"))
|
||||
assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n"
|
||||
'';
|
||||
}
|
71
nixos/tests/systemd-sysusers-mutable.nix
Normal file
71
nixos/tests/systemd-sysusers-mutable.nix
Normal file
|
@ -0,0 +1,71 @@
|
|||
{ lib, ... }:
|
||||
|
||||
let
|
||||
rootPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
|
||||
normaloPassword = "hello";
|
||||
newNormaloPassword = "$y$j9T$p6OI0WN7.rSfZBOijjRdR.$xUOA2MTcB48ac.9Oc5fz8cxwLv1mMqabnn333iOzSA6";
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
name = "activation-sysusers-mutable";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ nikstur ];
|
||||
|
||||
nodes.machine = { pkgs, ... }: {
|
||||
systemd.sysusers.enable = true;
|
||||
users.mutableUsers = true;
|
||||
|
||||
# Prerequisites
|
||||
system.etc.overlay.enable = true;
|
||||
boot.initrd.systemd.enable = true;
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
|
||||
# Override the empty root password set by the test instrumentation
|
||||
users.users.root.hashedPasswordFile = lib.mkForce null;
|
||||
users.users.root.initialHashedPassword = rootPassword;
|
||||
users.users.normalo = {
|
||||
isNormalUser = true;
|
||||
initialPassword = normaloPassword;
|
||||
};
|
||||
|
||||
specialisation.new-generation.configuration = {
|
||||
users.users.new-normalo = {
|
||||
isNormalUser = true;
|
||||
initialHashedPassword = newNormaloPassword;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("systemd-sysusers.service")
|
||||
|
||||
with subtest("systemd-sysusers.service contains the credentials"):
|
||||
sysusers_service = machine.succeed("systemctl cat systemd-sysusers.service")
|
||||
print(sysusers_service)
|
||||
assert "SetCredential=passwd.plaintext-password.normalo:${normaloPassword}" in sysusers_service
|
||||
|
||||
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"
|
||||
assert machine.succeed("stat -c '%a' /etc/gshadow") == "0\n"
|
||||
|
||||
with subtest("root user has correct password"):
|
||||
print(machine.succeed("getent passwd root"))
|
||||
assert "${rootPassword}" in machine.succeed("getent shadow root"), "root user password is not correct"
|
||||
|
||||
with subtest("normalo user is created"):
|
||||
print(machine.succeed("getent passwd normalo"))
|
||||
assert machine.succeed("stat -c '%U' /home/normalo") == "normalo\n"
|
||||
|
||||
|
||||
machine.succeed("/run/current-system/specialisation/new-generation/bin/switch-to-configuration switch")
|
||||
|
||||
|
||||
with subtest("new-normalo user is created after switching to new generation"):
|
||||
print(machine.succeed("getent passwd new-normalo"))
|
||||
assert machine.succeed("stat -c '%U' /home/new-normalo") == "new-normalo\n"
|
||||
assert "${newNormaloPassword}" in machine.succeed("getent shadow new-normalo"), "new-normalo user password is not correct"
|
||||
'';
|
||||
}
|
29
pkgs/by-name/mo/move-mount-beneath/package.nix
Normal file
29
pkgs/by-name/mo/move-mount-beneath/package.nix
Normal file
|
@ -0,0 +1,29 @@
|
|||
{ lib
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "move-mount-beneath";
|
||||
version = "unstable-2023-11-26";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "brauner";
|
||||
repo = "move-mount-beneath";
|
||||
rev = "d3d16c0d7766eb1892fcc24a75f8d35df4b0fe45";
|
||||
hash = "sha256-hUboFthw9ABwK6MRSNg7+iu9YbiJALNdsw9Ub3v43n4=";
|
||||
};
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
install -D move-mount $out/bin/move-mount
|
||||
runHook postInstall
|
||||
'';
|
||||
|
||||
meta = {
|
||||
description = "Toy binary to illustrate adding a mount beneath an existing mount";
|
||||
homepage = "https://github.com/brauner/move-mount-beneath";
|
||||
license = lib.licenses.mit0;
|
||||
maintainers = with lib.maintainers; [ nikstur ];
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue