diff --git a/flake.lock b/flake.lock index f16b495..0e7bd0a 100644 --- a/flake.lock +++ b/flake.lock @@ -24,11 +24,11 @@ "moonarch": { "flake": false, "locked": { - "lastModified": 1786722426, - "narHash": "sha256-zJyJC/7GWLj5DOblOVEsJ/Ki+bZBh+2Uws07pA6r4No=", + "lastModified": 1787067355, + "narHash": "sha256-2n1XcruV9FxUHOh05HdETpep6IIk1PNpcl58MjMcrhA=", "ref": "refs/heads/main", - "rev": "fe552fc7a5160597b9b73e5ec39b4fce4f63bcd1", - "revCount": 162, + "rev": "e21c046e3251317703e9dd2f2d4a76f3a5211329", + "revCount": 166, "type": "git", "url": "https://gitea.moonarch.de/nevaforget/moonarch.git" }, diff --git a/flake.nix b/flake.nix index 221f3da..cc3d114 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,7 @@ nixosModules.moonarch = { ... }: { imports = [ inputs.disko.nixosModules.disko + ./modules/user.nix ./modules/disk.nix ./modules/desktop.nix ./modules/greetd.nix @@ -98,9 +99,16 @@ ]; }; + # Desktop: 512G system disk, 1T disk for /home. + desktop = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + ./hosts/desktop + self.nixosModules.moonarch + ]; + }; + # ThinkPad T14 Gen 3 (AMD Ryzen 7 PRO 6850U). - # Needs hosts/thinkpad/hardware-configuration.nix from the installer - # before it evaluates — see README. thinkpad = nixpkgs.lib.nixosSystem { inherit system; modules = [ diff --git a/hosts/desktop/default.nix b/hosts/desktop/default.nix new file mode 100644 index 0000000..1a1a442 --- /dev/null +++ b/hosts/desktop/default.nix @@ -0,0 +1,118 @@ +# ABOUTME: Host configuration for the desktop: 512G system disk, 1T disk for /home. +# ABOUTME: Disk layout comes from modules/disk.nix, nothing is generated on the machine. + +{ config, lib, pkgs, ... }: + +{ + # Both device paths are placeholders. disko-install overrides them with + # `--disk main ` and `--disk home `, so the real paths are read + # from lsblk on the machine and never travel into this repository. The values + # here are deliberately invalid: without the flags the install fails instead + # of erasing whatever happens to be first in the enumeration. + moonarch.disk = { + enable = true; + device = "/dev/disk/by-id/SET-VIA-disko-install--disk-main"; + home = { + enable = true; + device = "/dev/disk/by-id/SET-VIA-disko-install--disk-home"; + }; + }; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + boot.kernelPackages = pkgs.linuxPackages_zen; + boot.kernelParams = [ "quiet" ]; + + networking.hostName = "desktop"; + networking.networkmanager.enable = true; + + time.timeZone = "Europe/Berlin"; + i18n.defaultLocale = "de_DE.UTF-8"; + i18n.extraLocaleSettings = { + LC_ADDRESS = "de_DE.UTF-8"; + LC_IDENTIFICATION = "de_DE.UTF-8"; + LC_MEASUREMENT = "de_DE.UTF-8"; + LC_MONETARY = "de_DE.UTF-8"; + LC_NAME = "de_DE.UTF-8"; + LC_NUMERIC = "de_DE.UTF-8"; + LC_PAPER = "de_DE.UTF-8"; + LC_TELEPHONE = "de_DE.UTF-8"; + LC_TIME = "de_DE.UTF-8"; + }; + console.keyMap = "de"; + services.xserver.xkb.layout = "de"; + + moonarch.user = "dkressler"; + + users.users.${config.moonarch.user} = { + isNormalUser = true; + extraGroups = [ "networkmanager" "wheel" "video" "input" "plugdev" "docker" ]; + shell = pkgs.zsh; + # Autologin gets past the greeter without a password, but swaylock + # authenticates through pam_unix and cannot unlock a screen for an account + # that has none. `mutableUsers` is at its default, so this applies when the + # account is created and `passwd` overrides it afterwards. Change it after + # the first boot — this value is in the repository. + initialPassword = "moonarch"; + }; + + # LUKS already asked for a passphrase at boot, so the greeter would only ask + # for a second one. initial_session runs once per boot — the greeter comes + # back on logout, which is also where a session gets switched. greetd's + # restart option flips itself off when this is set. + services.greetd.settings.initial_session = { + command = "${pkgs.niri}/bin/niri-session"; + user = config.moonarch.user; + }; + + # Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps + # its own generations, so pre/post package snapshots are redundant. /home is + # its own btrfs on the second disk, which snapper does not care about — a + # subvolume is a subvolume. + services.snapper = { + configs = { + root = { + SUBVOLUME = "/"; + ALLOW_USERS = [ config.moonarch.user ]; + TIMELINE_CREATE = true; + TIMELINE_CLEANUP = true; + }; + home = { + SUBVOLUME = "/home"; + ALLOW_USERS = [ config.moonarch.user ]; + TIMELINE_CREATE = true; + TIMELINE_CLEANUP = true; + }; + }; + snapshotInterval = "hourly"; + cleanupInterval = "1d"; + }; + + # No nixos-hardware profile applies to a self-built desktop, so the firmware + # this machine needs is enabled here instead. + hardware.enableRedistributableFirmware = true; + + # The nixos-hardware profile derives this from enableRedistributableFirmware + # on the ThinkPad; here nothing does, so without this line the CPU runs on the + # microcode in its BIOS. + hardware.cpu.amd.updateMicrocode = true; + + # The GPU is AMD, so amdgpu and Mesa cover it and there is nothing to declare: + # the kernel module is in the default initrd and Mesa comes with the graphics + # stack. An NVIDIA card would have needed videoDrivers and hardware.nvidia. + + services.fstrim.enable = true; + + # modules/services.nix turns fwupd on for every host. Lenovo publishes to the + # LVFS, so it earns its place on the ThinkPad; desktop mainboard vendors + # mostly do not, which leaves a daemon with nothing to update. mkForce because + # the shared module sets it unconditionally. + services.fwupd.enable = lib.mkForce false; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + nixpkgs.config.allowUnfree = true; + + # The release this machine is first installed with. It stays at this value + # for the life of the installation and is never raised by an update. + system.stateVersion = "26.11"; +} diff --git a/hosts/testvm/default.nix b/hosts/testvm/default.nix index c636ad8..28e816f 100644 --- a/hosts/testvm/default.nix +++ b/hosts/testvm/default.nix @@ -1,7 +1,7 @@ # ABOUTME: Host configuration for the QEMU/KVM test VM. # ABOUTME: Launcher script lives outside this repo at ~/VMs/moonix-vm.sh. -{ pkgs, modulesPath, ... }: +{ config, pkgs, modulesPath, ... }: { # virtio_blk and virtio_pci are not in boot.initrd.availableKernelModules by @@ -32,23 +32,25 @@ i18n.defaultLocale = "de_DE.UTF-8"; console.keyMap = "de"; - users.users.kresdo = { + moonarch.user = "moon"; + + users.users.${config.moonarch.user} = { isNormalUser = true; - description = "Dominik Kressler"; extraGroups = [ "networkmanager" "wheel" "video" "input" ]; shell = pkgs.zsh; + # Throwaway password for a throwaway guest. `mutableUsers` is at its + # default, so this applies when the account is created and `passwd` + # overrides it afterwards. + initialPassword = "moonarch"; }; # LUKS already asked for a passphrase at boot, so the greeter would only ask # for a second one. initial_session runs once per boot — the greeter comes # back on logout, which is also where a session gets switched. greetd's # restart option flips itself off when this is set. - # - # The account still needs a password: swaylock authenticates through - # pam_unix, and without a /etc/shadow entry the screen cannot be unlocked. services.greetd.settings.initial_session = { command = "${pkgs.niri}/bin/niri-session"; - user = "kresdo"; + user = config.moonarch.user; }; # Development access from the host via the QEMU port forward on 127.0.0.1:2222. diff --git a/hosts/thinkpad/default.nix b/hosts/thinkpad/default.nix index 71a7731..40b9cb9 100644 --- a/hosts/thinkpad/default.nix +++ b/hosts/thinkpad/default.nix @@ -1,7 +1,7 @@ # ABOUTME: Host configuration for the ThinkPad T14 Gen 3 (AMD). # ABOUTME: Disk layout comes from modules/disk.nix, nothing is generated on the machine. -{ pkgs, ... }: +{ config, pkgs, ... }: { # Partitioning, LUKS and the btrfs subvolumes. The device path is the only @@ -35,20 +35,18 @@ console.keyMap = "de"; services.xserver.xkb.layout = "de"; - users.users.dkressler = { + moonarch.user = "dkressler"; + + users.users.${config.moonarch.user} = { isNormalUser = true; - description = "Dominik Kressler"; extraGroups = [ "networkmanager" "wheel" "video" "input" "plugdev" "docker" ]; shell = pkgs.zsh; - # No password is declared here, so a freshly installed system has no - # /etc/shadow entry for this account. Autologin gets past the greeter - # without one, but swaylock then cannot unlock the screen, so the password - # is still set once after installing, see README. - # - # To make it declarative instead, put the output of `mkpasswd -m sha-512` - # in initialHashedPassword. That commits a hash to the repository, which is - # offline attackable if the repository ever leaks: - # initialHashedPassword = "$6$..."; + # Autologin gets past the greeter without a password, but swaylock + # authenticates through pam_unix and cannot unlock a screen for an account + # that has none. `mutableUsers` is at its default, so this applies when the + # account is created and `passwd` overrides it afterwards. Change it after + # the first boot — this value is in the repository. + initialPassword = "moonarch"; }; # LUKS already asked for a passphrase at boot, so the greeter would only ask @@ -57,7 +55,7 @@ # restart option flips itself off when this is set. services.greetd.settings.initial_session = { command = "${pkgs.niri}/bin/niri-session"; - user = "dkressler"; + user = config.moonarch.user; }; # Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps @@ -66,13 +64,13 @@ configs = { root = { SUBVOLUME = "/"; - ALLOW_USERS = [ "dkressler" ]; + ALLOW_USERS = [ config.moonarch.user ]; TIMELINE_CREATE = true; TIMELINE_CLEANUP = true; }; home = { SUBVOLUME = "/home"; - ALLOW_USERS = [ "dkressler" ]; + ALLOW_USERS = [ config.moonarch.user ]; TIMELINE_CREATE = true; TIMELINE_CLEANUP = true; }; @@ -81,6 +79,15 @@ cleanupInterval = "1d"; }; + # The nixos-hardware profile only sets hardware.cpu.amd.updateMicrocode as a + # default derived from this option, so without it there is neither a microcode + # update nor the redistributable firmware the WLAN card and amdgpu load. + hardware.enableRedistributableFirmware = true; + + # Fingerprint reader. The NixOS module wires pam_fprintd into the PAM stacks + # it owns; enrol a finger with `fprintd-enroll` after installing. + services.fprintd.enable = true; + services.fstrim.enable = true; services.fwupd.enable = true; diff --git a/modules/disk.nix b/modules/disk.nix index d758594..481ea6d 100644 --- a/modules/disk.nix +++ b/modules/disk.nix @@ -8,12 +8,15 @@ let mountOptions = [ "compress=zstd" "noatime" ]; - subvolumes = { + systemSubvolumes = { "/root" = { mountpoint = "/"; inherit mountOptions; }; - "/home" = { mountpoint = "/home"; inherit mountOptions; }; # Separate so the store stays out of the snapper snapshots of /. It is # reproducible from the flake and would only inflate them. "/nix" = { mountpoint = "/nix"; inherit mountOptions; }; + } // lib.optionalAttrs (!cfg.home.enable) { + # Only here when there is no second disk. With one, /home lives over there + # and the main disk carries the system alone. + "/home" = { mountpoint = "/home"; inherit mountOptions; }; } // lib.optionalAttrs (cfg.swapSize != "") { # No compression or noatime here: disko creates the swapfile inside this # subvolume, and btrfs requires it to be nodatacow, which it sets itself. @@ -23,7 +26,11 @@ let }; }; - btrfs = { + homeSubvolumes = { + "/home" = { mountpoint = "/home"; inherit mountOptions; }; + }; + + btrfs = subvolumes: { type = "btrfs"; # Overwrite an existing signature instead of asking. The device is erased # by this point either way. @@ -34,14 +41,24 @@ let # Neither passwordFile nor settings.keyFile is set, which makes disko's # askPassword default to true: it prompts for the passphrase twice while # formatting and never puts it on a command line. - luks = { + # + # With a second disk each container is asked for separately while formatting. + # Give both the same passphrase: systemd stage 1 caches the first one and + # tries it on the second container, so the boot prompts once. If it ever asks + # twice, boot.initrd.systemd.enable = false falls back to the script initrd, + # whose boot.initrd.luks.reusePassphrases does the same thing explicitly. + luks = name: content: { type = "luks"; - name = "crypted"; + inherit name; # Lets TRIM reach the SSD through the container. The tradeoff is that the # pattern of used blocks becomes visible on the raw device. settings.allowDiscards = true; - content = btrfs; + inherit content; }; + + # Wraps a filesystem in LUKS unless the host opted out of encryption. + maybeEncrypted = name: subvolumes: + if cfg.encrypt then luks name (btrfs subvolumes) else btrfs subvolumes; in { options.moonarch.disk = { @@ -63,6 +80,26 @@ in ''; }; + home = { + enable = lib.mkEnableOption "a second disk carrying /home"; + + device = lib.mkOption { + type = lib.types.str; + example = "/dev/disk/by-id/nvme-eui.fedcba9876543210"; + description = '' + Second whole disk, erased and partitioned like the first one. It takes + the /home subvolume, which then no longer exists on the main disk. + + Encryption follows moonarch.disk.encrypt, so both disks are either + encrypted or neither is. The container is named crypted-home to keep + it apart from the system one. + + disko-install addresses it as `--disk home `, the same way the + main disk is `--disk main `. + ''; + }; + }; + encrypt = lib.mkOption { type = lib.types.bool; default = true; @@ -97,31 +134,49 @@ in }; }; - config = lib.mkIf cfg.enable { - disko.devices.disk.main = { - type = "disk"; - device = cfg.device; - content = { - type = "gpt"; - partitions = { - ESP = { - size = cfg.espSize; - type = "EF00"; - content = { - type = "filesystem"; - format = "vfat"; - mountpoint = "/boot"; - mountOptions = [ "umask=0077" ]; + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + disko.devices.disk.main = { + type = "disk"; + device = cfg.device; + content = { + type = "gpt"; + partitions = { + ESP = { + size = cfg.espSize; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + # Named the same whether or not it is encrypted, so the partlabel the + # fileSystems entries point at does not depend on cfg.encrypt. + root = { + size = "100%"; + content = maybeEncrypted "crypted" systemSubvolumes; }; - }; - # Named the same whether or not it is encrypted, so the partlabel the - # fileSystems entries point at does not depend on cfg.encrypt. - root = { - size = "100%"; - content = if cfg.encrypt then luks else btrfs; }; }; }; - }; - }; + } + + # Second disk. No ESP on it: the firmware boots from the main disk, and a + # second one would only be another thing to keep in sync. + (lib.mkIf cfg.home.enable { + disko.devices.disk.home = { + type = "disk"; + device = cfg.home.device; + content = { + type = "gpt"; + partitions.home = { + size = "100%"; + content = maybeEncrypted "crypted-home" homeSubvolumes; + }; + }; + }; + }) + ]); } diff --git a/modules/user.nix b/modules/user.nix new file mode 100644 index 0000000..591fcd9 --- /dev/null +++ b/modules/user.nix @@ -0,0 +1,20 @@ +# ABOUTME: Declares the moonarch.user option; every host sets its own login name. +# ABOUTME: Autologin, group membership and snapper read moonarch.user from here. + +{ lib, ... }: + +{ + options.moonarch.user = lib.mkOption { + type = lib.types.str; + example = "moon"; + description = '' + Login name of the primary account, and therefore the directory name under + /home. Autologin, the group memberships and snapper's ALLOW_USERS all read + this, so the name is declared once instead of once per reference. + + There is no default: the name belongs to the machine, so every host sets + it. Changing it on an installed system does not rename the home directory — + that stays behind under the old name. + ''; + }; +}