diff --git a/modules/genebean/home/default.nix b/modules/genebean/home/default.nix index 2109669e..adea2ba3 100644 --- a/modules/genebean/home/default.nix +++ b/modules/genebean/home/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./kiosk-hardware.nix ./plasma.nix ./programs/angry-ip-scanner.nix ./programs/askpass.nix @@ -56,6 +57,7 @@ ./programs/zsh.nix ./services/chromium-kiosk.nix ./services/flatpak.nix + ./services/kiosk-backups.nix ./services/tailscale.nix ]; } diff --git a/modules/genebean/home/kiosk-hardware.nix b/modules/genebean/home/kiosk-hardware.nix new file mode 100644 index 00000000..e2227613 --- /dev/null +++ b/modules/genebean/home/kiosk-hardware.nix @@ -0,0 +1,11 @@ +{ lib, ... }: +{ + options.genebean.kiosk-hardware = { + enable = lib.mkEnableOption "shared NixOS-level config for small headless kiosk boxes"; + + wirelessInterface = lib.mkOption { + type = lib.types.str; + description = "Wireless interface name (e.g. wlan0, wlp3s0). Feeds networking.wireless.interfaces directly. genebean.services.chromium-kiosk has its own wirelessInterface option for its wpa_supplicant dependency - set that to config.genebean.kiosk-hardware.wirelessInterface at the call site rather than duplicating the value."; + }; + }; +} diff --git a/modules/genebean/home/services/chromium-kiosk.nix b/modules/genebean/home/services/chromium-kiosk.nix index c4f267c6..4e47632a 100644 --- a/modules/genebean/home/services/chromium-kiosk.nix +++ b/modules/genebean/home/services/chromium-kiosk.nix @@ -25,6 +25,12 @@ in description = "wlr-randr --transform value (e.g. \"90\", \"flipped-90\"). null = no --transform flag."; }; + wirelessInterface = lib.mkOption { + type = lib.types.str; + description = "Wireless interface name (e.g. wlan0, wlp3s0) - the NixOS side depends on wpa_supplicant-.service before starting the kiosk. Normally set to config.genebean.kiosk-hardware.wirelessInterface at the call site rather than duplicated."; + example = "wlan0"; + }; + wlrRandrOutput = lib.mkOption { type = lib.types.str; default = "HDMI-A-1"; diff --git a/modules/genebean/home/services/kiosk-backups.nix b/modules/genebean/home/services/kiosk-backups.nix new file mode 100644 index 00000000..1c4758b5 --- /dev/null +++ b/modules/genebean/home/services/kiosk-backups.nix @@ -0,0 +1,16 @@ +{ lib, username, ... }: +{ + options.genebean.services.kiosk-backups = { + enable = lib.mkEnableOption "restic-based disaster-recovery backups for kiosk state (chromium profile, atuin session, tailscale identity)"; + + paths = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ + "/home/${username}/.config/chromium" + "/home/${username}/.local/share/atuin" + "/var/lib/tailscale" + ]; + description = "Paths to back up daily and make available to kiosk-restic-full-restore. Must NOT include a /persist prefix - the NixOS side (modules/genebean/nixos/services/kiosk-backups.nix) adds that automatically when the host uses impermanence (environment.persistence.\"/persist\".enable). A path already starting with /persist is a configuration error - see that module's assertions."; + }; + }; +} diff --git a/modules/genebean/nixos/default.nix b/modules/genebean/nixos/default.nix index 408cb247..b6d01c63 100644 --- a/modules/genebean/nixos/default.nix +++ b/modules/genebean/nixos/default.nix @@ -1,5 +1,6 @@ { imports = [ + ./kiosk-hardware.nix ./plasma.nix ./ports.nix ./programs/askpass.nix @@ -11,6 +12,7 @@ ./programs/xfce4-terminal.nix ./services/chromium-kiosk.nix ./services/flatpak.nix + ./services/kiosk-backups.nix ./services/tailscale.nix ]; } diff --git a/modules/genebean/nixos/kiosk-hardware.nix b/modules/genebean/nixos/kiosk-hardware.nix new file mode 100644 index 00000000..be3311c7 --- /dev/null +++ b/modules/genebean/nixos/kiosk-hardware.nix @@ -0,0 +1,55 @@ +{ + config, + lib, + username, + ... +}: +let + cfg = config.home-manager.users.${username}.genebean.kiosk-hardware; +in +{ + config = lib.mkIf cfg.enable { + boot.supportedFilesystems = lib.mkForce [ + "ext4" + "f2fs" + "vfat" + ]; # kiosk-gene-desk's full list - small enough to just share as-is rather than juggle per-host overrides + + hardware = { + enableRedistributableFirmware = true; + graphics.enable = true; + }; + + networking = { + firewall.enable = false; + useNetworkd = true; + wireless = { + enable = true; + interfaces = [ cfg.wirelessInterface ]; + secretsFile = config.sops.secrets.wifi_creds.path; + }; + }; + + sops.secrets.wifi_creds = { + sopsFile = ../../shared/secrets.yaml; # two levels up from modules/genebean/nixos/, not three - this file lives at the nixos/ top level, not nested under programs/ or services/ + owner = "wpa_supplicant"; + restartUnits = [ "wpa_supplicant-${cfg.wirelessInterface}.service" ]; + }; + + users.users.${username} = { + isNormalUser = true; + description = "Gene Liverman"; + extraGroups = [ + "networkmanager" + "wheel" + ]; + linger = true; + }; + + zramSwap = { + enable = true; + algorithm = "zstd"; + memoryPercent = 90; + }; + }; +} diff --git a/modules/genebean/nixos/services/chromium-kiosk.nix b/modules/genebean/nixos/services/chromium-kiosk.nix index 6d3a2364..ef01054c 100644 --- a/modules/genebean/nixos/services/chromium-kiosk.nix +++ b/modules/genebean/nixos/services/chromium-kiosk.nix @@ -7,12 +7,23 @@ }: let cfg = config.home-manager.users.${username}.genebean.services.chromium-kiosk; - wifiInterface = lib.elemAt config.networking.wireless.interfaces 0; in { config = lib.mkIf cfg.enable { environment.systemPackages = [ pkgs.wlr-randr ]; + fonts = { + fontconfig = { + enable = true; + useEmbeddedBitmaps = true; + }; + packages = with pkgs; [ + noto-fonts + noto-fonts-color-emoji + noto-fonts-cjk-sans + ]; + }; + services.cage = { enable = true; environment.WLR_LIBINPUT_NO_DEVICES = "1"; # boot up even if no mouse/keyboard connected @@ -56,7 +67,7 @@ in # chromium/cage instance running. restartIfChanged = lib.mkForce true; wants = [ - "wpa_supplicant-${wifiInterface}.service" + "wpa_supplicant-${cfg.wirelessInterface}.service" "network-online.target" ]; }; diff --git a/modules/genebean/nixos/services/kiosk-backups.nix b/modules/genebean/nixos/services/kiosk-backups.nix new file mode 100644 index 00000000..205e3716 --- /dev/null +++ b/modules/genebean/nixos/services/kiosk-backups.nix @@ -0,0 +1,58 @@ +{ + config, + lib, + username, + ... +}: +let + cfg = config.home-manager.users.${username}.genebean.services.kiosk-backups; + usesImpermanence = config.environment.persistence."/persist".enable or false; + prefixedPaths = map (path: if usesImpermanence then "/persist${path}" else path) cfg.paths; +in +{ + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = !(lib.any (lib.hasPrefix "/persist") cfg.paths); + message = "genebean.services.kiosk-backups.paths must not include a /persist prefix - it's added automatically when the host uses impermanence."; + } + ]; + + services.restic.backups = { + daily = { + paths = prefixedPaths; + # Avoid a boot-triggered catch-up backup capturing the wrong state + # as "latest" before a chance to restore after a reinstall - + # confirmed on kiosk-gene-desk hardware, see its original comment. + timerConfig = { + OnCalendar = "daily"; + Persistent = false; + }; + }; + + pre-reinstall-cleanup = { + environmentFile = config.sops.secrets.restic_env.path; + passwordFile = config.sops.secrets.restic_password.path; + repositoryFile = config.sops.secrets.restic_repo.path; + pruneOpts = [ + "--tag pre-reinstall" + "--host ${config.networking.hostName}" + "--keep-within 45d" + ]; + timerConfig = { + OnCalendar = "daily"; + Persistent = false; + }; + }; + }; + + genebean.programs.kiosk-restic-full-restore = { + enable = true; + restorePaths = prefixedPaths; + stopServices = [ + "cage-tty1" + "tailscaled" + ]; + }; + }; +} diff --git a/modules/hosts/nixos/kiosk-entryway/default.nix b/modules/hosts/nixos/kiosk-entryway/default.nix index 332299c0..8e92e55a 100644 --- a/modules/hosts/nixos/kiosk-entryway/default.nix +++ b/modules/hosts/nixos/kiosk-entryway/default.nix @@ -1,7 +1,5 @@ { config, - lib, - pkgs, username, ... }: @@ -10,45 +8,17 @@ ./disk-config.nix ./hardware-configuration.nix ./monitoring.nix + ../../../shared/nixos/restic.nix ]; system.stateVersion = "24.11"; - boot.supportedFilesystems = lib.mkForce [ - "vfat" - "ext4" - ]; - - fonts = { - fontconfig = { - enable = true; - useEmbeddedBitmaps = true; - }; - packages = with pkgs; [ - noto-fonts - noto-fonts-color-emoji - noto-fonts-cjk-sans - ]; - }; - - hardware = { - enableRedistributableFirmware = true; - graphics.enable = true; - }; - - networking = { - firewall.enable = false; - useNetworkd = true; - wireless = { - enable = true; - # Specify the interface explicitly so wpa_supplicant doesn't try to - # auto-detect via /sys/class/net, which is not mounted in the 26.05 - # hardening sandbox (RootDirectory=/run/wpa_supplicant). - interfaces = [ "wlp3s0" ]; - secretsFile = "${config.sops.secrets.wifi_creds.path}"; - }; - }; - + # Not part of genebean.kiosk-hardware: nixpkgs.overlays helps construct + # `pkgs` itself, so gating it behind a home-manager-sourced cfg.enable + # check creates a genuine circular dependency (evaluating cfg.enable + # pulls in pkgs, which depends on nixpkgs.overlays, which is what we're + # trying to compute) - confirmed via a real "infinite recursion" eval + # error when this was tried. Stays per-host, unconditional. nixpkgs.overlays = [ (_final: super: { makeModulesClosure = x: super.makeModulesClosure (x // { allowMissing = true; }); @@ -78,29 +48,6 @@ owner = "${username}"; path = "${config.users.users.${username}.home}/.private-env"; }; - wifi_creds = { - sopsFile = ../../../shared/secrets.yaml; - owner = "wpa_supplicant"; - restartUnits = [ - "wpa_supplicant-wlp3s0.service" - ]; - }; }; }; - - users.users.${username} = { - isNormalUser = true; - description = "Gene Liverman"; - extraGroups = [ - "networkmanager" - "wheel" - ]; - linger = true; - }; - - zramSwap = { - enable = true; - algorithm = "zstd"; - memoryPercent = 90; - }; } diff --git a/modules/hosts/nixos/kiosk-entryway/home-gene.nix b/modules/hosts/nixos/kiosk-entryway/home-gene.nix index d453d701..15edfc69 100644 --- a/modules/hosts/nixos/kiosk-entryway/home-gene.nix +++ b/modules/hosts/nixos/kiosk-entryway/home-gene.nix @@ -1,9 +1,24 @@ +{ + config, + ... +}: { home.stateVersion = "24.11"; - genebean.services.chromium-kiosk = { - enable = true; - dashboardUrl = "http://192.168.22.22:8123/kiosk-entryway/immich?kiosk"; - extraCommandLineArgs = [ "--hide-scrollbars" ]; + genebean = { + kiosk-hardware = { + enable = true; + wirelessInterface = "wlp3s0"; + }; + + services = { + chromium-kiosk = { + enable = true; + dashboardUrl = "http://192.168.22.22:8123/kiosk-entryway/immich?kiosk"; + extraCommandLineArgs = [ "--hide-scrollbars" ]; + wirelessInterface = config.genebean.kiosk-hardware.wirelessInterface; + }; + kiosk-backups.enable = true; + }; }; } diff --git a/modules/hosts/nixos/kiosk-gene-desk/default.nix b/modules/hosts/nixos/kiosk-gene-desk/default.nix index 2f0043ed..3711c744 100644 --- a/modules/hosts/nixos/kiosk-gene-desk/default.nix +++ b/modules/hosts/nixos/kiosk-gene-desk/default.nix @@ -1,6 +1,5 @@ { config, - lib, pkgs, username, ... @@ -25,11 +24,6 @@ variant = "4"; # bootloader defaults to "uboot" for variant 4 }; - supportedFilesystems = lib.mkForce [ - "ext4" - "f2fs" # /persist - "vfat" - ]; }; environment.systemPackages = with pkgs; [ @@ -37,24 +31,6 @@ raspberrypi-eeprom ]; - hardware = { - enableRedistributableFirmware = true; # wifi firmware, not boot firmware - graphics.enable = true; - }; - - networking = { - firewall.enable = false; - useNetworkd = true; - wireless = { - enable = true; - # Specify the interface explicitly so wpa_supplicant doesn't try to - # auto-detect via /sys/class/net, which is not mounted in the 26.05 - # hardening sandbox (RootDirectory=/run/wpa_supplicant). - interfaces = [ "wlan0" ]; - secretsFile = "${config.sops.secrets.wifi_creds.path}"; - }; - }; - nix = { distributedBuilds = true; buildMachines = [ @@ -78,6 +54,12 @@ ''; }; + # Not part of genebean.kiosk-hardware: nixpkgs.overlays helps construct + # `pkgs` itself, so gating it behind a home-manager-sourced cfg.enable + # check creates a genuine circular dependency (evaluating cfg.enable + # pulls in pkgs, which depends on nixpkgs.overlays, which is what we're + # trying to compute) - confirmed via a real "infinite recursion" eval + # error when this was tried. Stays per-host, unconditional. nixpkgs.overlays = [ (_final: super: { makeModulesClosure = x: super.makeModulesClosure (x // { allowMissing = true; }); @@ -156,29 +138,6 @@ mode = "0600"; restartUnits = [ "sshd.service" ]; }; - wifi_creds = { - sopsFile = ../../../shared/secrets.yaml; - owner = "wpa_supplicant"; - restartUnits = [ - "wpa_supplicant-wlan0.service" - ]; - }; }; }; - - users.users.${username} = { - isNormalUser = true; - description = "Gene Liverman"; - extraGroups = [ - "networkmanager" - "wheel" - ]; - linger = true; - }; - - zramSwap = { - enable = true; - algorithm = "zstd"; - memoryPercent = 90; - }; } diff --git a/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix b/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix index e56125e9..812b098b 100644 --- a/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix +++ b/modules/hosts/nixos/kiosk-gene-desk/home-gene.nix @@ -1,15 +1,26 @@ +{ + config, + ... +}: { home.stateVersion = "24.11"; - genebean.services = { - chromium-kiosk = { + genebean = { + kiosk-hardware = { enable = true; - dashboardUrl = "http://192.168.22.22:8123/kiosk-gene-desk/immich?kiosk"; - rotate = "90"; + wirelessInterface = "wlan0"; + }; + + services = { + chromium-kiosk = { + enable = true; + dashboardUrl = "http://192.168.22.22:8123/kiosk-gene-desk/immich?kiosk"; + rotate = "90"; + wirelessInterface = config.genebean.kiosk-hardware.wirelessInterface; + }; + kiosk-backups.enable = true; }; - tailscale.enable = true; }; programs.zsh.history.path = "/tmp/zsh_history_gene"; # needed becaues of read only fs - } diff --git a/modules/hosts/nixos/kiosk-gene-desk/persistence.nix b/modules/hosts/nixos/kiosk-gene-desk/persistence.nix index 315cc7d2..976cf673 100644 --- a/modules/hosts/nixos/kiosk-gene-desk/persistence.nix +++ b/modules/hosts/nixos/kiosk-gene-desk/persistence.nix @@ -84,82 +84,11 @@ "/persist".neededForBoot = true; }; - # Recovery tool for the restic paths below - see modules/genebean/nixos/ - # programs/kiosk-restic-full-restore.nix. Shared across every kiosk - # running this same cage+chromium(+tailscale) setup, not just this host. - genebean.programs.kiosk-restic-full-restore = { - enable = true; - restorePaths = config.services.restic.backups.daily.paths; - stopServices = [ - "cage-tty1" - "tailscaled" - ]; - }; - + # Backup policy (paths, pre-reinstall-cleanup, kiosk-restic-full-restore + # wiring) now lives in genebean.services.kiosk-backups - see + # ./home-gene.nix and modules/genebean/nixos/services/kiosk-backups.nix. # What's persisted above survives a reboot (still on the same SD card), - # but not a card death - only what's backed up here does. Everything - # else in the persist allowlist above either comes from sops/git - # (ssh host key, gene's ssh key, wifi creds - already safe, backed up - # by the repo itself) or is cheap to lose (.ssh/known_hosts, uid/gid - # allocations). These three are the ones that would actually be a - # hassle to reconstruct by hand on a kiosk with no keyboard normally - # attached: - # - .config/chromium: hass-browser_mod's device registration - # - .local/share/atuin: sync-server login session/key - # - /var/lib/tailscale: node identity (else: re-approve in the - # Tailscale admin console) - # All backed up from their real /persist-side paths directly rather - # than the impermanence bind-mounted aliases under $HOME, matching - # sops.age.keyFile's precedent in ./default.nix of preferring the real - # path over the alias. - services.restic.backups = { - daily = { - paths = [ - "/persist${config.users.users.${username}.home}/.config/chromium" - "/persist${config.users.users.${username}.home}/.local/share/atuin" - "/persist/var/lib/tailscale" - ]; - - # The shared module's default (Persistent = true) fires a catch-up - # backup right after every boot if the last scheduled run was - # missed - which, on a host that gets reinstalled/rebooted a lot, - # means it nearly always fires before there's a chance to restore - # fresh state after a reinstall. Confirmed on hardware: that catch- - # up run captured the empty post-reinstall chromium/atuin/tailscale - # state as today's newest snapshot, and --keep-daily 7 then - # legitimately forgot the real (pre-reinstall) same-day snapshot as - # superseded before a restore could happen. A missed day here is a - # non-issue, so just wait for the next scheduled run instead. - timerConfig = { - OnCalendar = "daily"; - Persistent = false; - }; - }; - - # Companion prune-only job (no `paths` - see the shared restic - # module's own docs on that) so pre-reinstall-tagged snapshots don't - # just pile up forever: anything tagged pre-reinstall older than 45 - # days is fair game again. --tag and --host together scope this - # forget run to ONLY this host's pre-reinstall-tagged snapshots - - # everything else (including any other host that might someday reuse - # the same tag) is untouched by this job. --keep-within is based on - # each snapshot's own (immutable) creation time, not when it was - # tagged. - pre-reinstall-cleanup = { - environmentFile = config.sops.secrets.restic_env.path; - passwordFile = config.sops.secrets.restic_password.path; - pruneOpts = [ - "--tag pre-reinstall" - "--host ${config.networking.hostName}" - "--keep-within 45d" - ]; - repositoryFile = config.sops.secrets.restic_repo.path; - timerConfig = { - OnCalendar = "daily"; - Persistent = false; - }; - }; - }; + # but not a card death - only what's backed up via that module does. systemd = { services = {