diff options
Diffstat (limited to 'nixos/modules')
99 files changed, 2251 insertions, 915 deletions
diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index e44cce11f3a8b..d1e9c8072eac4 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -428,6 +428,8 @@ let uidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) cfg.users) "uid"; gidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) cfg.groups) "gid"; + sdInitrdUidsAreUnique = idsAreUnique (filterAttrs (n: u: u.uid != null) config.boot.initrd.systemd.users) "uid"; + sdInitrdGidsAreUnique = idsAreUnique (filterAttrs (n: g: g.gid != null) config.boot.initrd.systemd.groups) "gid"; spec = pkgs.writeText "users-groups.json" (builtins.toJSON { inherit (cfg) mutableUsers; @@ -534,6 +536,54 @@ in { WARNING: enabling this can lock you out of your system. Enable this only if you know what are you doing. ''; }; + + # systemd initrd + boot.initrd.systemd.users = mkOption { + visible = false; + description = '' + Users to include in initrd. + ''; + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + options.uid = mkOption { + visible = false; + type = types.int; + description = '' + ID of the user in initrd. + ''; + defaultText = literalExpression "config.users.users.\${name}.uid"; + default = cfg.users.${name}.uid; + }; + options.group = mkOption { + visible = false; + type = types.singleLineStr; + description = '' + Group the user belongs to in initrd. + ''; + defaultText = literalExpression "config.users.users.\${name}.group"; + default = cfg.users.${name}.group; + }; + })); + }; + + boot.initrd.systemd.groups = mkOption { + visible = false; + description = '' + Groups to include in initrd. + ''; + default = {}; + type = types.attrsOf (types.submodule ({ name, ... }: { + options.gid = mkOption { + visible = false; + type = types.int; + description = '' + ID of the group in initrd. + ''; + defaultText = literalExpression "config.users.groups.\${name}.gid"; + default = cfg.groups.${name}.gid; + }; + })); + }; }; @@ -639,10 +689,52 @@ in { "/etc/profiles/per-user/$USER" ]; + # systemd initrd + boot.initrd.systemd = lib.mkIf config.boot.initrd.systemd.enable { + contents = { + "/etc/passwd".text = '' + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { uid, group }: let + g = config.boot.initrd.systemd.groups.${group}; + in "${n}:x:${toString uid}:${toString g.gid}::/var/empty:") config.boot.initrd.systemd.users)} + ''; + "/etc/group".text = '' + ${lib.concatStringsSep "\n" (lib.mapAttrsToList (n: { gid }: "${n}:x:${toString gid}:") config.boot.initrd.systemd.groups)} + ''; + }; + + users = { + root = {}; + nobody = {}; + }; + + groups = { + root = {}; + nogroup = {}; + systemd-journal = {}; + tty = {}; + dialout = {}; + kmem = {}; + input = {}; + video = {}; + render = {}; + sgx = {}; + audio = {}; + video = {}; + lp = {}; + disk = {}; + cdrom = {}; + tape = {}; + kvm = {}; + }; + }; + assertions = [ { assertion = !cfg.enforceIdUniqueness || (uidsAreUnique && gidsAreUnique); message = "UIDs and GIDs must be unique!"; } + { assertion = !cfg.enforceIdUniqueness || (sdInitrdUidsAreUnique && sdInitrdGidsAreUnique); + message = "systemd initrd UIDs and GIDs must be unique!"; + } { # If mutableUsers is false, to prevent users creating a # configuration that locks them out of the system, ensure that # there is at least one "privileged" account that has a diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix index 520db128acd9f..2a35afad2ac76 100644 --- a/nixos/modules/i18n/input-method/ibus.nix +++ b/nixos/modules/i18n/input-method/ibus.nix @@ -10,10 +10,7 @@ let check = x: (lib.types.package.check x) && (attrByPath ["meta" "isIbusEngine"] false x); }; - impanel = - if cfg.panel != null - then "--panel=${cfg.panel}" - else ""; + impanel = optionalString (cfg.panel != null) "--panel=${cfg.panel}"; ibusAutostart = pkgs.writeTextFile { name = "autostart-ibus-daemon"; diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix index 8fa070b03db31..ea17e2a705ede 100644 --- a/nixos/modules/installer/cd-dvd/iso-image.nix +++ b/nixos/modules/installer/cd-dvd/iso-image.nix @@ -22,8 +22,8 @@ let (option: '' menuentry '${defaults.name} ${ # Name appended to menuentry defaults to params if no specific name given. - option.name or (if option ? params then "(${option.params})" else "") - }' ${if option ? class then " --class ${option.class}" else ""} { + option.name or (optionalString (option ? params) "(${option.params})") + }' ${optionalString (option ? class) " --class ${option.class}"} { linux ${defaults.image} \''${isoboot} ${defaults.params} ${ option.params or "" } diff --git a/nixos/modules/installer/tools/nixos-generate-config.pl b/nixos/modules/installer/tools/nixos-generate-config.pl index 74972c0994bed..a082ed3450e96 100644 --- a/nixos/modules/installer/tools/nixos-generate-config.pl +++ b/nixos/modules/installer/tools/nixos-generate-config.pl @@ -200,7 +200,7 @@ sub pciCheck { } # In case this is a virtio scsi device, we need to explicitly make this available. - if ($vendor eq "0x1af4" && $device eq "0x1004") { + if ($vendor eq "0x1af4" && ($device eq "0x1004" || $device eq "0x1048") ) { push @initrdAvailableKernelModules, "virtio_scsi"; } diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index bac096efac2c9..f1c459f755708 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -149,6 +149,7 @@ ./programs/cdemu.nix ./programs/cfs-zen-tweaks.nix ./programs/chromium.nix + ./programs/clash-verge.nix ./programs/cnping.nix ./programs/command-not-found/command-not-found.nix ./programs/criu.nix @@ -514,6 +515,7 @@ ./services/hardware/usbrelayd.nix ./services/hardware/vdr.nix ./services/hardware/keyd.nix + ./services/home-automation/esphome.nix ./services/home-automation/evcc.nix ./services/home-automation/home-assistant.nix ./services/home-automation/zigbee2mqtt.nix @@ -668,6 +670,7 @@ ./services/misc/polaris.nix ./services/misc/portunus.nix ./services/misc/prowlarr.nix + ./services/misc/pufferpanel.nix ./services/misc/pykms.nix ./services/misc/radarr.nix ./services/misc/readarr.nix @@ -882,6 +885,7 @@ ./services/networking/iscsi/initiator.nix ./services/networking/iscsi/root-initiator.nix ./services/networking/iscsi/target.nix + ./services/networking/ivpn.nix ./services/networking/iwd.nix ./services/networking/jibri/default.nix ./services/networking/jicofo.nix @@ -1040,6 +1044,7 @@ ./services/networking/wg-netmanager.nix ./services/networking/webhook.nix ./services/networking/wg-quick.nix + ./services/networking/wgautomesh.nix ./services/networking/wireguard.nix ./services/networking/wpa_supplicant.nix ./services/networking/wstunnel.nix @@ -1130,7 +1135,7 @@ ./services/video/epgstation/default.nix ./services/video/mirakurun.nix ./services/video/replay-sorcery.nix - ./services/video/rtsp-simple-server.nix + ./services/video/mediamtx.nix ./services/video/unifi-video.nix ./services/video/v4l2-relayd.nix ./services/wayland/cage.nix @@ -1165,7 +1170,6 @@ ./services/web-apps/hledger-web.nix ./services/web-apps/icingaweb2/icingaweb2.nix ./services/web-apps/icingaweb2/module-monitoring.nix - ./services/web-apps/ihatemoney ./services/web-apps/invidious.nix ./services/web-apps/invoiceplane.nix ./services/web-apps/isso.nix @@ -1181,6 +1185,7 @@ ./services/web-apps/mattermost.nix ./services/web-apps/mediawiki.nix ./services/web-apps/miniflux.nix + ./services/web-apps/monica.nix ./services/web-apps/moodle.nix ./services/web-apps/netbox.nix ./services/web-apps/nextcloud.nix diff --git a/nixos/modules/programs/clash-verge.nix b/nixos/modules/programs/clash-verge.nix new file mode 100644 index 0000000000000..29977be3858f0 --- /dev/null +++ b/nixos/modules/programs/clash-verge.nix @@ -0,0 +1,41 @@ +{ config, lib, pkgs, ... }: + +{ + options.programs.clash-verge = { + enable = lib.mkEnableOption (lib.mdDoc '' + Clash Verge. + ''); + + autoStart = lib.mkEnableOption (lib.mdDoc '' + Clash Verge Auto Launch. + ''); + + tunMode = lib.mkEnableOption (lib.mdDoc '' + Clash Verge Tun Mode. + ''); + }; + + config = + let + cfg = config.programs.clash-verge; + in + lib.mkIf cfg.enable { + + environment.systemPackages = [ + pkgs.clash-verge + (lib.mkIf cfg.autoStart (pkgs.makeAutostartItem { + name = "clash-verge"; + package = pkgs.clash-verge; + })) + ]; + + security.wrappers.clash-verge = lib.mkIf cfg.tunMode { + owner = "root"; + group = "root"; + capabilities = "cap_net_bind_service,cap_net_admin=+ep"; + source = "${lib.getExe pkgs.clash-verge}"; + }; + }; + + meta.maintainers = with lib.maintainers; [ zendo ]; +} diff --git a/nixos/modules/programs/firefox.nix b/nixos/modules/programs/firefox.nix index 3a5105c57d767..ead048134d8d3 100644 --- a/nixos/modules/programs/firefox.nix +++ b/nixos/modules/programs/firefox.nix @@ -201,6 +201,7 @@ in nativeMessagingHosts = mapAttrs (_: v: mkEnableOption (mdDoc v)) { browserpass = "Browserpass support"; bukubrow = "Bukubrow support"; + euwebid = "Web eID support"; ff2mpv = "ff2mpv support"; fxCast = "fx_cast support"; gsconnect = "GSConnect support"; @@ -217,6 +218,8 @@ in extraPrefs = cfg.autoConfig; extraNativeMessagingHosts = with pkgs; optionals nmh.ff2mpv [ ff2mpv + ] ++ optionals nmh.euwebid [ + web-eid-app ] ++ optionals nmh.gsconnect [ gnomeExtensions.gsconnect ] ++ optionals nmh.jabref [ @@ -230,6 +233,7 @@ in nixpkgs.config.firefox = { enableBrowserpass = nmh.browserpass; enableBukubrow = nmh.bukubrow; + enableEUWebID = nmh.euwebid; enableTridactylNative = nmh.tridactyl; enableUgetIntegrator = nmh.ugetIntegrator; enableFXCastBridge = nmh.fxCast; diff --git a/nixos/modules/programs/less.nix b/nixos/modules/programs/less.nix index a1134e774364a..81c68307aee14 100644 --- a/nixos/modules/programs/less.nix +++ b/nixos/modules/programs/less.nix @@ -11,7 +11,7 @@ let ${concatStringsSep "\n" (mapAttrsToList (command: action: "${command} ${action}") cfg.commands) } - ${if cfg.clearDefaultCommands then "#stop" else ""} + ${optionalString cfg.clearDefaultCommands "#stop"} #line-edit ${concatStringsSep "\n" diff --git a/nixos/modules/programs/neovim.nix b/nixos/modules/programs/neovim.nix index 4562e5a2c29b8..3f0e9fc173bdf 100644 --- a/nixos/modules/programs/neovim.nix +++ b/nixos/modules/programs/neovim.nix @@ -4,12 +4,8 @@ with lib; let cfg = config.programs.neovim; - - runtime' = filter (f: f.enable) (attrValues cfg.runtime); - - runtime = pkgs.linkFarm "neovim-runtime" (map (x: { name = "etc/${x.target}"; path = x.source; }) runtime'); - -in { +in +{ options.programs.neovim = { enable = mkOption { type = types.bool; @@ -70,7 +66,7 @@ in { configure = mkOption { type = types.attrs; - default = {}; + default = { }; example = literalExpression '' { customRC = ''' @@ -105,7 +101,7 @@ in { }; runtime = mkOption { - default = {}; + default = { }; example = literalExpression '' { "ftplugin/c.vim".text = "setlocal omnifunc=v:lua.vim.lsp.omnifunc"; } ''; @@ -115,14 +111,15 @@ in { type = with types; attrsOf (submodule ( { name, config, ... }: - { options = { + { + options = { enable = mkOption { type = types.bool; default = true; description = lib.mdDoc '' - Whether this /etc file should be generated. This - option allows specific /etc files to be disabled. + Whether this runtime directory should be generated. This + option allows specific runtime files to be disabled. ''; }; @@ -147,14 +144,9 @@ in { }; - config = { - target = mkDefault name; - source = mkIf (config.text != null) ( - let name' = "neovim-runtime" + baseNameOf name; - in mkDefault (pkgs.writeText name' config.text)); - }; - - })); + config.target = mkDefault name; + } + )); }; }; @@ -165,14 +157,17 @@ in { ]; environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "nvim"); - programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package { - inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby; - configure = cfg.configure // { + environment.etc = listToAttrs (attrValues (mapAttrs + (name: value: { + name = "xdg/nvim/${name}"; + value = value // { + target = "xdg/nvim/${value.target}"; + }; + }) + cfg.runtime)); - customRC = (cfg.configure.customRC or "") + '' - set runtimepath^=${runtime}/etc - ''; - }; + programs.neovim.finalPackage = pkgs.wrapNeovim cfg.package { + inherit (cfg) viAlias vimAlias withPython3 withNodeJs withRuby configure; }; }; } diff --git a/nixos/modules/programs/regreet.nix b/nixos/modules/programs/regreet.nix index 89b93737f4a27..f6c750a45bf54 100644 --- a/nixos/modules/programs/regreet.nix +++ b/nixos/modules/programs/regreet.nix @@ -50,7 +50,7 @@ in config = lib.mkIf cfg.enable { services.greetd = { enable = lib.mkDefault true; - settings.default_session.command = lib.mkDefault "${lib.getExe pkgs.cage} -s -- ${lib.getExe cfg.package}"; + settings.default_session.command = lib.mkDefault "${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} -s -- ${lib.getExe cfg.package}"; }; environment.etc = { diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix index 1ec698820a8b0..7c85d1e7c3d51 100644 --- a/nixos/modules/programs/ssh.nix +++ b/nixos/modules/programs/ssh.nix @@ -26,7 +26,7 @@ let + (if h.publicKey != null then h.publicKey else readFile h.publicKeyFile) )) + "\n"; - knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" "/etc/ssh/ssh_known_hosts2" ] + knownHostsFiles = [ "/etc/ssh/ssh_known_hosts" ] ++ map pkgs.copyPathToStore cfg.knownHostsFiles; in @@ -232,9 +232,8 @@ in description = lib.mdDoc '' Files containing SSH host keys to set as global known hosts. `/etc/ssh/ssh_known_hosts` (which is - generated by {option}`programs.ssh.knownHosts`) and - `/etc/ssh/ssh_known_hosts2` are always - included. + generated by {option}`programs.ssh.knownHosts`) is + always included. ''; example = literalExpression '' [ diff --git a/nixos/modules/programs/tmux.nix b/nixos/modules/programs/tmux.nix index 4fb9175fb8d21..4f452f1d7f9b5 100644 --- a/nixos/modules/programs/tmux.nix +++ b/nixos/modules/programs/tmux.nix @@ -1,7 +1,7 @@ { config, pkgs, lib, ... }: let - inherit (lib) mkOption mkIf types; + inherit (lib) mkOption mkIf types optionalString; cfg = config.programs.tmux; @@ -17,17 +17,17 @@ let set -g base-index ${toString cfg.baseIndex} setw -g pane-base-index ${toString cfg.baseIndex} - ${if cfg.newSession then "new-session" else ""} + ${optionalString cfg.newSession "new-session"} - ${if cfg.reverseSplit then '' + ${optionalString cfg.reverseSplit '' bind v split-window -h bind s split-window -v - '' else ""} + ''} set -g status-keys ${cfg.keyMode} set -g mode-keys ${cfg.keyMode} - ${if cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize then '' + ${optionalString (cfg.keyMode == "vi" && cfg.customPaneNavigationAndResize) '' bind h select-pane -L bind j select-pane -D bind k select-pane -U @@ -37,15 +37,15 @@ let bind -r J resize-pane -D ${toString cfg.resizeAmount} bind -r K resize-pane -U ${toString cfg.resizeAmount} bind -r L resize-pane -R ${toString cfg.resizeAmount} - '' else ""} + ''} - ${if (cfg.shortcut != defaultShortcut) then '' + ${optionalString (cfg.shortcut != defaultShortcut) '' # rebind main key: C-${cfg.shortcut} unbind C-${defaultShortcut} set -g prefix C-${cfg.shortcut} bind ${cfg.shortcut} send-prefix bind C-${cfg.shortcut} last-window - '' else ""} + ''} setw -g aggressive-resize ${boolToStr cfg.aggressiveResize} setw -g clock-mode-style ${if cfg.clock24 then "24" else "12"} @@ -160,7 +160,10 @@ in { default = defaultTerminal; example = "screen-256color"; type = types.str; - description = lib.mdDoc "Set the $TERM variable."; + description = lib.mdDoc '' + Set the $TERM variable. Use tmux-direct if italics or 24bit true color + support is needed. + ''; }; secureSocket = mkOption { diff --git a/nixos/modules/programs/zsh/zsh.nix b/nixos/modules/programs/zsh/zsh.nix index 0b152e54cf95f..6bb21cb3ef66f 100644 --- a/nixos/modules/programs/zsh/zsh.nix +++ b/nixos/modules/programs/zsh/zsh.nix @@ -236,6 +236,9 @@ in setopt ${concatStringsSep " " cfg.setOptions} ''} + # Alternative method of determining short and full hostname. + HOST=${config.networking.fqdnOrHostName} + # Setup command line history. # Don't export these, otherwise other shells (bash) will try to use same HISTFILE. SAVEHIST=${toString cfg.histSize} diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix index 158c7934195be..45a27029dff19 100644 --- a/nixos/modules/rename.nix +++ b/nixos/modules/rename.nix @@ -58,6 +58,7 @@ with lib; (mkRemovedOptionModule [ "services" "fourStoreEndpoint" ] "The fourStoreEndpoint module has been removed") (mkRemovedOptionModule [ "services" "fprot" ] "The corresponding package was removed from nixpkgs.") (mkRemovedOptionModule [ "services" "frab" ] "The frab module has been removed") + (mkRemovedOptionModule [ "services" "ihatemoney" ] "The ihatemoney module has been removed for lack of downstream maintainer") (mkRemovedOptionModule [ "services" "kippo" ] "The corresponding package was removed from nixpkgs.") (mkRemovedOptionModule [ "services" "mailpile" ] "The corresponding package was removed from nixpkgs.") (mkRemovedOptionModule [ "services" "marathon" ] "The corresponding package was removed from nixpkgs.") @@ -106,6 +107,7 @@ with lib; (mkRemovedOptionModule [ "services" "openfire" ] "The corresponding package was removed from nixpkgs.") (mkRemovedOptionModule [ "services" "riak" ] "The corresponding package was removed from nixpkgs.") (mkRemovedOptionModule [ "services" "cryptpad" ] "The corresponding package was removed from nixpkgs.") + (mkRemovedOptionModule [ "services" "rtsp-simple-server" ] "Package has been completely rebranded by upstream as mediamtx, and thus the service and the package were renamed in NixOS as well.") (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx" ] "The fcitx module has been removed. Plesae use fcitx5 instead") diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index ef0636258994c..66d1d2c5d9c5b 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -781,11 +781,11 @@ in { # FIXME Most of these custom warnings and filters for security.acme.certs.* are required # because using mkRemovedOptionModule/mkChangedOptionModule with attrsets isn't possible. - warnings = filter (w: w != "") (mapAttrsToList (cert: data: if data.extraDomains != "_mkMergedOptionModule" then '' + warnings = filter (w: w != "") (mapAttrsToList (cert: data: optionalString (data.extraDomains != "_mkMergedOptionModule") '' The option definition `security.acme.certs.${cert}.extraDomains` has changed to `security.acme.certs.${cert}.extraDomainNames` and is now a list of strings. Setting a custom webroot for extra domains is not possible, instead use separate certs. - '' else "") cfg.certs); + '') cfg.certs); assertions = let certs = attrValues cfg.certs; diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix index 2af42eeb3705b..dbab741bf6fc7 100644 --- a/nixos/modules/services/audio/snapserver.nix +++ b/nixos/modules/services/audio/snapserver.nix @@ -275,9 +275,9 @@ in { warnings = # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85 - filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then '' + filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") '' services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead. - '' else "") cfg.streams); + '') cfg.streams); systemd.services.snapserver = { after = [ "network.target" ]; diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix index e7cd6ae4bb573..5ee036e68c7bc 100644 --- a/nixos/modules/services/backup/borgmatic.nix +++ b/nixos/modules/services/backup/borgmatic.nix @@ -72,5 +72,8 @@ in cfg.configurations; systemd.packages = [ pkgs.borgmatic ]; + + # Workaround: https://github.com/NixOS/nixpkgs/issues/81138 + systemd.timers.borgmatic.wantedBy = [ "timers.target" ]; }; } diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix index 289291c6bd2f9..9fbc599cd41af 100644 --- a/nixos/modules/services/backup/mysql-backup.nix +++ b/nixos/modules/services/backup/mysql-backup.nix @@ -20,7 +20,7 @@ let ''; backupDatabaseScript = db: '' dest="${cfg.location}/${db}.gz" - if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then + if ${mariadb}/bin/mysqldump ${optionalString cfg.singleTransaction "--single-transaction"} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then mv $dest.tmp $dest echo "Backed up to $dest" else diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index ca796cf7797e6..d19b98a3e4bbb 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -300,7 +300,7 @@ in filesFromTmpFile = "/run/restic-backups-${name}/includes"; backupPaths = if (backup.dynamicFilesFrom == null) - then if (backup.paths != null) then concatStringsSep " " backup.paths else "" + then optionalString (backup.paths != null) (concatStringsSep " " backup.paths) else "--files-from ${filesFromTmpFile}"; pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts)) diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix index eca308dc366d1..d12516ca2f249 100644 --- a/nixos/modules/services/blockchain/ethereum/geth.nix +++ b/nixos/modules/services/blockchain/ethereum/geth.nix @@ -196,9 +196,9 @@ in --gcmode ${cfg.gcmode} \ --port ${toString cfg.port} \ --maxpeers ${toString cfg.maxpeers} \ - ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \ + ${optionalString cfg.http.enable ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}''} \ ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \ - ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \ + ${optionalString cfg.websocket.enable ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}''} \ ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \ ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \ --authrpc.addr ${cfg.authrpc.address} --authrpc.port ${toString cfg.authrpc.port} --authrpc.vhosts ${lib.concatStringsSep "," cfg.authrpc.vhosts} \ diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix index 5fb715f4d779a..1879fef9666f7 100644 --- a/nixos/modules/services/computing/boinc/client.nix +++ b/nixos/modules/services/computing/boinc/client.nix @@ -6,7 +6,7 @@ let cfg = config.services.boinc; allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc"; - fhsEnv = pkgs.buildFHSUserEnv { + fhsEnv = pkgs.buildFHSEnv { name = "boinc-fhs-env"; targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages; runScript = "/bin/boinc_client"; diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix index 3a1c6c1a371df..d6a8c2a3f7cce 100644 --- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix +++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix @@ -242,7 +242,7 @@ in { jobdir="${jenkinsCfg.home}/$jenkinsjobname" rm -rf "$jobdir" done - '' + (if cfg.accessUser != "" then reloadScript else ""); + '' + (optionalString (cfg.accessUser != "") reloadScript); serviceConfig = { Type = "oneshot"; User = jenkinsCfg.user; diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix index 8c64e3d9a5605..74f56f5890fce 100644 --- a/nixos/modules/services/development/lorri.nix +++ b/nixos/modules/services/development/lorri.nix @@ -50,6 +50,6 @@ in { }; }; - environment.systemPackages = [ cfg.package ]; + environment.systemPackages = [ cfg.package pkgs.direnv ]; }; } diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix index e8c96881673b5..578364ec542bb 100644 --- a/nixos/modules/services/games/minetest-server.nix +++ b/nixos/modules/services/games/minetest-server.nix @@ -4,7 +4,7 @@ with lib; let cfg = config.services.minetest-server; - flag = val: name: if val != null then "--${name} ${toString val} " else ""; + flag = val: name: optionalString (val != null) "--${name} ${toString val} "; flags = [ (flag cfg.gameId "gameid") (flag cfg.world "world") diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix index d95261332419d..95c2a4fc5c3e1 100644 --- a/nixos/modules/services/hardware/udev.nix +++ b/nixos/modules/services/hardware/udev.nix @@ -16,16 +16,6 @@ let ''; - # networkd link files are used early by udev to set up interfaces early. - # This must be done in stage 1 to avoid race conditions between udev and - # network daemons. - # TODO move this into the initrd-network module when it exists - initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} '' - mkdir -p $out - ln -s ${udev}/lib/systemd/network/*.link $out/ - ${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))} - ''; - extraUdevRules = pkgs.writeTextFile { name = "extra-udev-rules"; text = cfg.extraRules; @@ -398,7 +388,6 @@ in systemd = config.boot.initrd.systemd.package; binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ]; }; - "/etc/systemd/network".source = initrdLinkUnits; }; # Insert initrd rules boot.initrd.services.udev.packages = [ diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix new file mode 100644 index 0000000000000..d7dbb6f0b90e3 --- /dev/null +++ b/nixos/modules/services/home-automation/esphome.nix @@ -0,0 +1,136 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + literalExpression + maintainers + mkEnableOption + mkIf + mkOption + mdDoc + types + ; + + cfg = config.services.esphome; + + stateDir = "/var/lib/esphome"; + + esphomeParams = + if cfg.enableUnixSocket + then "--socket /run/esphome/esphome.sock" + else "--address ${cfg.address} --port ${toString cfg.port}"; +in +{ + meta.maintainers = with maintainers; [ oddlama ]; + + options.services.esphome = { + enable = mkEnableOption (mdDoc "esphome"); + + package = mkOption { + type = types.package; + default = pkgs.esphome; + defaultText = literalExpression "pkgs.esphome"; + description = mdDoc "The package to use for the esphome command."; + }; + + enableUnixSocket = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Listen on a unix socket `/run/esphome/esphome.sock` instead of the TCP port."; + }; + + address = mkOption { + type = types.str; + default = "localhost"; + description = mdDoc "esphome address"; + }; + + port = mkOption { + type = types.port; + default = 6052; + description = mdDoc "esphome port"; + }; + + openFirewall = mkOption { + default = false; + type = types.bool; + description = mdDoc "Whether to open the firewall for the specified port."; + }; + + allowedDevices = mkOption { + default = ["char-ttyS" "char-ttyUSB"]; + example = ["/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"]; + description = lib.mdDoc '' + A list of device nodes to which {command}`esphome` has access to. + Refer to DeviceAllow in systemd.resource-control(5) for more information. + Beware that if a device is referred to by an absolute path instead of a device category, + it will only allow devices that already are plugged in when the service is started. + ''; + type = types.listOf types.str; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port]; + + systemd.services.esphome = { + description = "ESPHome dashboard"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + path = [cfg.package]; + + # platformio fails to determine the home directory when using DynamicUser + environment.PLATFORMIO_CORE_DIR = "${stateDir}/.platformio"; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/esphome dashboard ${esphomeParams} ${stateDir}"; + DynamicUser = true; + User = "esphome"; + Group = "esphome"; + WorkingDirectory = stateDir; + StateDirectory = "esphome"; + StateDirectoryMode = "0750"; + Restart = "on-failure"; + RuntimeDirectory = mkIf cfg.enableUnixSocket "esphome"; + RuntimeDirectoryMode = "0750"; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + DevicePolicy = "closed"; + DeviceAllow = map (d: "${d} rw") cfg.allowedDevices; + SupplementaryGroups = ["dialout"]; + #NoNewPrivileges = true; # Implied by DynamicUser + PrivateUsers = true; + #PrivateTmp = true; # Implied by DynamicUser + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + #RemoveIPC = true; # Implied by DynamicUser + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + RestrictNamespaces = false; # Required by platformio for chroot + RestrictRealtime = true; + #RestrictSUIDSGID = true; # Implied by DynamicUser + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@mount" # Required by platformio for chroot + ]; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix index b056f96c3630b..342ac5ec6e049 100644 --- a/nixos/modules/services/logging/logrotate.nix +++ b/nixos/modules/services/logging/logrotate.nix @@ -83,9 +83,8 @@ let }; mailOption = - if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings) - then "--mail=${pkgs.mailutils}/bin/mail" - else ""; + optionalString (foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)) + "--mail=${pkgs.mailutils}/bin/mail"; in { imports = [ diff --git a/nixos/modules/services/logging/syslogd.nix b/nixos/modules/services/logging/syslogd.nix index 43969402588db..553973e255f7e 100644 --- a/nixos/modules/services/logging/syslogd.nix +++ b/nixos/modules/services/logging/syslogd.nix @@ -7,7 +7,7 @@ let cfg = config.services.syslogd; syslogConf = pkgs.writeText "syslog.conf" '' - ${if (cfg.tty != "") then "kern.warning;*.err;authpriv.none /dev/${cfg.tty}" else ""} + ${optionalString (cfg.tty != "") "kern.warning;*.err;authpriv.none /dev/${cfg.tty}"} ${cfg.defaultConfig} ${cfg.extraConfig} ''; diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix index 5f3a9b56292d2..d0b525bcb0027 100644 --- a/nixos/modules/services/mail/maddy.nix +++ b/nixos/modules/services/mail/maddy.nix @@ -228,8 +228,8 @@ in { default = []; description = lib.mdDoc '' List of IMAP accounts which get automatically created. Note that for - a complete setup, user credentials for these accounts are required too - and can be created using the command `maddyctl creds`. + a complete setup, user credentials for these accounts are required + and can be created using the `ensureCredentials` option. This option does not delete accounts which are not (anymore) listed. ''; example = [ @@ -238,6 +238,33 @@ in { ]; }; + ensureCredentials = mkOption { + default = {}; + description = lib.mdDoc '' + List of user accounts which get automatically created if they don't + exist yet. Note that for a complete setup, corresponding mail boxes + have to get created using the `ensureAccounts` option. + This option does not delete accounts which are not (anymore) listed. + ''; + example = { + "user1@localhost".passwordFile = /secrets/user1-localhost; + "user2@localhost".passwordFile = /secrets/user2-localhost; + }; + type = types.attrsOf (types.submodule { + options = { + passwordFile = mkOption { + type = types.path; + example = "/path/to/file"; + default = null; + description = lib.mdDoc '' + Specifies the path to a file containing the + clear text password for the user. + ''; + }; + }; + }); + }; + }; }; @@ -265,6 +292,13 @@ in { fi '') cfg.ensureAccounts} ''} + ${optionalString (cfg.ensureCredentials != {}) '' + ${concatStringsSep "\n" (mapAttrsToList (name: cfg: '' + if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then + ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${escapeShellArg cfg.passwordFile}) ${name} + fi + '') cfg.ensureCredentials)} + ''} ''; serviceConfig = { Type = "oneshot"; diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix index 852340c05aa7a..23c47aaca7e23 100644 --- a/nixos/modules/services/mail/postfix.nix +++ b/nixos/modules/services/mail/postfix.nix @@ -234,7 +234,7 @@ let headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks; - aliases = let separator = if cfg.aliasMapType == "hash" then ":" else ""; in + aliases = let separator = optionalString (cfg.aliasMapType == "hash") ":"; in optionalString (cfg.postmasterAlias != "") '' postmaster${separator} ${cfg.postmasterAlias} '' diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix index 7b6d82219298c..3aaec145930db 100644 --- a/nixos/modules/services/mail/roundcube.nix +++ b/nixos/modules/services/mail/roundcube.nix @@ -7,7 +7,7 @@ let fpm = config.services.phpfpm.pools.roundcube; localDB = cfg.database.host == "localhost"; user = cfg.database.username; - phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled); + phpWithPspell = pkgs.php81.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled); in { options.services.roundcube = { diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix index 83f4efe695a27..eff725f5a8685 100644 --- a/nixos/modules/services/misc/gammu-smsd.nix +++ b/nixos/modules/services/misc/gammu-smsd.nix @@ -10,7 +10,7 @@ let Connection = ${cfg.device.connection} SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"} LogFormat = ${cfg.log.format} - ${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""} + ${optionalString (cfg.device.pin != null) "PIN = ${cfg.device.pin}"} ${cfg.extraConfig.gammu} @@ -33,10 +33,10 @@ let ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") ( with cfg.backend; '' Driver = ${sql.driver} - ${if (sql.database!= null) then "Database = ${sql.database}" else ""} - ${if (sql.host != null) then "Host = ${sql.host}" else ""} - ${if (sql.user != null) then "User = ${sql.user}" else ""} - ${if (sql.password != null) then "Password = ${sql.password}" else ""} + ${optionalString (sql.database!= null) "Database = ${sql.database}"} + ${optionalString (sql.host != null) "Host = ${sql.host}"} + ${optionalString (sql.user != null) "User = ${sql.user}"} + ${optionalString (sql.password != null) "Password = ${sql.password}"} '')} ${cfg.extraConfig.smsd} diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix index e019e431a1890..a5d7a73dd06aa 100644 --- a/nixos/modules/services/misc/gitea.nix +++ b/nixos/modules/services/misc/gitea.nix @@ -26,9 +26,18 @@ in imports = [ (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ]) (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ]) + (mkRenamedOptionModule [ "services" "gitea" "domain" ] [ "services" "gitea" "settings" "server" "DOMAIN" ]) + (mkRenamedOptionModule [ "services" "gitea" "httpAddress" ] [ "services" "gitea" "settings" "server" "HTTP_ADDR" ]) + (mkRenamedOptionModule [ "services" "gitea" "httpPort" ] [ "services" "gitea" "settings" "server" "HTTP_PORT" ]) (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ]) (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ]) + (mkRenamedOptionModule [ "services" "gitea" "rootUrl" ] [ "services" "gitea" "settings" "server" "ROOT_URL" ]) (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ]) + (mkRenamedOptionModule [ "services" "gitea" "staticRootPath" ] [ "services" "gitea" "settings" "server" "STATIC_ROOT_PATH" ]) + + (mkChangedOptionModule [ "services" "gitea" "enableUnixSocket" ] [ "services" "gitea" "settings" "server" "PROTOCOL" ] ( + config: if config.services.gitea.enableUnixSocket then "http+unix" else "http" + )) (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted") ]; @@ -57,7 +66,14 @@ in stateDir = mkOption { default = "/var/lib/gitea"; type = types.str; - description = lib.mdDoc "gitea data directory."; + description = lib.mdDoc "Gitea data directory."; + }; + + customDir = mkOption { + default = "${cfg.stateDir}/custom"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"''; + type = types.str; + description = lib.mdDoc "Gitea custom directory. Used for config, custom templates and other options."; }; user = mkOption { @@ -66,6 +82,12 @@ in description = lib.mdDoc "User account under which gitea runs."; }; + group = mkOption { + type = types.str; + default = "gitea"; + description = lib.mdDoc "Group under which gitea runs."; + }; + database = { type = mkOption { type = types.enum [ "sqlite3" "mysql" "postgres" ]; @@ -216,44 +238,6 @@ in description = lib.mdDoc "Path to the git repositories."; }; - domain = mkOption { - type = types.str; - default = "localhost"; - description = lib.mdDoc "Domain name of your server."; - }; - - rootUrl = mkOption { - type = types.str; - default = "http://localhost:3000/"; - description = lib.mdDoc "Full public URL of gitea server."; - }; - - httpAddress = mkOption { - type = types.str; - default = "0.0.0.0"; - description = lib.mdDoc "HTTP listen address."; - }; - - httpPort = mkOption { - type = types.port; - default = 3000; - description = lib.mdDoc "HTTP listen port."; - }; - - enableUnixSocket = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port."; - }; - - staticRootPath = mkOption { - type = types.either types.str types.path; - default = cfg.package.data; - defaultText = literalExpression "package.data"; - example = "/var/lib/gitea/data"; - description = lib.mdDoc "Upper level of template and static files path."; - }; - mailerPasswordFile = mkOption { type = types.nullOr types.str; default = null; @@ -285,7 +269,7 @@ in }; } ''; - type = with types; submodule { + type = types.submodule { freeformType = format.type; options = { log = { @@ -303,6 +287,46 @@ in }; server = { + PROTOCOL = mkOption { + type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ]; + default = "http"; + description = lib.mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."''; + }; + + HTTP_ADDR = mkOption { + type = types.either types.str types.path; + default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"; + defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"''; + description = lib.mdDoc "Listen address. Must be a path when using a unix socket."; + }; + + HTTP_PORT = mkOption { + type = types.port; + default = 3000; + description = lib.mdDoc "Listen port. Ignored when using a unix socket."; + }; + + DOMAIN = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Domain name of your server."; + }; + + ROOT_URL = mkOption { + type = types.str; + default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/"; + defaultText = literalExpression ''"http://''${config.services.gitea.settings.server.DOMAIN}:''${toString config.services.gitea.settings.server.HTTP_PORT}/"''; + description = lib.mdDoc "Full public URL of gitea server."; + }; + + STATIC_ROOT_PATH = mkOption { + type = types.either types.str types.path; + default = cfg.package.data; + defaultText = literalExpression "config.${opt.package}.data"; + example = "/var/lib/gitea/data"; + description = lib.mdDoc "Upper level of template and static files path."; + }; + DISABLE_SSH = mkOption { type = types.bool; default = false; @@ -359,7 +383,7 @@ in config = mkIf cfg.enable { assertions = [ - { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user; + { assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user; message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned"; } ]; @@ -389,26 +413,10 @@ in ROOT = cfg.repositoryRoot; }; - server = mkMerge [ - { - DOMAIN = cfg.domain; - STATIC_ROOT_PATH = toString cfg.staticRootPath; - LFS_JWT_SECRET = "#lfsjwtsecret#"; - ROOT_URL = cfg.rootUrl; - } - (mkIf cfg.enableUnixSocket { - PROTOCOL = "http+unix"; - HTTP_ADDR = "/run/gitea/gitea.sock"; - }) - (mkIf (!cfg.enableUnixSocket) { - HTTP_ADDR = cfg.httpAddress; - HTTP_PORT = cfg.httpPort; - }) - (mkIf cfg.lfs.enable { - LFS_START_SERVER = true; - }) - - ]; + server = mkIf cfg.lfs.enable { + LFS_START_SERVER = true; + LFS_JWT_SECRET = "#lfsjwtsecret#"; + }; session = { COOKIE_NAME = lib.mkDefault "session"; @@ -428,7 +436,7 @@ in JWT_SECRET = "#oauth2jwtsecret#"; }; - lfs = mkIf (cfg.lfs.enable) { + lfs = mkIf cfg.lfs.enable { PATH = cfg.lfs.contentDir; }; }; @@ -457,33 +465,35 @@ in }; systemd.tmpfiles.rules = [ - "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -" - "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -" - "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -" - "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -" - "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -" - "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -" - "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -" - "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -" - "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -" - "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/data' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/data' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -" - "Z '${cfg.stateDir}' - ${cfg.user} gitea - -" + "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.dump.backupDir}' - ${cfg.user} ${cfg.group} - -" + "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.repositoryRoot}' - ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -" # If we have a folder or symlink with gitea locales, remove it # And symlink the current gitea locales in place "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" + + ] ++ lib.optionals cfg.lfs.enable [ + "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.lfs.contentDir}' - ${cfg.user} ${cfg.group} - -" ]; systemd.services.gitea = { @@ -500,47 +510,52 @@ in # lfs_jwt_secret. # We have to consider this to stay compatible with older installations. preStart = let - runConfig = "${cfg.stateDir}/custom/conf/app.ini"; - secretKey = "${cfg.stateDir}/custom/conf/secret_key"; - oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret"; - oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET - lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET - internalToken = "${cfg.stateDir}/custom/conf/internal_token"; + runConfig = "${cfg.customDir}/conf/app.ini"; + secretKey = "${cfg.customDir}/conf/secret_key"; + oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret"; + oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET + lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET + internalToken = "${cfg.customDir}/conf/internal_token"; replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; in '' - # copy custom configuration and generate a random secret key if needed + # copy custom configuration and generate random secrets if needed ${optionalString (!cfg.useWizard) '' function gitea_setup { - cp -f ${configFile} ${runConfig} + cp -f '${configFile}' '${runConfig}' - if [ ! -s ${secretKey} ]; then - ${exe} generate secret SECRET_KEY > ${secretKey} + if [ ! -s '${secretKey}' ]; then + ${exe} generate secret SECRET_KEY > '${secretKey}' fi # Migrate LFS_JWT_SECRET filename - if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then - mv ${oldLfsJwtSecret} ${lfsJwtSecret} + if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then + mv '${oldLfsJwtSecret}' '${lfsJwtSecret}' fi - if [ ! -s ${oauth2JwtSecret} ]; then - ${exe} generate secret JWT_SECRET > ${oauth2JwtSecret} + if [ ! -s '${oauth2JwtSecret}' ]; then + ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}' fi - if [ ! -s ${lfsJwtSecret} ]; then - ${exe} generate secret LFS_JWT_SECRET > ${lfsJwtSecret} + ${lib.optionalString cfg.lfs.enable '' + if [ ! -s '${lfsJwtSecret}' ]; then + ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}' fi + ''} - if [ ! -s ${internalToken} ]; then - ${exe} generate secret INTERNAL_TOKEN > ${internalToken} + if [ ! -s '${internalToken}' ]; then + ${exe} generate secret INTERNAL_TOKEN > '${internalToken}' fi chmod u+w '${runConfig}' ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' - ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' + ${lib.optionalString cfg.lfs.enable '' + ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'" + ''} + ${lib.optionalString (cfg.mailerPasswordFile != null) '' ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' ''} @@ -565,7 +580,7 @@ in serviceConfig = { Type = "simple"; User = cfg.user; - Group = "gitea"; + Group = cfg.group; WorkingDirectory = cfg.stateDir; ExecStart = "${exe} web --pid /run/gitea/gitea.pid"; Restart = "always"; @@ -573,7 +588,7 @@ in RuntimeDirectory = "gitea"; RuntimeDirectoryMode = "0755"; # Access write directories - ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; + ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; UMask = "0027"; # Capabilities CapabilityBoundingSet = ""; @@ -606,6 +621,7 @@ in USER = cfg.user; HOME = cfg.stateDir; GITEA_WORK_DIR = cfg.stateDir; + GITEA_CUSTOM = cfg.customDir; }; }; @@ -614,12 +630,14 @@ in description = "Gitea Service"; home = cfg.stateDir; useDefaultShell = true; - group = "gitea"; + group = cfg.group; isSystemUser = true; }; }; - users.groups.gitea = {}; + users.groups = mkIf (cfg.group == "gitea") { + gitea = {}; + }; warnings = optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++ diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index d278b571a6410..12c67c5f5a1e7 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -1215,7 +1215,7 @@ in { enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly extraConfig = { auth.token = { - realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth"; + realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth"; service = cfg.registry.serviceName; issuer = cfg.registry.issuer; rootcertbundle = cfg.registry.certFile; diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix index 1a6b54854d1cd..e75c352541438 100644 --- a/nixos/modules/services/misc/mbpfan.nix +++ b/nixos/modules/services/misc/mbpfan.nix @@ -3,7 +3,7 @@ with lib; let cfg = config.services.mbpfan; - verbose = if cfg.verbose then "v" else ""; + verbose = optionalString cfg.verbose "v"; settingsFormat = pkgs.formats.ini {}; settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings; diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix new file mode 100644 index 0000000000000..78ec356469076 --- /dev/null +++ b/nixos/modules/services/misc/pufferpanel.nix @@ -0,0 +1,176 @@ +{ config, pkgs, lib, ... }: +let + cfg = config.services.pufferpanel; +in +{ + options.services.pufferpanel = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable PufferPanel game management server. + + Note that [PufferPanel templates] and binaries downloaded by PufferPanel + expect [FHS environment]. It is possible to set {option}`package` option + to use PufferPanel wrapper with FHS environment. For example, to use + `Download Game from Steam` and `Download Java` template operations: + ```Nix + { lib, pkgs, ... }: { + services.pufferpanel = { + enable = true; + extraPackages = with pkgs; [ bash curl gawk gnutar gzip ]; + package = pkgs.buildFHSUserEnv { + name = "pufferpanel-fhs"; + runScript = lib.getExe pkgs.pufferpanel; + targetPkgs = pkgs': with pkgs'; [ icu openssl zlib ]; + }; + }; + } + ``` + + [PufferPanel templates]: https://github.com/PufferPanel/templates + [FHS environment]: https://wikipedia.org/wiki/Filesystem_Hierarchy_Standard + ''; + }; + + package = lib.mkPackageOptionMD pkgs "pufferpanel" { }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "podman" ]; + description = lib.mdDoc '' + Additional groups for the systemd service. + ''; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + example = lib.literalExpression "[ pkgs.jre ]"; + description = lib.mdDoc '' + Packages to add to the PATH environment variable. Both the {file}`bin` + and {file}`sbin` subdirectories of each package are added. + ''; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = lib.literalExpression '' + { + PUFFER_WEB_HOST = ":8080"; + PUFFER_DAEMON_SFTP_HOST = ":5657"; + PUFFER_DAEMON_CONSOLE_BUFFER = "1000"; + PUFFER_DAEMON_CONSOLE_FORWARD = "true"; + PUFFER_PANEL_REGISTRATIONENABLED = "false"; + } + ''; + description = lib.mdDoc '' + Environment variables to set for the service. Secrets should be + specified using {option}`environmentFile`. + + Refer to the [PufferPanel source code][] for the list of available + configuration options. Variable name is an upper-cased configuration + entry name with underscores instead of dots, prefixed with `PUFFER_`. + For example, `panel.settings.companyName` entry can be set using + {env}`PUFFER_PANEL_SETTINGS_COMPANYNAME`. + + When running with panel enabled (configured with `PUFFER_PANEL_ENABLE` + environment variable), it is recommended disable registration using + `PUFFER_PANEL_REGISTRATIONENABLED` environment variable (registration is + enabled by default). To create the initial administrator user, run + {command}`pufferpanel --workDir /var/lib/pufferpanel user add --admin`. + + Some options override corresponding settings set via web interface (e.g. + `PUFFER_PANEL_REGISTRATIONENABLED`). Those options can be temporarily + toggled or set in settings but do not persist between restarts. + + [PufferPanel source code]: https://github.com/PufferPanel/PufferPanel/blob/master/config/entries.go + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + File to load environment variables from. Loaded variables override + values set in {option}`environment`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.pufferpanel = { + description = "PufferPanel game management server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + path = cfg.extraPackages; + environment = cfg.environment; + + # Note that we export environment variables for service directories if the + # value is not set. An empty environment variable is considered to be set. + # E.g. + # export PUFFER_LOGS=${PUFFER_LOGS-$LOGS_DIRECTORY} + # would set PUFFER_LOGS to $LOGS_DIRECTORY if PUFFER_LOGS environment + # variable is not defined. + script = '' + ${lib.concatLines (lib.mapAttrsToList (name: value: '' + export ${name}="''${${name}-${value}}" + '') { + PUFFER_LOGS = "$LOGS_DIRECTORY"; + PUFFER_DAEMON_DATA_CACHE = "$CACHE_DIRECTORY"; + PUFFER_DAEMON_DATA_SERVERS = "$STATE_DIRECTORY/servers"; + PUFFER_DAEMON_DATA_BINARIES = "$STATE_DIRECTORY/binaries"; + })} + exec ${lib.getExe cfg.package} run --workDir "$STATE_DIRECTORY" + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + UMask = "0077"; + + SupplementaryGroups = cfg.extraGroups; + + StateDirectory = "pufferpanel"; + StateDirectoryMode = "0700"; + CacheDirectory = "pufferpanel"; + CacheDirectoryMode = "0700"; + LogsDirectory = "pufferpanel"; + LogsDirectoryMode = "0700"; + + EnvironmentFile = cfg.environmentFile; + + # Command "pufferpanel shutdown --pid $MAINPID" sends SIGTERM (code 15) + # to the main process and waits for termination. This is essentially + # KillMode=mixed we are using here. See + # https://freedesktop.org/software/systemd/man/systemd.kill.html#KillMode= + KillMode = "mixed"; + + DynamicUser = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProtectClock = true; + ProtectHostname = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateUsers = true; + PrivateDevices = true; + RestrictRealtime = true; + RestrictNamespaces = [ "user" "mnt" ]; # allow buildFHSUserEnv + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + LockPersonality = true; + DeviceAllow = [ "" ]; + DevicePolicy = "closed"; + CapabilityBoundingSet = [ "" ]; + }; + }; + }; + + meta.maintainers = [ lib.maintainers.tie ]; +} diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix index 58a595b5c76f5..d881ea913695c 100644 --- a/nixos/modules/services/misc/redmine.nix +++ b/nixos/modules/services/misc/redmine.nix @@ -283,13 +283,13 @@ in services.redmine.settings = { production = { - scm_subversion_command = if cfg.components.subversion then "${pkgs.subversion}/bin/svn" else ""; - scm_mercurial_command = if cfg.components.mercurial then "${pkgs.mercurial}/bin/hg" else ""; - scm_git_command = if cfg.components.git then "${pkgs.git}/bin/git" else ""; - scm_cvs_command = if cfg.components.cvs then "${pkgs.cvs}/bin/cvs" else ""; - scm_bazaar_command = if cfg.components.breezy then "${pkgs.breezy}/bin/bzr" else ""; - imagemagick_convert_command = if cfg.components.imagemagick then "${pkgs.imagemagick}/bin/convert" else ""; - gs_command = if cfg.components.ghostscript then "${pkgs.ghostscript}/bin/gs" else ""; + scm_subversion_command = optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn"; + scm_mercurial_command = optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg"; + scm_git_command = optionalString cfg.components.git "${pkgs.git}/bin/git"; + scm_cvs_command = optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs"; + scm_bazaar_command = optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr"; + imagemagick_convert_command = optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert"; + gs_command = optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs"; minimagick_font_path = "${cfg.components.minimagick_font_path}"; }; }; diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix index f1a1ed4d29b38..99b25bdb8e9ed 100644 --- a/nixos/modules/services/misc/siproxd.nix +++ b/nixos/modules/services/misc/siproxd.nix @@ -20,7 +20,7 @@ let ${optionalString (cfg.hostsAllowReg != []) "hosts_allow_reg = ${concatStringsSep "," cfg.hostsAllowReg}"} ${optionalString (cfg.hostsAllowSip != []) "hosts_allow_sip = ${concatStringsSep "," cfg.hostsAllowSip}"} ${optionalString (cfg.hostsDenySip != []) "hosts_deny_sip = ${concatStringsSep "," cfg.hostsDenySip}"} - ${if (cfg.passwordFile != "") then "proxy_auth_pwfile = ${cfg.passwordFile}" else ""} + ${optionalString (cfg.passwordFile != "") "proxy_auth_pwfile = ${cfg.passwordFile}"} ${cfg.extraConfig} ''; diff --git a/nixos/modules/services/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix index 270d888afb781..b7761c34fe51a 100644 --- a/nixos/modules/services/monitoring/grafana-agent.nix +++ b/nixos/modules/services/monitoring/grafana-agent.nix @@ -140,7 +140,7 @@ in # We can't use Environment=HOSTNAME=%H, as it doesn't include the domain part. export HOSTNAME=$(< /proc/sys/kernel/hostname) - exec ${cfg.package}/bin/agent -config.expand-env -config.file ${configFile} + exec ${lib.getExe cfg.package} -config.expand-env -config.file ${configFile} ''; serviceConfig = { Restart = "always"; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix index 0c2de683ecf72..f67596f05a3a1 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix @@ -58,10 +58,10 @@ in }; }; serviceOpts = let - collectSettingsArgs = if (cfg.collectdBinary.enable) then '' + collectSettingsArgs = optionalString (cfg.collectdBinary.enable) '' --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \ --collectd.security-level ${cfg.collectdBinary.securityLevel} \ - '' else ""; + ''; in { serviceConfig = { ExecStart = '' diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix index f80aeae9c6b7d..50e1321a1e9ce 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix @@ -4,12 +4,12 @@ with lib; let cfg = config.services.prometheus.exporters.smartctl; - args = concatStrings [ - "--web.listen-address=\"${cfg.listenAddress}:${toString cfg.port}\" " - "--smartctl.path=\"${pkgs.smartmontools}/bin/smartctl\" " - "--smartctl.interval=\"${cfg.maxInterval}\" " - "${concatMapStringsSep " " (device: "--smartctl.device=${device}") cfg.devices}" - ] ++ cfg.extraFlags; + args = lib.escapeShellArgs ([ + "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}" + "--smartctl.path=${pkgs.smartmontools}/bin/smartctl" + "--smartctl.interval=${cfg.maxInterval}" + ] ++ map (device: "--smartctl.device=${device}") cfg.devices + ++ cfg.extraFlags); in { port = 9633; diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix index 0cb0e126d4c50..2537bb1b8d80f 100644 --- a/nixos/modules/services/network-filesystems/kubo.nix +++ b/nixos/modules/services/network-filesystems/kubo.nix @@ -22,6 +22,18 @@ let configFile = settingsFormat.generate "kubo-config.json" customizedConfig; + # Create a fake repo containing only the file "api". + # $IPFS_PATH will point to this directory instead of the real one. + # For some reason the Kubo CLI tools insist on reading the + # config file when it exists. But the Kubo daemon sets the file + # permissions such that only the ipfs user is allowed to read + # this file. This prevents normal users from talking to the daemon. + # To work around this terrible design, create a fake repo with no + # config file, only an api file and everything should work as expected. + fakeKuboRepo = pkgs.writeTextDir "api" '' + /unix/run/ipfs.sock + ''; + kuboFlags = utils.escapeSystemdExecArgs ( optional cfg.autoMount "--mount" ++ optional cfg.enableGC "--enable-gc" ++ @@ -38,6 +50,22 @@ let splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw); + multiaddrsToListenStreams = addrIn: + let + addrs = if builtins.typeOf addrIn == "list" + then addrIn else [ addrIn ]; + unfilteredResult = map multiaddrToListenStream addrs; + in + builtins.filter (addr: addr != null) unfilteredResult; + + multiaddrsToListenDatagrams = addrIn: + let + addrs = if builtins.typeOf addrIn == "list" + then addrIn else [ addrIn ]; + unfilteredResult = map multiaddrToListenDatagram addrs; + in + builtins.filter (addr: addr != null) unfilteredResult; + multiaddrToListenStream = addrRaw: let addr = splitMulitaddr addrRaw; @@ -154,13 +182,18 @@ in options = { Addresses.API = mkOption { - type = types.str; - default = "/ip4/127.0.0.1/tcp/5001"; - description = lib.mdDoc "Where Kubo exposes its API to"; + type = types.oneOf [ types.str (types.listOf types.str) ]; + default = [ ]; + description = lib.mdDoc '' + Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on. + In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket. + To allow the ipfs CLI tools to communicate with the daemon over that socket, + add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];` + ''; }; Addresses.Gateway = mkOption { - type = types.str; + type = types.oneOf [ types.str (types.listOf types.str) ]; default = "/ip4/127.0.0.1/tcp/8080"; description = lib.mdDoc "Where the IPFS Gateway can be reached"; }; @@ -248,7 +281,7 @@ in ]; environment.systemPackages = [ cfg.package ]; - environment.variables.IPFS_PATH = cfg.dataDir; + environment.variables.IPFS_PATH = fakeKuboRepo; # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; @@ -319,6 +352,10 @@ in # change when the changes are applied. Whyyyyyy..... ipfs --offline config replace - ''; + postStop = mkIf cfg.autoMount '' + # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked + umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true + ''; serviceConfig = { ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ]; User = cfg.user; @@ -334,27 +371,23 @@ in wantedBy = [ "sockets.target" ]; socketConfig = { ListenStream = - let - fromCfg = multiaddrToListenStream cfg.settings.Addresses.Gateway; - in - [ "" ] ++ lib.optional (fromCfg != null) fromCfg; + [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway); ListenDatagram = - let - fromCfg = multiaddrToListenDatagram cfg.settings.Addresses.Gateway; - in - [ "" ] ++ lib.optional (fromCfg != null) fromCfg; + [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway); }; }; systemd.sockets.ipfs-api = { wantedBy = [ "sockets.target" ]; - # We also include "%t/ipfs.sock" because there is no way to put the "%t" - # in the multiaddr. - socketConfig.ListenStream = - let - fromCfg = multiaddrToListenStream cfg.settings.Addresses.API; - in - [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg; + socketConfig = { + # We also include "%t/ipfs.sock" because there is no way to put the "%t" + # in the multiaddr. + ListenStream = + [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API); + SocketMode = "0660"; + SocketUser = cfg.user; + SocketGroup = cfg.group; + }; }; }; diff --git a/nixos/modules/services/network-filesystems/openafs/lib.nix b/nixos/modules/services/network-filesystems/openafs/lib.nix index 80628f4dfaf29..e5e147a8dc338 100644 --- a/nixos/modules/services/network-filesystems/openafs/lib.nix +++ b/nixos/modules/services/network-filesystems/openafs/lib.nix @@ -1,13 +1,13 @@ { config, lib, ...}: let - inherit (lib) concatStringsSep mkOption types; + inherit (lib) concatStringsSep mkOption types optionalString; in { mkCellServDB = cellName: db: '' >${cellName} - '' + (concatStringsSep "\n" (map (dbm: if (dbm.ip != "" && dbm.dnsname != "") then dbm.ip + " #" + dbm.dnsname else "") + '' + (concatStringsSep "\n" (map (dbm: optionalString (dbm.ip != "" && dbm.dnsname != "") "${dbm.ip} #${dbm.dnsname}") db)) + "\n"; diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix index 4434fedce1eb8..895467cc674ab 100644 --- a/nixos/modules/services/networking/iscsi/root-initiator.nix +++ b/nixos/modules/services/networking/iscsi/root-initiator.nix @@ -185,6 +185,10 @@ in assertion = cfg.loginAll -> cfg.target == null; message = "iSCSI target name is set while login on all portals is enabled."; } + { + assertion = !config.boot.initrd.systemd.enable; + message = "systemd stage 1 does not support iscsi yet."; + } ]; }; } diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix new file mode 100644 index 0000000000000..6df630c1f1947 --- /dev/null +++ b/nixos/modules/services/networking/ivpn.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.ivpn; +in +with lib; +{ + options.services.ivpn = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + This option enables iVPN daemon. + This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security. + ''; + }; + }; + + config = mkIf cfg.enable { + boot.kernelModules = [ "tun" ]; + + environment.systemPackages = with pkgs; [ ivpn ivpn-service ]; + + # iVPN writes to /etc/iproute2/rt_tables + networking.iproute2.enable = true; + networking.firewall.checkReversePath = "loose"; + + systemd.services.ivpn-service = { + description = "iVPN daemon"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network.target" ]; + after = [ + "network-online.target" + "NetworkManager.service" + "systemd-resolved.service" + ]; + path = [ + # Needed for mount + "/run/wrappers" + ]; + startLimitBurst = 5; + startLimitIntervalSec = 20; + serviceConfig = { + ExecStart = "${pkgs.ivpn-service}/bin/ivpn-service --logging"; + Restart = "always"; + RestartSec = 1; + }; + }; + }; + + meta.maintainers = with maintainers; [ ataraxiasjel ]; +} diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix index 98c58d2d5db1b..d221c95ae6200 100644 --- a/nixos/modules/services/networking/ndppd.nix +++ b/nixos/modules/services/networking/ndppd.nix @@ -17,7 +17,7 @@ let ttl ${toString proxy.ttl} ${render proxy.rules (ruleNetworkName: rule: '' rule ${prefer rule.network ruleNetworkName} { - ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""} + ${rule.method}${optionalString (rule.method == "iface") " ${rule.interface}"} }'')} }'')} ''); diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix index 5bd9e9ca61696..647c0ce3e6d1f 100644 --- a/nixos/modules/services/networking/netbird.nix +++ b/nixos/modules/services/networking/netbird.nix @@ -41,9 +41,10 @@ in { documentation = [ "https://netbird.io/docs/" ]; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ + openresolv + ]; serviceConfig = { - AmbientCapabilities = [ "CAP_NET_ADMIN" ]; - DynamicUser = true; Environment = [ "NB_CONFIG=/var/lib/netbird/config.json" "NB_LOG_FILE=console" diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix index e6344d7ff3b34..bf7ec19f02a68 100644 --- a/nixos/modules/services/networking/ntopng.nix +++ b/nixos/modules/services/networking/ntopng.nix @@ -86,7 +86,7 @@ in redis.createInstance = mkOption { type = types.nullOr types.str; - default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else ""; + default = optionalString (versionAtLeast config.system.stateVersion "22.05") "ntopng"; description = lib.mdDoc '' Local Redis instance name. Set to `null` to disable local Redis instance. Defaults to `""` for diff --git a/nixos/modules/services/networking/peroxide.nix b/nixos/modules/services/networking/peroxide.nix index 6cac4bf2f89a1..885ee1d96cd05 100644 --- a/nixos/modules/services/networking/peroxide.nix +++ b/nixos/modules/services/networking/peroxide.nix @@ -9,7 +9,7 @@ let in { options.services.peroxide = { - enable = mkEnableOption (lib.mdDoc "enable"); + enable = mkEnableOption (lib.mdDoc "peroxide"); package = mkPackageOptionMD pkgs "peroxide" { default = [ "peroxide" ]; diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix index c2c2a370cb004..19ab3f1aa48c0 100644 --- a/nixos/modules/services/networking/smokeping.nix +++ b/nixos/modules/services/networking/smokeping.nix @@ -339,14 +339,9 @@ in }; preStart = '' mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data - rm -f ${smokepingHome}/cropper - ln -s ${cfg.package}/htdocs/cropper ${smokepingHome}/cropper - rm -f ${smokepingHome}/css - ln -s ${cfg.package}/htdocs/css ${smokepingHome}/css - rm -f ${smokepingHome}/js - ln -s ${cfg.package}/htdocs/js ${smokepingHome}/js - rm -f ${smokepingHome}/smokeping.fcgi - ln -s ${cgiHome} ${smokepingHome}/smokeping.fcgi + ln -sf ${cfg.package}/htdocs/css ${smokepingHome}/css + ln -sf ${cfg.package}/htdocs/js ${smokepingHome}/js + ln -sf ${cgiHome} ${smokepingHome}/smokeping.fcgi ${cfg.package}/bin/smokeping --check --config=${configPath} ${cfg.package}/bin/smokeping --static --config=${configPath} ''; diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix index 7932bac9ca3a1..af64969c2fcd4 100644 --- a/nixos/modules/services/networking/ssh/lshd.nix +++ b/nixos/modules/services/networking/ssh/lshd.nix @@ -169,11 +169,11 @@ in else (concatStrings (map (i: "--interface=\"${i}\"") interfaces))} \ -h "${hostKey}" \ - ${if !syslog then "--no-syslog" else ""} \ + ${optionalString (!syslog) "--no-syslog" } \ ${if passwordAuthentication then "--password" else "--no-password" } \ ${if publicKeyAuthentication then "--publickey" else "--no-publickey" } \ ${if rootLogin then "--root-login" else "--no-root-login" } \ - ${if loginShell != null then "--login-shell=\"${loginShell}\"" else "" } \ + ${optionalString (loginShell != null) "--login-shell=\"${loginShell}\"" } \ ${if srpKeyExchange then "--srp-keyexchange" else "--no-srp-keyexchange" } \ ${if !tcpForwarding then "--no-tcpip-forward" else "--tcpip-forward"} \ ${if x11Forwarding then "--x11-forward" else "--no-x11-forward" } \ diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix index 5f225682b7779..89ddf82152993 100644 --- a/nixos/modules/services/networking/ssh/sshd.nix +++ b/nixos/modules/services/networking/ssh/sshd.nix @@ -474,10 +474,10 @@ in mkdir -m 0755 -p "$(dirname '${k.path}')" ssh-keygen \ -t "${k.type}" \ - ${if k ? bits then "-b ${toString k.bits}" else ""} \ - ${if k ? rounds then "-a ${toString k.rounds}" else ""} \ - ${if k ? comment then "-C '${k.comment}'" else ""} \ - ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \ + ${optionalString (k ? bits) "-b ${toString k.bits}"} \ + ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \ + ${optionalString (k ? comment) "-C '${k.comment}'"} \ + ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \ -f "${k.path}" \ -N "" fi @@ -536,7 +536,7 @@ in # https://github.com/NixOS/nixpkgs/pull/10155 # https://github.com/NixOS/nixpkgs/pull/41745 services.openssh.authorizedKeysFiles = - [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ]; + [ "%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u" ]; services.openssh.extraConfig = mkOrder 0 '' @@ -550,7 +550,7 @@ in '') cfg.ports} ${concatMapStrings ({ port, addr, ... }: '' - ListenAddress ${addr}${if port != null then ":" + toString port else ""} + ListenAddress ${addr}${optionalString (port != null) (":" + toString port)} '') cfg.listenAddresses} ${optionalString cfgc.setXAuthLocation '' diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix index 8b1398bfd47d4..e58526814d1ad 100644 --- a/nixos/modules/services/networking/strongswan.nix +++ b/nixos/modules/services/networking/strongswan.nix @@ -4,7 +4,7 @@ let inherit (builtins) toFile; inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList - mkIf mkEnableOption mkOption types literalExpression; + mkIf mkEnableOption mkOption types literalExpression optionalString; cfg = config.services.strongswan; @@ -34,8 +34,8 @@ let strongswanConf = {setup, connections, ca, secretsFile, managePlugins, enabledPlugins}: toFile "strongswan.conf" '' charon { - ${if managePlugins then "load_modular = no" else ""} - ${if managePlugins then ("load = " + (concatStringsSep " " enabledPlugins)) else ""} + ${optionalString managePlugins "load_modular = no"} + ${optionalString managePlugins ("load = " + (concatStringsSep " " enabledPlugins))} plugins { stroke { secrets_file = ${secretsFile} diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix index 4f592fb312d33..996e9b2253921 100644 --- a/nixos/modules/services/networking/stunnel.nix +++ b/nixos/modules/services/networking/stunnel.nix @@ -154,8 +154,8 @@ in environment.systemPackages = [ pkgs.stunnel ]; environment.etc."stunnel.cfg".text = '' - ${ if cfg.user != null then "setuid = ${cfg.user}" else "" } - ${ if cfg.group != null then "setgid = ${cfg.group}" else "" } + ${ optionalString (cfg.user != null) "setuid = ${cfg.user}" } + ${ optionalString (cfg.group != null) "setgid = ${cfg.group}" } debug = ${cfg.logLevel} diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix new file mode 100644 index 0000000000000..93227a9b625d0 --- /dev/null +++ b/nixos/modules/services/networking/wgautomesh.nix @@ -0,0 +1,161 @@ +{ lib, config, pkgs, ... }: +with lib; +let + cfg = config.services.wgautomesh; + settingsFormat = pkgs.formats.toml { }; + configFile = + # Have to remove nulls manually as TOML generator will not just skip key + # if value is null + settingsFormat.generate "wgautomesh-config.toml" + (filterAttrs (k: v: v != null) + (mapAttrs + (k: v: + if k == "peers" + then map (e: filterAttrs (k: v: v != null) e) v + else v) + cfg.settings)); + runtimeConfigFile = + if cfg.enableGossipEncryption + then "/run/wgautomesh/wgautomesh.toml" + else configFile; +in +{ + options.services.wgautomesh = { + enable = mkEnableOption (mdDoc "the wgautomesh daemon"); + logLevel = mkOption { + type = types.enum [ "trace" "debug" "info" "warn" "error" ]; + default = "info"; + description = mdDoc "wgautomesh log level."; + }; + enableGossipEncryption = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable encryption of gossip traffic."; + }; + gossipSecretFile = mkOption { + type = types.path; + description = mdDoc '' + File containing the shared secret key to use for gossip encryption. + Required if `enableGossipEncryption` is set. + ''; + }; + enablePersistence = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable persistence of Wireguard peer info between restarts."; + }; + openFirewall = mkOption { + type = types.bool; + default = true; + description = mdDoc "Automatically open gossip port in firewall (recommended)."; + }; + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + + interface = mkOption { + type = types.str; + description = mdDoc '' + Wireguard interface to manage (it is NOT created by wgautomesh, you + should use another NixOS option to create it such as + `networking.wireguard.interfaces.wg0 = {...};`). + ''; + example = "wg0"; + }; + gossip_port = mkOption { + type = types.port; + description = mdDoc '' + wgautomesh gossip port, this MUST be the same number on all nodes in + the wgautomesh network. + ''; + default = 1666; + }; + lan_discovery = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable discovery of peers on the same LAN using UDP broadcast."; + }; + upnp_forward_external_port = mkOption { + type = types.nullOr types.port; + default = null; + description = mdDoc '' + Public port number to try to redirect to this machine's Wireguard + daemon using UPnP IGD. + ''; + }; + peers = mkOption { + type = types.listOf (types.submodule { + options = { + pubkey = mkOption { + type = types.str; + description = mdDoc "Wireguard public key of this peer."; + }; + address = mkOption { + type = types.str; + description = mdDoc '' + Wireguard address of this peer (a single IP address, multliple + addresses or address ranges are not supported). + ''; + example = "10.0.0.42"; + }; + endpoint = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Bootstrap endpoint for connecting to this Wireguard peer if no + other address is known or none are working. + ''; + default = null; + example = "wgnode.mydomain.example:51820"; + }; + }; + }); + default = [ ]; + description = mdDoc "wgautomesh peer list."; + }; + }; + + }; + default = { }; + description = mdDoc "Configuration for wgautomesh."; + }; + }; + + config = mkIf cfg.enable { + services.wgautomesh.settings = { + gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret"; + persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state"; + }; + + systemd.services.wgautomesh = { + path = [ pkgs.wireguard-tools ]; + environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; }; + description = "wgautomesh"; + serviceConfig = { + Type = "simple"; + + ExecStart = "${getExe pkgs.wgautomesh} ${runtimeConfigFile}"; + Restart = "always"; + RestartSec = "30"; + LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ]; + + ExecStartPre = mkIf cfg.enableGossipEncryption [ + ''${pkgs.envsubst}/bin/envsubst \ + -i ${configFile} \ + -o ${runtimeConfigFile}'' + ]; + + DynamicUser = true; + StateDirectory = "wgautomesh"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "wgautomesh"; + AmbientCapabilities = "CAP_NET_ADMIN"; + CapabilityBoundingSet = "CAP_NET_ADMIN"; + }; + wantedBy = [ "multi-user.target" ]; + }; + networking.firewall.allowedUDPPorts = + mkIf cfg.openFirewall [ cfg.settings.gossip_port ]; + }; +} + diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix index 440b617f60a39..067d5df487255 100644 --- a/nixos/modules/services/networking/wstunnel.nix +++ b/nixos/modules/services/networking/wstunnel.nix @@ -294,7 +294,7 @@ let DynamicUser = true; SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group; PrivateTmp = true; - AmbientCapabilities = optional (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; @@ -340,7 +340,7 @@ let EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile; DynamicUser = true; PrivateTmp = true; - AmbientCapabilities = (optional (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optional ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]); + AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]); NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; diff --git a/nixos/modules/services/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix index b9120f37ba247..fb3de7077e31e 100644 --- a/nixos/modules/services/networking/xinetd.nix +++ b/nixos/modules/services/networking/xinetd.nix @@ -27,7 +27,7 @@ let ${optionalString srv.unlisted "type = UNLISTED"} ${optionalString (srv.flags != "") "flags = ${srv.flags}"} socket_type = ${if srv.protocol == "udp" then "dgram" else "stream"} - ${if srv.port != 0 then "port = ${toString srv.port}" else ""} + ${optionalString (srv.port != 0) "port = ${toString srv.port}"} wait = ${if srv.protocol == "udp" then "yes" else "no"} user = ${srv.user} server = ${srv.server} diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix index 9ac89e057620b..f6a23fb900f08 100644 --- a/nixos/modules/services/printing/cupsd.nix +++ b/nixos/modules/services/printing/cupsd.nix @@ -317,6 +317,7 @@ in environment.etc.cups.source = "/var/lib/cups"; services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper; + services.udev.packages = cfg.drivers; # Allow asswordless printer admin for members of wheel group security.polkit.extraConfig = mkIf polkitEnabled '' diff --git a/nixos/modules/services/search/qdrant.nix b/nixos/modules/services/search/qdrant.nix index a843c44dbb5f9..e1f7365d951a0 100644 --- a/nixos/modules/services/search/qdrant.nix +++ b/nixos/modules/services/search/qdrant.nix @@ -100,6 +100,7 @@ in { after = [ "network.target" ]; serviceConfig = { + LimitNOFILE=65536; ExecStart = "${pkgs.qdrant}/bin/qdrant --config-path ${configFile}"; DynamicUser = true; Restart = "on-failure"; diff --git a/nixos/modules/services/security/authelia.nix b/nixos/modules/services/security/authelia.nix index 143c441c7e153..28c5fd0a1df59 100644 --- a/nixos/modules/services/security/authelia.nix +++ b/nixos/modules/services/security/authelia.nix @@ -336,7 +336,7 @@ in ProtectProc = "noaccess"; ProtectSystem = "strict"; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix index ead24d1470717..93962d40ce4b4 100644 --- a/nixos/modules/services/security/fail2ban.nix +++ b/nixos/modules/services/security/fail2ban.nix @@ -78,6 +78,13 @@ in ''; }; + bantime = mkOption { + default = null; + type = types.nullOr types.str; + example = "10m"; + description = lib.mdDoc "Number of seconds that a host is banned."; + }; + maxretry = mkOption { default = 3; type = types.ints.unsigned; @@ -202,6 +209,20 @@ in ''; }; + extraSettings = mkOption { + type = with types; attrsOf (oneOf [ bool ints.positive str ]); + default = {}; + description = lib.mdDoc '' + Extra default configuration for all jails (i.e. `[DEFAULT]`). See + <https://github.com/fail2ban/fail2ban/blob/master/config/jail.conf> for an overview. + ''; + example = literalExpression '' + { + findtime = "15m"; + } + ''; + }; + jails = mkOption { default = { }; example = literalExpression '' @@ -320,11 +341,18 @@ in ''} # Miscellaneous options ignoreip = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP} + ${optionalString (cfg.bantime != null) '' + bantime = ${cfg.bantime} + ''} maxretry = ${toString cfg.maxretry} backend = systemd # Actions banaction = ${cfg.banaction} banaction_allports = ${cfg.banaction-allports} + ${optionalString (cfg.extraSettings != {}) '' + # Extra settings + ${generators.toKeyValue {} cfg.extraSettings} + ''} ''; # Block SSH if there are too many failing connection attempts. # Benefits from verbose sshd logging to observe failed login attempts, diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix index 5583c39368f77..2f19decb5cb17 100644 --- a/nixos/modules/services/security/kanidm.nix +++ b/nixos/modules/services/security/kanidm.nix @@ -7,6 +7,18 @@ let serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); + certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ]; + + # Merge bind mount paths and remove paths where a prefix is already mounted. + # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is alread in the mount + # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. + hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; + mergePaths = lib.foldl' (merged: newPath: let + # If the new path is a prefix to some existing path, we need to filter it out + filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + # If a prefix of the new path is already in the list, do not add it + filteredNew = if hasPrefixInList filteredPaths newPath then [] else [ newPath ]; + in filteredPaths ++ filteredNew) []; defaultServiceConfig = { BindReadOnlyPaths = [ @@ -16,7 +28,7 @@ let "-/etc/hosts" "-/etc/localtime" ]; - CapabilityBoundingSet = ""; + CapabilityBoundingSet = []; # ProtectClock= adds DeviceAllow=char-rtc r DeviceAllow = ""; # Implies ProtectSystem=strict, which re-mounts all paths @@ -216,22 +228,28 @@ in description = "kanidm identity management daemon"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - serviceConfig = defaultServiceConfig // { - StateDirectory = "kanidm"; - StateDirectoryMode = "0700"; - ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; - User = "kanidm"; - Group = "kanidm"; + serviceConfig = lib.mkMerge [ + # Merge paths and ignore existing prefixes needs to sidestep mkMerge + (defaultServiceConfig // { + BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); + }) + { + StateDirectory = "kanidm"; + StateDirectoryMode = "0700"; + ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; + User = "kanidm"; + Group = "kanidm"; - AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; - CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; - # This would otherwise override the CAP_NET_BIND_SERVICE capability. - PrivateUsers = false; - # Port needs to be exposed to the host network - PrivateNetwork = false; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; - TemporaryFileSystem = "/:ro"; - }; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + # This would otherwise override the CAP_NET_BIND_SERVICE capability. + PrivateUsers = lib.mkForce false; + # Port needs to be exposed to the host network + PrivateNetwork = lib.mkForce false; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + TemporaryFileSystem = "/:ro"; + } + ]; environment.RUST_LOG = "info"; }; @@ -240,34 +258,32 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; restartTriggers = [ unixConfigFile clientConfigFile ]; - serviceConfig = defaultServiceConfig // { - CacheDirectory = "kanidm-unixd"; - CacheDirectoryMode = "0700"; - RuntimeDirectory = "kanidm-unixd"; - ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; - User = "kanidm-unixd"; - Group = "kanidm-unixd"; + serviceConfig = lib.mkMerge [ + defaultServiceConfig + { + CacheDirectory = "kanidm-unixd"; + CacheDirectoryMode = "0700"; + RuntimeDirectory = "kanidm-unixd"; + ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; + User = "kanidm-unixd"; + Group = "kanidm-unixd"; - BindReadOnlyPaths = [ - "/nix/store" - "-/etc/resolv.conf" - "-/etc/nsswitch.conf" - "-/etc/hosts" - "-/etc/localtime" - "-/etc/kanidm" - "-/etc/static/kanidm" - "-/etc/ssl" - "-/etc/static/ssl" - ]; - BindPaths = [ - # To create the socket - "/run/kanidm-unixd:/var/run/kanidm-unixd" - ]; - # Needs to connect to kanidmd - PrivateNetwork = false; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; - TemporaryFileSystem = "/:ro"; - }; + BindReadOnlyPaths = [ + "-/etc/kanidm" + "-/etc/static/kanidm" + "-/etc/ssl" + "-/etc/static/ssl" + ]; + BindPaths = [ + # To create the socket + "/run/kanidm-unixd:/var/run/kanidm-unixd" + ]; + # Needs to connect to kanidmd + PrivateNetwork = lib.mkForce false; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + TemporaryFileSystem = "/:ro"; + } + ]; environment.RUST_LOG = "info"; }; diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix index e3f8e75ca2476..12547acabfe05 100644 --- a/nixos/modules/services/security/oauth2_proxy.nix +++ b/nixos/modules/services/security/oauth2_proxy.nix @@ -72,15 +72,14 @@ let } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig; mapConfig = key: attr: - if attr != null && attr != [] then ( + optionalString (attr != null && attr != []) ( if isDerivation attr then mapConfig key (toString attr) else if (builtins.typeOf attr) == "set" then concatStringsSep " " (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else - "--${key}=${toString attr}") - else ""; + "--${key}=${toString attr}"); configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig); in diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix index 11769d4e3095f..06494ddb631af 100644 --- a/nixos/modules/services/system/cachix-agent/default.nix +++ b/nixos/modules/services/system/cachix-agent/default.nix @@ -72,7 +72,7 @@ in { EnvironmentFile = cfg.credentialsFile; ExecStart = '' ${cfg.package}/bin/cachix ${lib.optionalString cfg.verbose "--verbose"} ${lib.optionalString (cfg.host != null) "--host ${cfg.host}"} \ - deploy agent ${cfg.name} ${if cfg.profile != null then cfg.profile else ""} + deploy agent ${cfg.name} ${optionalString (cfg.profile != null) cfg.profile} ''; }; }; diff --git a/nixos/modules/services/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix index 85e9509bcc82d..89157b460b9a4 100644 --- a/nixos/modules/services/system/cachix-watch-store.nix +++ b/nixos/modules/services/system/cachix-watch-store.nix @@ -62,7 +62,13 @@ in after = [ "network-online.target" ]; path = [ config.nix.package ]; wantedBy = [ "multi-user.target" ]; + unitConfig = { + # allow to restart indefinitely + StartLimitIntervalSec = 0; + }; serviceConfig = { + # don't put too much stress on the machine when restarting + RestartSec = 1; # we don't want to kill children processes as those are deployments KillMode = "process"; Restart = "on-failure"; diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix index c677088101f0c..9d8a62ec78c53 100644 --- a/nixos/modules/services/system/dbus.nix +++ b/nixos/modules/services/system/dbus.nix @@ -14,13 +14,17 @@ let serviceDirectories = cfg.packages; }; - inherit (lib) mkOption mkIf mkMerge types; + inherit (lib) mkOption mkEnableOption mkIf mkMerge types; in { options = { + boot.initrd.systemd.dbus = { + enable = mkEnableOption (lib.mdDoc "dbus in stage 1") // { visible = false; }; + }; + services.dbus = { enable = mkOption { @@ -111,6 +115,21 @@ in ]; } + (mkIf config.boot.initrd.systemd.dbus.enable { + boot.initrd.systemd = { + users.messagebus = { }; + groups.messagebus = { }; + contents."/etc/dbus-1".source = pkgs.makeDBusConf { + inherit (cfg) apparmor; + suidHelper = "/bin/false"; + serviceDirectories = [ pkgs.dbus ]; + }; + packages = [ pkgs.dbus ]; + storePaths = [ "${pkgs.dbus}/bin/dbus-daemon" ]; + targets.sockets.wants = [ "dbus.socket" ]; + }; + }) + (mkIf (cfg.implementation == "dbus") { environment.systemPackages = [ pkgs.dbus diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix index 2dd62edab7871..18a9e3d5fe305 100644 --- a/nixos/modules/services/video/rtsp-simple-server.nix +++ b/nixos/modules/services/video/mediamtx.nix @@ -3,19 +3,19 @@ with lib; let - cfg = config.services.rtsp-simple-server; - package = pkgs.rtsp-simple-server; + cfg = config.services.mediamtx; + package = pkgs.mediamtx; format = pkgs.formats.yaml {}; in { options = { - services.rtsp-simple-server = { - enable = mkEnableOption (lib.mdDoc "RTSP Simple Server"); + services.mediamtx = { + enable = mkEnableOption (lib.mdDoc "MediaMTX"); settings = mkOption { description = lib.mdDoc '' - Settings for rtsp-simple-server. - Read more at <https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml> + Settings for MediaMTX. + Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml> ''; type = format.type; @@ -25,7 +25,7 @@ in "stdout" ]; # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default. - logFile = "/var/log/rtsp-simple-server/rtsp-simple-server.log"; + logFile = "/var/log/mediamtx/mediamtx.log"; }; example = { @@ -40,20 +40,20 @@ in env = mkOption { type = with types; attrsOf anything; - description = lib.mdDoc "Extra environment variables for RTSP Simple Server"; + description = lib.mdDoc "Extra environment variables for MediaMTX"; default = {}; example = { - RTSP_CONFKEY = "mykey"; + MTX_CONFKEY = "mykey"; }; }; }; }; config = mkIf (cfg.enable) { - # NOTE: rtsp-simple-server watches this file and automatically reloads if it changes - environment.etc."rtsp-simple-server.yaml".source = format.generate "rtsp-simple-server.yaml" cfg.settings; + # NOTE: mediamtx watches this file and automatically reloads if it changes + environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings; - systemd.services.rtsp-simple-server = { + systemd.services.mediamtx = { environment = cfg.env; after = [ "network.target" ]; @@ -65,15 +65,15 @@ in serviceConfig = { DynamicUser = true; - User = "rtsp-simple-server"; - Group = "rtsp-simple-server"; + User = "mediamtx"; + Group = "mediamtx"; - LogsDirectory = "rtsp-simple-server"; + LogsDirectory = "mediamtx"; # user likely may want to stream cameras, can't hurt to add video group SupplementaryGroups = "video"; - ExecStart = "${package}/bin/rtsp-simple-server /etc/rtsp-simple-server.yaml"; + ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml"; }; }; }; diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix index 151fb812ddea6..5b2bd5aeeb09c 100644 --- a/nixos/modules/services/web-apps/discourse.nix +++ b/nixos/modules/services/web-apps/discourse.nix @@ -1025,8 +1025,8 @@ in services.postfix = lib.mkIf cfg.mail.incoming.enable { enable = true; - sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else ""; - sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else ""; + sslCert = lib.optionalString (cfg.sslCertificate != null) cfg.sslCertificate; + sslKey = lib.optionalString (cfg.sslCertificateKey != null) cfg.sslCertificateKey; origin = cfg.hostname; relayDomains = [ cfg.hostname ]; diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix deleted file mode 100644 index a61aa445f82c5..0000000000000 --- a/nixos/modules/services/web-apps/ihatemoney/default.nix +++ /dev/null @@ -1,153 +0,0 @@ -{ config, pkgs, lib, ... }: -with lib; -let - cfg = config.services.ihatemoney; - user = "ihatemoney"; - group = "ihatemoney"; - db = "ihatemoney"; - python3 = config.services.uwsgi.package.python3; - pkg = python3.pkgs.ihatemoney; - toBool = x: if x then "True" else "False"; - configFile = pkgs.writeText "ihatemoney.cfg" '' - from secrets import token_hex - # load a persistent secret key - SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key" - SECRET_KEY = "" - try: - with open(SECRET_KEY_FILE) as f: - SECRET_KEY = f.read() - except FileNotFoundError: - pass - if not SECRET_KEY: - print("ihatemoney: generating a new secret key") - SECRET_KEY = token_hex(50) - with open(SECRET_KEY_FILE, "w") as f: - f.write(SECRET_KEY) - del token_hex - del SECRET_KEY_FILE - - # "normal" configuration - DEBUG = False - SQLALCHEMY_DATABASE_URI = '${ - if cfg.backend == "sqlite" - then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite" - else "postgresql:///${db}"}' - SQLALCHEMY_TRACK_MODIFICATIONS = False - MAIL_DEFAULT_SENDER = (r"${cfg.defaultSender.name}", r"${cfg.defaultSender.email}") - ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject} - ADMIN_PASSWORD = r"${toString cfg.adminHashedPassword /*toString null == ""*/}" - ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation} - ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard} - SESSION_COOKIE_SECURE = ${toBool cfg.secureCookie} - ENABLE_CAPTCHA = ${toBool cfg.enableCaptcha} - LEGAL_LINK = r"${toString cfg.legalLink}" - - ${cfg.extraConfig} - ''; -in - { - options.services.ihatemoney = { - enable = mkEnableOption (lib.mdDoc "ihatemoney webapp. Note that this will set uwsgi to emperor mode"); - backend = mkOption { - type = types.enum [ "sqlite" "postgresql" ]; - default = "sqlite"; - description = lib.mdDoc '' - The database engine to use for ihatemoney. - If `postgresql` is selected, then a database called - `${db}` will be created. If you disable this option, - it will however not be removed. - ''; - }; - adminHashedPassword = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc "The hashed password of the administrator. To obtain it, run `ihatemoney generate_password_hash`"; - }; - uwsgiConfig = mkOption { - type = types.attrs; - example = { - http = ":8000"; - }; - description = lib.mdDoc "Additional configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen."; - }; - defaultSender = { - name = mkOption { - type = types.str; - default = "Budget manager"; - description = lib.mdDoc "The display name of the sender of ihatemoney emails"; - }; - email = mkOption { - type = types.str; - default = "ihatemoney@${config.networking.hostName}"; - defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"''; - description = lib.mdDoc "The email of the sender of ihatemoney emails"; - }; - }; - secureCookie = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc "Use secure cookies. Disable this when ihatemoney is served via http instead of https"; - }; - enableDemoProject = mkEnableOption (lib.mdDoc "access to the demo project in ihatemoney"); - enablePublicProjectCreation = mkEnableOption (lib.mdDoc "permission to create projects in ihatemoney by anyone"); - enableAdminDashboard = mkEnableOption (lib.mdDoc "ihatemoney admin dashboard"); - enableCaptcha = mkEnableOption (lib.mdDoc "a simplistic captcha for some forms"); - legalLink = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc "The URL to a page explaining legal statements about your service, eg. GDPR-related information."; - }; - extraConfig = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation."; - }; - }; - config = mkIf cfg.enable { - services.postgresql = mkIf (cfg.backend == "postgresql") { - enable = true; - ensureDatabases = [ db ]; - ensureUsers = [ { - name = user; - ensurePermissions = { - "DATABASE ${db}" = "ALL PRIVILEGES"; - }; - } ]; - }; - systemd.services.postgresql = mkIf (cfg.backend == "postgresql") { - wantedBy = [ "uwsgi.service" ]; - before = [ "uwsgi.service" ]; - }; - systemd.tmpfiles.rules = [ - "d /var/lib/ihatemoney 770 ${user} ${group}" - ]; - users = { - users.${user} = { - isSystemUser = true; - inherit group; - }; - groups.${group} = {}; - }; - services.uwsgi = { - enable = true; - plugins = [ "python3" ]; - instance = { - type = "emperor"; - vassals.ihatemoney = { - type = "normal"; - strict = true; - immediate-uid = user; - immediate-gid = group; - # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c - enable-threads = true; - module = "wsgi:application"; - chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney"; - env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ]; - pythonPackages = self: [ self.ihatemoney ]; - } // cfg.uwsgiConfig; - }; - }; - }; - } - - diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix new file mode 100644 index 0000000000000..442044fedb14e --- /dev/null +++ b/nixos/modules/services/web-apps/monica.nix @@ -0,0 +1,468 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.monica; + monica = pkgs.monica.override { + dataDir = cfg.dataDir; + }; + db = cfg.database; + mail = cfg.mail; + + user = cfg.user; + group = cfg.group; + + # shell script for local administration + artisan = pkgs.writeScriptBin "monica" '' + #! ${pkgs.runtimeShell} + cd ${monica} + sudo() { + if [[ "$USER" != ${user} ]]; then + exec /run/wrappers/bin/sudo -u ${user} "$@" + else + exec "$@" + fi + } + sudo ${pkgs.php}/bin/php artisan "$@" + ''; + + tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME; +in { + options.services.monica = { + enable = mkEnableOption (lib.mdDoc "monica"); + + user = mkOption { + default = "monica"; + description = lib.mdDoc "User monica runs as."; + type = types.str; + }; + + group = mkOption { + default = "monica"; + description = lib.mdDoc "Group monica runs as."; + type = types.str; + }; + + appKeyFile = mkOption { + description = lib.mdDoc '' + A file containing the Laravel APP_KEY - a 32 character long, + base64 encoded key used for encryption where needed. Can be + generated with <code>head -c 32 /dev/urandom | base64</code>. + ''; + example = "/run/keys/monica-appkey"; + type = types.path; + }; + + hostname = lib.mkOption { + type = lib.types.str; + default = + if config.networking.domain != null + then config.networking.fqdn + else config.networking.hostName; + defaultText = lib.literalExpression "config.networking.fqdn"; + example = "monica.example.com"; + description = lib.mdDoc '' + The hostname to serve monica on. + ''; + }; + + appURL = mkOption { + description = lib.mdDoc '' + The root URL that you want to host monica on. All URLs in monica will be generated using this value. + If you change this in the future you may need to run a command to update stored URLs in the database. + Command example: <code>php artisan monica:update-url https://old.example.com https://new.example.com</code> + ''; + default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}"; + defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}''; + example = "https://example.com"; + type = types.str; + }; + + dataDir = mkOption { + description = lib.mdDoc "monica data directory"; + default = "/var/lib/monica"; + type = types.path; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Database host address."; + }; + port = mkOption { + type = types.port; + default = 3306; + description = lib.mdDoc "Database host port."; + }; + name = mkOption { + type = types.str; + default = "monica"; + description = lib.mdDoc "Database name."; + }; + user = mkOption { + type = types.str; + default = user; + defaultText = lib.literalExpression "user"; + description = lib.mdDoc "Database username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/monica-dbpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + <option>database.user</option>. + ''; + }; + createLocally = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Create the database and database user locally."; + }; + }; + + mail = { + driver = mkOption { + type = types.enum ["smtp" "sendmail"]; + default = "smtp"; + description = lib.mdDoc "Mail driver to use."; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Mail host address."; + }; + port = mkOption { + type = types.port; + default = 1025; + description = lib.mdDoc "Mail host port."; + }; + fromName = mkOption { + type = types.str; + default = "monica"; + description = lib.mdDoc "Mail \"from\" name."; + }; + from = mkOption { + type = types.str; + default = "mail@monica.com"; + description = lib.mdDoc "Mail \"from\" email."; + }; + user = mkOption { + type = with types; nullOr str; + default = null; + example = "monica"; + description = lib.mdDoc "Mail username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/monica-mailpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + <option>mail.user</option>. + ''; + }; + encryption = mkOption { + type = with types; nullOr (enum ["tls"]); + default = null; + description = lib.mdDoc "SMTP encryption mechanism to use."; + }; + }; + + maxUploadSize = mkOption { + type = types.str; + default = "18M"; + example = "1G"; + description = lib.mdDoc "The maximum size for uploads (e.g. images)."; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [str int bool]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = lib.mdDoc '' + Options for the monica PHP pool. See the documentation on <literal>php-fpm.conf</literal> + for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix {inherit config lib;}) {} + ); + default = {}; + example = '' + { + serverAliases = [ + "monica.''${config.networking.domain}" + ]; + # To enable encryption and let let's encrypt take care of certificate + forceSSL = true; + enableACME = true; + } + ''; + description = lib.mdDoc '' + With this option, you can customize the nginx virtualHost settings. + ''; + }; + + config = mkOption { + type = with types; + attrsOf + (nullOr + (either + (oneOf [ + bool + int + port + path + str + ]) + (submodule { + options = { + _secret = mkOption { + type = nullOr str; + description = lib.mdDoc '' + The path to a file containing the value the + option should be set to in the final + configuration file. + ''; + }; + }; + }))); + default = {}; + example = '' + { + ALLOWED_IFRAME_HOSTS = "https://example.com"; + WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf"; + AUTH_METHOD = "oidc"; + OIDC_NAME = "MyLogin"; + OIDC_DISPLAY_NAME_CLAIMS = "name"; + OIDC_CLIENT_ID = "monica"; + OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"}; + OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm"; + OIDC_ISSUER_DISCOVER = true; + } + ''; + description = lib.mdDoc '' + monica configuration options to set in the + <filename>.env</filename> file. + + Refer to <link xlink:href="https://github.com/monicahq/monica"/> + for details on supported values. + + Settings containing secret data should be set to an attribute + set containing the attribute <literal>_secret</literal> - a + string pointing to a file containing the value the option + should be set to. See the example to get a better picture of + this: in the resulting <filename>.env</filename> file, the + <literal>OIDC_CLIENT_SECRET</literal> key will be set to the + contents of the <filename>/run/keys/oidc_secret</filename> + file. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = db.createLocally -> db.user == user; + message = "services.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true."; + } + { + assertion = db.createLocally -> db.passwordFile == null; + message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true."; + } + ]; + + services.monica.config = { + APP_ENV = "production"; + APP_KEY._secret = cfg.appKeyFile; + APP_URL = cfg.appURL; + DB_HOST = db.host; + DB_PORT = db.port; + DB_DATABASE = db.name; + DB_USERNAME = db.user; + MAIL_DRIVER = mail.driver; + MAIL_FROM_NAME = mail.fromName; + MAIL_FROM = mail.from; + MAIL_HOST = mail.host; + MAIL_PORT = mail.port; + MAIL_USERNAME = mail.user; + MAIL_ENCRYPTION = mail.encryption; + DB_PASSWORD._secret = db.passwordFile; + MAIL_PASSWORD._secret = mail.passwordFile; + APP_SERVICES_CACHE = "/run/monica/cache/services.php"; + APP_PACKAGES_CACHE = "/run/monica/cache/packages.php"; + APP_CONFIG_CACHE = "/run/monica/cache/config.php"; + APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php"; + APP_EVENTS_CACHE = "/run/monica/cache/events.php"; + SESSION_SECURE_COOKIE = tlsEnabled; + }; + + environment.systemPackages = [artisan]; + + services.mysql = mkIf db.createLocally { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [db.name]; + ensureUsers = [ + { + name = db.user; + ensurePermissions = {"${db.name}.*" = "ALL PRIVILEGES";}; + } + ]; + }; + + services.phpfpm.pools.monica = { + inherit user group; + phpOptions = '' + log_errors = on + post_max_size = ${cfg.maxUploadSize} + upload_max_filesize = ${cfg.maxUploadSize} + ''; + settings = { + "listen.mode" = "0660"; + "listen.owner" = user; + "listen.group" = group; + } // cfg.poolConfig; + }; + + services.nginx = { + enable = mkDefault true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedBrotliSettings = true; + recommendedProxySettings = true; + virtualHosts.${cfg.hostname} = mkMerge [ + cfg.nginx + { + root = mkForce "${monica}/public"; + locations = { + "/" = { + index = "index.php"; + tryFiles = "$uri $uri/ /index.php?$query_string"; + }; + "~ \.php$".extraConfig = '' + fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket}; + ''; + "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { + extraConfig = "expires 365d;"; + }; + }; + } + ]; + }; + + systemd.services.monica-setup = { + description = "Preperation tasks for monica"; + before = ["phpfpm-monica.service"]; + after = optional db.createLocally "mysql.service"; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = user; + UMask = 077; + WorkingDirectory = "${monica}"; + RuntimeDirectory = "monica/cache"; + RuntimeDirectoryMode = 0700; + }; + path = [pkgs.replace-secret]; + script = let + isSecret = v: isAttrs v && v ? _secret && isString v._secret; + monicaEnvVars = lib.generators.toKeyValue { + mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { + mkValueString = v: + with builtins; + if isInt v + then toString v + else if isString v + then v + else if true == v + then "true" + else if false == v + then "false" + else if isSecret v + then hashString "sha256" v._secret + else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}"; + }; + }; + secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config); + mkSecretReplacement = file: '' + replace-secret ${escapeShellArgs [(builtins.hashString "sha256" file) file "${cfg.dataDir}/.env"]} + ''; + secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; + filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{} null])) cfg.config; + monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig); + in '' + # error handling + set -euo pipefail + + # create .env file + install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env" + ${secretReplacements} + if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then + sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env" + fi + + # migrate & seed db + ${pkgs.php}/bin/php artisan key:generate --force + ${pkgs.php}/bin/php artisan setup:production -v --force + ''; + }; + + systemd.services.monica-scheduler = { + description = "Background tasks for monica"; + startAt = "minutely"; + after = ["monica-setup.service"]; + serviceConfig = { + Type = "oneshot"; + User = user; + WorkingDirectory = "${monica}"; + ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v"; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0710 ${user} ${group} - -" + "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" + ]; + + users = { + users = mkIf (user == "monica") { + monica = { + inherit group; + isSystemUser = true; + }; + "${config.services.nginx.user}".extraGroups = [group]; + }; + groups = mkIf (group == "monica") { + monica = {}; + }; + }; + }; +} + diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix index 5f8d9c5b15f4c..b617e9a593795 100644 --- a/nixos/modules/services/web-apps/moodle.nix +++ b/nixos/modules/services/web-apps/moodle.nix @@ -56,7 +56,7 @@ let mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; - phpExt = pkgs.php80.buildEnv { + phpExt = pkgs.php81.buildEnv { extensions = { all, ... }: with all; [ iconv mbstring curl openssl tokenizer soap ctype zip gd simplexml dom intl sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache exif sodium ]; extraConfig = "max_input_vars = 5000"; }; diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md index 7ef3cca281f9e..15c1f2da2724b 100644 --- a/nixos/modules/services/web-apps/nextcloud.md +++ b/nixos/modules/services/web-apps/nextcloud.md @@ -132,7 +132,9 @@ Auto updates for Nextcloud apps can be enabled using Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html). This is not an end-to-end encryption, but can be used to encrypt files that will be persisted to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3 - for PHP's openssl extension because this is implemented using the legacy cipher RC4. + for PHP's openssl extension and **Nextcloud 25 or older** because this is implemented using the + legacy cipher RC4. For Nextcloud26 this isn't relevant anymore, because Nextcloud has an RC4 implementation + written in native PHP and thus doesn't need `ext-openssl` for that anymore. If [](#opt-system.stateVersion) is *above* `22.05`, this is disabled by default. To turn it on again and for further information please refer to [](#opt-services.nextcloud.enableBrokenCiphersForSSE). diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index 76a0172747ffd..2824b7ee24562 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -204,7 +204,7 @@ in { package = mkOption { type = types.package; description = lib.mdDoc "Which package to use for the Nextcloud instance."; - relatedPackages = [ "nextcloud24" "nextcloud25" "nextcloud26" ]; + relatedPackages = [ "nextcloud25" "nextcloud26" ]; }; phpPackage = mkOption { type = types.package; @@ -712,6 +712,10 @@ in { See <https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html#disabling-encryption> on how to achieve this. For more context, here is the implementing pull request: https://github.com/NixOS/nixpkgs/pull/198470 + '') + ++ (optional (cfg.enableBrokenCiphersForSSE && versionAtLeast cfg.package.version "26") '' + Nextcloud26 supports RC4 without requiring legacy OpenSSL, so + `services.nextcloud.enableBrokenCiphersForSSE` can be set to `false`. ''); services.nextcloud.package = with pkgs; diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix index f64254d62524e..893dfa10acbc0 100644 --- a/nixos/modules/services/web-apps/plausible.nix +++ b/nixos/modules/services/web-apps/plausible.nix @@ -9,6 +9,8 @@ in { options.services.plausible = { enable = mkEnableOption (lib.mdDoc "plausible"); + package = mkPackageOptionMD pkgs "plausible" { }; + releaseCookiePath = mkOption { type = with types; either str path; description = lib.mdDoc '' @@ -180,12 +182,12 @@ in { services.epmd.enable = true; - environment.systemPackages = [ pkgs.plausible ]; + environment.systemPackages = [ cfg.package ]; systemd.services = mkMerge [ { plausible = { - inherit (pkgs.plausible.meta) description; + inherit (cfg.package.meta) description; documentation = [ "https://plausible.io/docs/self-hosting" ]; wantedBy = [ "multi-user.target" ]; after = optional cfg.database.clickhouse.setup "clickhouse.service" @@ -233,7 +235,7 @@ in { SMTP_USER_NAME = cfg.mail.smtp.user; }); - path = [ pkgs.plausible ] + path = [ cfg.package ] ++ optional cfg.database.postgres.setup config.services.postgresql.package; script = '' export CONFIG_DIR=$CREDENTIALS_DIRECTORY @@ -241,10 +243,10 @@ in { export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )" # setup - ${pkgs.plausible}/createdb.sh - ${pkgs.plausible}/migrate.sh + ${cfg.package}/createdb.sh + ${cfg.package}/migrate.sh ${optionalString cfg.adminUser.activate '' - if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then + if ! ${cfg.package}/init-admin.sh | grep 'already exists'; then psql -d plausible <<< "UPDATE users SET email_verified=true;" fi ''} diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix index f9c91fb35db23..3a57ef383065b 100644 --- a/nixos/modules/services/web-servers/fcgiwrap.nix +++ b/nixos/modules/services/web-servers/fcgiwrap.nix @@ -54,7 +54,7 @@ in { serviceConfig = { ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${ - if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else "" + optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}" }"; } // (if cfg.user != null && cfg.group != null then { User = cfg.user; diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix index 811afe8e0af66..0438e12e7da82 100644 --- a/nixos/modules/services/web-servers/lighttpd/default.nix +++ b/nixos/modules/services/web-servers/lighttpd/default.nix @@ -64,7 +64,7 @@ let ]; maybeModuleString = moduleName: - if elem moduleName cfg.enableModules then ''"${moduleName}"'' else ""; + optionalString (elem moduleName cfg.enableModules) ''"${moduleName}"''; modulesIncludeString = concatStringsSep ",\n" (filter (x: x != "") (map maybeModuleString allKnownModules)); @@ -106,15 +106,15 @@ let static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" ) index-file.names = ( "index.html" ) - ${if cfg.mod_userdir then '' + ${optionalString cfg.mod_userdir '' userdir.path = "public_html" - '' else ""} + ''} - ${if cfg.mod_status then '' + ${optionalString cfg.mod_status '' status.status-url = "/server-status" status.statistics-url = "/server-statistics" status.config-url = "/server-config" - '' else ""} + ''} ${cfg.extraConfig} ''; diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 3d19186e1a9d3..1e6cb0d374053 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -318,7 +318,7 @@ let listenString = { addr, port, ssl, extraParameters ? [], ... }: # UDP listener for QUIC transport protocol. - (if ssl && vhost.quic then " + (optionalString (ssl && vhost.quic) (" listen ${addr}:${toString port} quic " + optionalString vhost.default "default_server " + optionalString vhost.reuseport "reuseport " @@ -326,7 +326,7 @@ let let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ]; isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters); in filter isCompatibleParameter extraParameters)) - + ";" else "") + + ";")) + " listen ${addr}:${toString port} " diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix index d8bfee547c79a..4d2c36287be69 100644 --- a/nixos/modules/services/web-servers/tomcat.nix +++ b/nixos/modules/services/web-servers/tomcat.nix @@ -234,11 +234,11 @@ in ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i` done - ${if cfg.extraConfigFiles != [] then '' + ${optionalString (cfg.extraConfigFiles != []) '' for i in ${toString cfg.extraConfigFiles}; do ln -sfn $i ${cfg.baseDir}/conf/`basename $i` done - '' else ""} + ''} # Create a modified catalina.properties file # Change all references from CATALINA_HOME to CATALINA_BASE and add support for shared libraries @@ -345,7 +345,7 @@ in # Symlink all the given web applications files or paths into the webapps/ directory # of this virtual host - for i in "${if virtualHost ? webapps then toString virtualHost.webapps else ""}"; do + for i in "${optionalString (virtualHost ? webapps) (toString virtualHost.webapps)}"; do if [ -f $i ]; then # If the given web application is a file, symlink it into the webapps/ directory ln -sfn $i ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $i` diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix index 73a864bb95fe8..38f932ffb4206 100644 --- a/nixos/modules/services/x11/desktop-managers/plasma5.nix +++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix @@ -429,7 +429,8 @@ in dolphin-plugins ffmpegthumbs kdegraphics-thumbnailers - pkgs.kio-admin + kde-inotify-survey + kio-admin kio-extras ]; optionalPackages = [ diff --git a/nixos/modules/system/boot/grow-partition.nix b/nixos/modules/system/boot/grow-partition.nix index 034b2b9906f55..a2764187a5333 100644 --- a/nixos/modules/system/boot/grow-partition.nix +++ b/nixos/modules/system/boot/grow-partition.nix @@ -17,6 +17,11 @@ with lib; config = mkIf config.boot.growPartition { + assertions = [{ + assertion = !config.boot.initrd.systemd.enable; + message = "systemd stage 1 does not support 'boot.growPartition' yet."; + }]; + boot.initrd.extraUtilsCommands = '' copy_bin_and_libs ${pkgs.gawk}/bin/gawk copy_bin_and_libs ${pkgs.gnused}/bin/sed diff --git a/nixos/modules/system/boot/initrd-network.nix b/nixos/modules/system/boot/initrd-network.nix index a1017c3e24204..e8bbf1d040329 100644 --- a/nixos/modules/system/boot/initrd-network.nix +++ b/nixos/modules/system/boot/initrd-network.nix @@ -67,11 +67,15 @@ in boot.initrd.network.flushBeforeStage2 = mkOption { type = types.bool; - default = true; + default = !config.boot.initrd.systemd.enable; + defaultText = "!config.boot.initrd.systemd.enable"; description = lib.mdDoc '' Whether to clear the configuration of the interfaces that were set up in the initrd right before stage 2 takes over. Stage 2 will do the regular network configuration based on the NixOS networking options. + + The default is false when systemd is enabled in initrd, + because the systemd-networkd documentation suggests it. ''; }; diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix index cbc61d55d6bb3..2530240628e42 100644 --- a/nixos/modules/system/boot/initrd-openvpn.nix +++ b/nixos/modules/system/boot/initrd-openvpn.nix @@ -51,7 +51,7 @@ in # Add openvpn and ip binaries to the initrd # The shared libraries are required for DNS resolution - boot.initrd.extraUtilsCommands = '' + boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) '' copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn copy_bin_and_libs ${pkgs.iproute2}/bin/ip @@ -59,18 +59,33 @@ in cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib ''; + boot.initrd.systemd.storePaths = [ + "${pkgs.openvpn}/bin/openvpn" + "${pkgs.iproute2}/bin/ip" + "${pkgs.glibc}/lib/libresolv.so.2" + "${pkgs.glibc}/lib/libnss_dns.so.2" + ]; + boot.initrd.secrets = { "/etc/initrd.ovpn" = cfg.configuration; }; # openvpn --version would exit with 1 instead of 0 - boot.initrd.extraUtilsCommandsTest = '' + boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) '' $out/bin/openvpn --show-gateway ''; - boot.initrd.network.postCommands = '' + boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) '' openvpn /etc/initrd.ovpn & ''; + + boot.initrd.systemd.services.openvpn = { + wantedBy = [ "initrd.target" ]; + path = [ pkgs.iproute2 ]; + after = [ "network.target" "initrd-nixos-copy-secrets.service" ]; + serviceConfig.ExecStart = "${pkgs.openvpn}/bin/openvpn /etc/initrd.ovpn"; + serviceConfig.Type = "notify"; + }; }; } diff --git a/nixos/modules/system/boot/initrd-ssh.nix b/nixos/modules/system/boot/initrd-ssh.nix index 125f75d667069..60c5ff62ffff0 100644 --- a/nixos/modules/system/boot/initrd-ssh.nix +++ b/nixos/modules/system/boot/initrd-ssh.nix @@ -5,6 +5,10 @@ with lib; let cfg = config.boot.initrd.network.ssh; + shell = if cfg.shell == null then "/bin/ash" else cfg.shell; + inherit (config.programs.ssh) package; + + enabled = let initrd = config.boot.initrd; in (initrd.network.enable || initrd.systemd.network.enable) && cfg.enable; in @@ -33,8 +37,9 @@ in }; shell = mkOption { - type = types.str; - default = "/bin/ash"; + type = types.nullOr types.str; + default = null; + defaultText = ''"/bin/ash"''; description = lib.mdDoc '' Login shell of the remote user. Can be used to limit actions user can do. ''; @@ -119,9 +124,11 @@ in sshdCfg = config.services.openssh; sshdConfig = '' + UsePAM no Port ${toString cfg.port} PasswordAuthentication no + AuthorizedKeysFile %h/.ssh/authorized_keys %h/.ssh/authorized_keys2 /etc/ssh/authorized_keys.d/%u ChallengeResponseAuthentication no ${flip concatMapStrings cfg.hostKeys (path: '' @@ -142,7 +149,7 @@ in ${cfg.extraConfig} ''; - in mkIf (config.boot.initrd.network.enable && cfg.enable) { + in mkIf enabled { assertions = [ { assertion = cfg.authorizedKeys != []; @@ -157,14 +164,19 @@ in for instructions. ''; } + + { + assertion = config.boot.initrd.systemd.enable -> cfg.shell == null; + message = "systemd stage 1 does not support boot.initrd.network.ssh.shell"; + } ]; - boot.initrd.extraUtilsCommands = '' - copy_bin_and_libs ${pkgs.openssh}/bin/sshd + boot.initrd.extraUtilsCommands = mkIf (!config.boot.initrd.systemd.enable) '' + copy_bin_and_libs ${package}/bin/sshd cp -pv ${pkgs.glibc.out}/lib/libnss_files.so.* $out/lib ''; - boot.initrd.extraUtilsCommandsTest = '' + boot.initrd.extraUtilsCommandsTest = mkIf (!config.boot.initrd.systemd.enable) '' # sshd requires a host key to check config, so we pass in the test's tmpkey="$(mktemp initrd-ssh-testkey.XXXXXXXXXX)" cp "${../../../tests/initrd-network-ssh/ssh_host_ed25519_key}" "$tmpkey" @@ -176,9 +188,9 @@ in rm "$tmpkey" ''; - boot.initrd.network.postCommands = '' - echo '${cfg.shell}' > /etc/shells - echo 'root:x:0:0:root:/root:${cfg.shell}' > /etc/passwd + boot.initrd.network.postCommands = mkIf (!config.boot.initrd.systemd.enable) '' + echo '${shell}' > /etc/shells + echo 'root:x:0:0:root:/root:${shell}' > /etc/passwd echo 'sshd:x:1:1:sshd:/var/empty:/bin/nologin' >> /etc/passwd echo 'passwd: files' > /etc/nsswitch.conf @@ -204,7 +216,7 @@ in /bin/sshd -e ''; - boot.initrd.postMountCommands = '' + boot.initrd.postMountCommands = mkIf (!config.boot.initrd.systemd.enable) '' # Stop sshd cleanly before stage 2. # # If you want to keep it around to debug post-mount SSH issues, @@ -217,6 +229,38 @@ in boot.initrd.secrets = listToAttrs (map (path: nameValuePair (initrdKeyPath path) path) cfg.hostKeys); + + # Systemd initrd stuff + boot.initrd.systemd = mkIf config.boot.initrd.systemd.enable { + users.sshd = { uid = 1; group = "sshd"; }; + groups.sshd = { gid = 1; }; + + contents."/etc/ssh/authorized_keys.d/root".text = + concatStringsSep "\n" config.boot.initrd.network.ssh.authorizedKeys; + contents."/etc/ssh/sshd_config".text = sshdConfig; + storePaths = ["${package}/bin/sshd"]; + + services.sshd = { + description = "SSH Daemon"; + wantedBy = ["initrd.target"]; + after = ["network.target" "initrd-nixos-copy-secrets.service"]; + + # Keys from Nix store are world-readable, which sshd doesn't + # like. If this were a real nix store and not the initrd, we + # neither would nor could do this + preStart = flip concatMapStrings cfg.hostKeys (path: '' + /bin/chmod 0600 "${initrdKeyPath path}" + ''); + unitConfig.DefaultDependencies = false; + serviceConfig = { + ExecStart = "${package}/bin/sshd -D -f /etc/ssh/sshd_config"; + Type = "simple"; + KillMode = "process"; + Restart = "on-failure"; + }; + }; + }; + }; } diff --git a/nixos/modules/system/boot/loader/grub/grub.nix b/nixos/modules/system/boot/loader/grub/grub.nix index 121d7e88e74de..5c0a07fb51272 100644 --- a/nixos/modules/system/boot/loader/grub/grub.nix +++ b/nixos/modules/system/boot/loader/grub/grub.nix @@ -33,7 +33,7 @@ let then realGrub.override { efiSupport = cfg.efiSupport; } else null; - f = x: if x == null then "" else "" + x; + f = x: optionalString (x != null) ("" + x); grubConfig = args: let @@ -52,7 +52,7 @@ let fullName = lib.getName realGrub; fullVersion = lib.getVersion realGrub; grubEfi = f grubEfi; - grubTargetEfi = if cfg.efiSupport && (cfg.version == 2) then f (grubEfi.grubTarget or "") else ""; + grubTargetEfi = optionalString (cfg.efiSupport && (cfg.version == 2)) (f (grubEfi.grubTarget or "")); bootPath = args.path; storePath = config.boot.loader.grub.storePath; bootloaderId = if args.efiBootloaderId == null then "${config.system.nixos.distroName}${efiSysMountPoint'}" else args.efiBootloaderId; diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py index c92451997203b..a040518a5a575 100755 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -302,8 +302,12 @@ def main() -> None: if is_default: write_loader_conf(*gen) except OSError as e: - profile = f"profile '{gen.profile}'" if gen.profile else "default profile" - print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr) + # See https://github.com/NixOS/nixpkgs/issues/114552 + if e.errno == errno.EINVAL: + profile = f"profile '{gen.profile}'" if gen.profile else "default profile" + print("ignoring {} in the list of boot entries because of the following error:\n{}".format(profile, e), file=sys.stderr) + else: + raise e for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False): relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/") diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix index 757dc080d4b61..8a3e89e5888bc 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -20,7 +20,7 @@ let nix = config.nix.package.out; - timeout = if config.boot.loader.timeout != null then config.boot.loader.timeout else ""; + timeout = optionalString (config.boot.loader.timeout != null) config.boot.loader.timeout; editor = if cfg.editor then "True" else "False"; @@ -32,9 +32,9 @@ let inherit (config.system.nixos) distroName; - memtest86 = if cfg.memtest86.enable then pkgs.memtest86-efi else ""; + memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86-efi; - netbootxyz = if cfg.netbootxyz.enable then pkgs.netbootxyz-efi else ""; + netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi; copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' empty_file=$(${pkgs.coreutils}/bin/mktemp) diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index 8954c90812f92..b8f36538e70fe 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -1024,13 +1024,12 @@ in copy_bin_and_libs ${pkgs.gnupg}/libexec/scdaemon ${concatMapStringsSep "\n" (x: - if x.gpgCard != null then + optionalString (x.gpgCard != null) '' mkdir -p $out/secrets/gpg-keys/${x.device} cp -a ${x.gpgCard.encryptedPass} $out/secrets/gpg-keys/${x.device}/cryptkey.gpg cp -a ${x.gpgCard.publicKey} $out/secrets/gpg-keys/${x.device}/pubkey.asc '' - else "" ) (attrValues luks.devices) } ''} diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index 05a667a09efc1..e6b96433e8417 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -6,8 +6,6 @@ with lib; let - cfg = config.systemd.network; - check = { global = { @@ -2941,16 +2939,14 @@ let + def.extraConfig; }; - unitFiles = listToAttrs (map (name: { - name = "systemd/network/${name}"; + mkUnitFiles = prefix: cfg: listToAttrs (map (name: { + name = "${prefix}systemd/network/${name}"; value.source = "${cfg.units.${name}.unit}/${name}"; }) (attrNames cfg.units)); -in -{ - options = { + commonOptions = visible: { - systemd.network.enable = mkOption { + enable = mkOption { default = false; type = types.bool; description = lib.mdDoc '' @@ -2958,31 +2954,35 @@ in ''; }; - systemd.network.links = mkOption { + links = mkOption { default = {}; + inherit visible; type = with types; attrsOf (submodule [ { options = linkOptions; } ]); description = lib.mdDoc "Definition of systemd network links."; }; - systemd.network.netdevs = mkOption { + netdevs = mkOption { default = {}; + inherit visible; type = with types; attrsOf (submodule [ { options = netdevOptions; } ]); description = lib.mdDoc "Definition of systemd network devices."; }; - systemd.network.networks = mkOption { + networks = mkOption { default = {}; + inherit visible; type = with types; attrsOf (submodule [ { options = networkOptions; } networkConfig ]); description = lib.mdDoc "Definition of systemd networks."; }; - systemd.network.config = mkOption { + config = mkOption { default = {}; + inherit visible; type = with types; submodule [ { options = networkdOptions; } networkdConfig ]; description = lib.mdDoc "Definition of global systemd network config."; }; - systemd.network.units = mkOption { + units = mkOption { description = lib.mdDoc "Definition of networkd units."; default = {}; internal = true; @@ -2995,7 +2995,7 @@ in })); }; - systemd.network.wait-online = { + wait-online = { enable = mkOption { type = types.bool; default = true; @@ -3051,12 +3051,11 @@ in }; - config = mkMerge [ + commonConfig = config: let cfg = config.systemd.network; in mkMerge [ # .link units are honored by udev, no matter if systemd-networkd is enabled or not. { systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.link" (linkToUnit n v)) cfg.links; - environment.etc = unitFiles; systemd.network.wait-online.extraArgs = [ "--timeout=${toString cfg.wait-online.timeout}" ] @@ -3066,14 +3065,6 @@ in (mkIf config.systemd.network.enable { - users.users.systemd-network.group = "systemd-network"; - - systemd.additionalUpstreamSystemUnits = [ - "systemd-networkd-wait-online.service" - "systemd-networkd.service" - "systemd-networkd.socket" - ]; - systemd.network.units = mapAttrs' (n: v: nameValuePair "${n}.netdev" (netdevToUnit n v)) cfg.netdevs // mapAttrs' (n: v: nameValuePair "${n}.network" (networkToUnit n v)) cfg.networks; @@ -3082,14 +3073,6 @@ in # networkd. systemd.sockets.systemd-networkd.wantedBy = [ "sockets.target" ]; - systemd.services.systemd-networkd = { - wantedBy = [ "multi-user.target" ]; - aliases = [ "dbus-org.freedesktop.network1.service" ]; - restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [ - config.environment.etc."systemd/networkd.conf".source - ]; - }; - systemd.services.systemd-networkd-wait-online = { inherit (cfg.wait-online) enable; wantedBy = [ "network-online.target" ]; @@ -3111,8 +3094,37 @@ in }; }; + }) + ]; + + stage2Config = let + cfg = config.systemd.network; + unitFiles = mkUnitFiles "" cfg; + in mkMerge [ + (commonConfig config) + + { environment.etc = unitFiles; } + + (mkIf config.systemd.network.enable { + + users.users.systemd-network.group = "systemd-network"; + + systemd.additionalUpstreamSystemUnits = [ + "systemd-networkd-wait-online.service" + "systemd-networkd.service" + "systemd-networkd.socket" + ]; + environment.etc."systemd/networkd.conf" = renderConfig cfg.config; + systemd.services.systemd-networkd = { + wantedBy = [ "multi-user.target" ]; + restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [ + config.environment.etc."systemd/networkd.conf".source + ]; + aliases = [ "dbus-org.freedesktop.network1.service" ]; + }; + networking.iproute2 = mkIf (cfg.config.addRouteTablesToIPRoute2 && cfg.config.routeTables != { }) { enable = mkDefault true; rttablesExtraConfig = '' @@ -3123,6 +3135,117 @@ in }; services.resolved.enable = mkDefault true; + + }) + ]; + + stage1Config = let + cfg = config.boot.initrd.systemd.network; + in mkMerge [ + (commonConfig config.boot.initrd) + + { + systemd.network.enable = mkDefault config.boot.initrd.network.enable; + systemd.contents = mkUnitFiles "/etc/" cfg; + + # Networkd link files are used early by udev to set up interfaces early. + # This must be done in stage 1 to avoid race conditions between udev and + # network daemons. + systemd.network.units = lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units; + systemd.storePaths = ["${config.boot.initrd.systemd.package}/lib/systemd/network/99-default.link"]; + } + + (mkIf cfg.enable { + + systemd.package = pkgs.systemdStage1Network; + + # For networkctl + systemd.dbus.enable = mkDefault true; + + systemd.additionalUpstreamUnits = [ + "systemd-networkd-wait-online.service" + "systemd-networkd.service" + "systemd-networkd.socket" + "systemd-network-generator.service" + "network-online.target" + "network-pre.target" + "network.target" + "nss-lookup.target" + "nss-user-lookup.target" + "remote-fs-pre.target" + "remote-fs.target" + ]; + systemd.users.systemd-network = {}; + systemd.groups.systemd-network = {}; + + systemd.contents."/etc/systemd/networkd.conf" = renderConfig cfg.config; + + systemd.services.systemd-networkd.wantedBy = [ "initrd.target" ]; + systemd.services.systemd-network-generator.wantedBy = [ "sysinit.target" ]; + + systemd.storePaths = [ + "${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd" + "${config.boot.initrd.systemd.package}/lib/systemd/systemd-networkd-wait-online" + "${config.boot.initrd.systemd.package}/lib/systemd/systemd-network-generator" + ]; + kernelModules = [ "af_packet" ]; + + systemd.services.nixos-flush-networkd = mkIf config.boot.initrd.network.flushBeforeStage2 { + description = "Flush Network Configuration"; + wantedBy = ["initrd.target"]; + after = ["systemd-networkd.service" "dbus.socket" "dbus.service"]; + before = ["shutdown.target" "initrd-switch-root.target"]; + conflicts = ["shutdown.target" "initrd-switch-root.target"]; + unitConfig.DefaultDependencies = false; + serviceConfig = { + # This service does nothing when starting, but brings down + # interfaces when switching root. This is the easiest way to + # ensure proper ordering while stopping. See systemd.unit(5) + # section on Before= and After=. The important part is that + # we are stopped before units we need, like dbus.service, + # and that we are stopped before starting units like + # initrd-switch-root.target + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "/bin/true"; + }; + # systemd-networkd doesn't bring down interfaces on its own + # when it exits (see: systemd-networkd(8)), so we have to do + # it ourselves. The networkctl command doesn't have a way to + # bring all interfaces down, so we have to iterate over the + # list and filter out unmanaged interfaces to bring them down + # individually. + preStop = '' + networkctl list --full --no-legend | while read _idx link _type _operational setup _; do + [ "$setup" = unmanaged ] && continue + networkctl down "$link" + done + ''; + }; + + }) + ]; + +in + +{ + options = { + systemd.network = commonOptions true; + boot.initrd.systemd.network = commonOptions "shallow"; + }; + + config = mkMerge [ + stage2Config + (mkIf config.boot.initrd.systemd.enable { + assertions = [{ + assertion = config.boot.initrd.network.udhcpc.extraArgs == []; + message = '' + boot.initrd.network.udhcpc.extraArgs is not supported when + boot.initrd.systemd.enable is enabled + ''; + }]; + + boot.initrd = stage1Config; }) ]; } diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix index d26ea7597c450..1229f6357523f 100644 --- a/nixos/modules/system/boot/stage-1.nix +++ b/nixos/modules/system/boot/stage-1.nix @@ -445,7 +445,8 @@ let ) config.boot.initrd.secrets) } - (cd "$tmp" && find . -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \ + # mindepth 1 so that we don't change the mode of / + (cd "$tmp" && find . -mindepth 1 -print0 | sort -z | bsdtar --uid 0 --gid 0 -cnf - -T - | bsdtar --null -cf - --format=newc @-) | \ ${compressorExe} ${lib.escapeShellArgs initialRamdisk.compressorArgs} >> "$1" ''; diff --git a/nixos/modules/system/boot/systemd/initrd-secrets.nix b/nixos/modules/system/boot/systemd/initrd-secrets.nix index bc65880719d7a..7b59c0cbe7b84 100644 --- a/nixos/modules/system/boot/systemd/initrd-secrets.nix +++ b/nixos/modules/system/boot/systemd/initrd-secrets.nix @@ -19,13 +19,13 @@ # drop this service, we'd mount the /run tmpfs over the secret, making it # invisible in stage 2. script = '' - for secret in $(cd /.initrd-secrets; find . -type f); do + for secret in $(cd /.initrd-secrets; find . -type f -o -type l); do mkdir -p "$(dirname "/$secret")" cp "/.initrd-secrets/$secret" "/$secret" done ''; - unitConfig = { + serviceConfig = { Type = "oneshot"; RemainAfterExit = true; }; diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index ffe96f3ad9c30..6f991b84bbd03 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -1,4 +1,4 @@ -{ lib, config, utils, pkgs, ... }: +{ lib, options, config, utils, pkgs, ... }: with lib; @@ -72,15 +72,6 @@ let "systemd-tmpfiles-setup.service" "timers.target" "umount.target" - - # TODO: Networking - # "network-online.target" - # "network-pre.target" - # "network.target" - # "nss-lookup.target" - # "nss-user-lookup.target" - # "remote-fs-pre.target" - # "remote-fs.target" ] ++ cfg.additionalUpstreamUnits; upstreamWants = [ @@ -135,18 +126,20 @@ in { options.boot.initrd.systemd = { enable = mkEnableOption (lib.mdDoc "systemd in initrd") // { description = lib.mdDoc '' - Whether to enable systemd in initrd. - - Note: This is in very early development and is highly - experimental. Most of the features NixOS supports in initrd are - not yet supported by the intrd generated with this option. + Whether to enable systemd in initrd. The unit options such as + {option}`boot.initrd.systemd.services` are the same as their + stage 2 counterparts such as {option}`systemd.services`, + except that `restartTriggers` and `reloadTriggers` are not + supported. + + Note: This is experimental. Some of the `boot.initrd` options + are not supported when this is enabled, and the options under + `boot.initrd.systemd` are subject to change. ''; }; - package = (mkPackageOptionMD pkgs "systemd" { + package = mkPackageOptionMD pkgs "systemd" { default = "systemdStage1"; - }) // { - visible = false; }; extraConfig = mkOption { @@ -176,7 +169,6 @@ in { "/etc/hostname".text = "mymachine"; } ''; - visible = false; default = {}; type = utils.systemdUtils.types.initrdContents; }; @@ -226,7 +218,6 @@ in { emergencyAccess = mkOption { type = with types; oneOf [ bool (nullOr (passwdEntry str)) ]; - visible = false; description = lib.mdDoc '' Set to true for unauthenticated emergency access, and false for no emergency access. @@ -240,7 +231,6 @@ in { initrdBin = mkOption { type = types.listOf types.package; default = []; - visible = false; description = lib.mdDoc '' Packages to include in /bin for the stage 1 emergency shell. ''; @@ -249,7 +239,6 @@ in { additionalUpstreamUnits = mkOption { default = [ ]; type = types.listOf types.str; - visible = false; example = [ "debug-shell.service" "systemd-quotacheck.service" ]; description = lib.mdDoc '' Additional units shipped with systemd that shall be enabled. @@ -260,7 +249,6 @@ in { default = [ ]; type = types.listOf types.str; example = [ "systemd-backlight@.service" ]; - visible = false; description = lib.mdDoc '' A list of units to skip when generating system systemd configuration directory. This has priority over upstream units, {option}`boot.initrd.systemd.units`, and @@ -273,13 +261,12 @@ in { units = mkOption { description = lib.mdDoc "Definition of systemd units."; default = {}; - visible = false; + visible = "shallow"; type = systemdUtils.types.units; }; packages = mkOption { default = []; - visible = false; type = types.listOf types.package; example = literalExpression "[ pkgs.systemd-cryptsetup-generator ]"; description = lib.mdDoc "Packages providing systemd units and hooks."; @@ -287,7 +274,7 @@ in { targets = mkOption { default = {}; - visible = false; + visible = "shallow"; type = systemdUtils.types.initrdTargets; description = lib.mdDoc "Definition of systemd target units."; }; @@ -295,35 +282,35 @@ in { services = mkOption { default = {}; type = systemdUtils.types.initrdServices; - visible = false; + visible = "shallow"; description = lib.mdDoc "Definition of systemd service units."; }; sockets = mkOption { default = {}; type = systemdUtils.types.initrdSockets; - visible = false; + visible = "shallow"; description = lib.mdDoc "Definition of systemd socket units."; }; timers = mkOption { default = {}; type = systemdUtils.types.initrdTimers; - visible = false; + visible = "shallow"; description = lib.mdDoc "Definition of systemd timer units."; }; paths = mkOption { default = {}; type = systemdUtils.types.initrdPaths; - visible = false; + visible = "shallow"; description = lib.mdDoc "Definition of systemd path units."; }; mounts = mkOption { default = []; type = systemdUtils.types.initrdMounts; - visible = false; + visible = "shallow"; description = lib.mdDoc '' Definition of systemd mount units. This is a list instead of an attrSet, because systemd mandates the names to be derived from @@ -334,7 +321,7 @@ in { automounts = mkOption { default = []; type = systemdUtils.types.automounts; - visible = false; + visible = "shallow"; description = lib.mdDoc '' Definition of systemd automount units. This is a list instead of an attrSet, because systemd mandates the names to be derived from @@ -345,12 +332,31 @@ in { slices = mkOption { default = {}; type = systemdUtils.types.slices; - visible = false; + visible = "shallow"; description = lib.mdDoc "Definition of slice configurations."; }; }; config = mkIf (config.boot.initrd.enable && cfg.enable) { + assertions = map (name: { + assertion = config.boot.initrd.${name} == ""; + message = '' + systemd stage 1 does not support 'boot.initrd.${name}'. Please + convert it to analogous systemd units in 'boot.initrd.systemd'. + + Definitions: + ${lib.concatMapStringsSep "\n" ({ file, ... }: "- ${file}") options.boot.initrd.${name}.definitionsWithLocations} + ''; + }) [ + "preFailCommands" + "preDeviceCommands" + "preLVMCommands" + "postDeviceCommands" + "postMountCommands" + "extraUtilsCommands" + "extraUtilsCommandsTest" + ]; + system.build = { inherit initialRamdisk; }; boot.initrd.availableKernelModules = [ @@ -378,7 +384,7 @@ in { "/etc/systemd/system.conf".text = '' [Manager] - DefaultEnvironment=PATH=/bin:/sbin ${optionalString (isBool cfg.emergencyAccess && cfg.emergencyAccess) "SYSTEMD_SULOGIN_FORCE=1"} + DefaultEnvironment=PATH=/bin:/sbin ${cfg.extraConfig} ManagerEnvironment=${lib.concatStringsSep " " (lib.mapAttrsToList (n: v: "${n}=${lib.escapeShellArg v}") cfg.managerEnvironment)} ''; @@ -388,8 +394,10 @@ in { "/etc/modules-load.d/nixos.conf".text = concatStringsSep "\n" config.boot.initrd.kernelModules; - "/etc/passwd".source = "${pkgs.fakeNss}/etc/passwd"; - "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then "!" else cfg.emergencyAccess}:::::::"; + # We can use either ! or * to lock the root account in the + # console, but some software like OpenSSH won't even allow you + # to log in with an SSH key if you use ! so we use * instead + "/etc/shadow".text = "root:${if isBool cfg.emergencyAccess then optionalString (!cfg.emergencyAccess) "*" else cfg.emergencyAccess}:::::::"; "/bin".source = "${initrdBinEnv}/bin"; "/sbin".source = "${initrdBinEnv}/sbin"; diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix index 822f1593474eb..326862f836a5c 100644 --- a/nixos/modules/tasks/filesystems.nix +++ b/nixos/modules/tasks/filesystems.nix @@ -319,7 +319,7 @@ in message = let fs = head (filter notAutoResizable fileSystems); in - "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${if fs.fsType == "auto" then " fsType has to be explicitly set and" else ""} only the ext filesystems and f2fs support it."; + "Mountpoint '${fs.mountPoint}': 'autoResize = true' is not supported for 'fsType = \"${fs.fsType}\"':${optionalString (fs.fsType == "auto") " fsType has to be explicitly set and"} only the ext filesystems and f2fs support it."; } ]; diff --git a/nixos/modules/tasks/network-interfaces-systemd.nix b/nixos/modules/tasks/network-interfaces-systemd.nix index b24b29c32d4ac..0fcd3c10219c1 100644 --- a/nixos/modules/tasks/network-interfaces-systemd.nix +++ b/nixos/modules/tasks/network-interfaces-systemd.nix @@ -28,11 +28,164 @@ let # TODO: warn the user that any address configured on those interfaces will be useless ++ concatMap (i: attrNames (filterAttrs (_: config: config.type != "internal") i.interfaces)) (attrValues cfg.vswitches); + domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain); + genericNetwork = override: + let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address + ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address; + makeGateway = gateway: { + routeConfig = { + Gateway = gateway; + GatewayOnLink = false; + }; + }; + in optionalAttrs (gateway != [ ]) { + routes = override (map makeGateway gateway); + } // optionalAttrs (domains != [ ]) { + domains = override domains; + }; + + genericDhcpNetworks = initrd: mkIf cfg.useDHCP { + networks."99-ethernet-default-dhcp" = { + # We want to match physical ethernet interfaces as commonly + # found on laptops, desktops and servers, to provide an + # "out-of-the-box" setup that works for common cases. This + # heuristic isn't perfect (it could match interfaces with + # custom names that _happen_ to start with en or eth), but + # should be good enough to make the common case easy and can + # be overridden on a case-by-case basis using + # higher-priority networks or by disabling useDHCP. + + # Type=ether matches veth interfaces as well, and this is + # more likely to result in interfaces being configured to + # use DHCP when they shouldn't. + + # When wait-online.anyInterface is enabled, RequiredForOnline really + # means "sufficient for online", so we can enable it. + # Otherwise, don't block the network coming online because of default networks. + matchConfig.Name = ["en*" "eth*"]; + DHCP = "yes"; + linkConfig.RequiredForOnline = + lib.mkDefault (if initrd + then config.boot.initrd.systemd.network.wait-online.anyInterface + else config.systemd.network.wait-online.anyInterface); + networkConfig.IPv6PrivacyExtensions = "kernel"; + }; + networks."99-wireless-client-dhcp" = { + # Like above, but this is much more likely to be correct. + matchConfig.WLANInterfaceType = "station"; + DHCP = "yes"; + linkConfig.RequiredForOnline = + lib.mkDefault config.systemd.network.wait-online.anyInterface; + networkConfig.IPv6PrivacyExtensions = "kernel"; + # We also set the route metric to one more than the default + # of 1024, so that Ethernet is preferred if both are + # available. + dhcpV4Config.RouteMetric = 1025; + ipv6AcceptRAConfig.RouteMetric = 1025; + }; + }; + + + interfaceNetworks = mkMerge (forEach interfaces (i: { + netdevs = mkIf i.virtual ({ + "40-${i.name}" = { + netdevConfig = { + Name = i.name; + Kind = i.virtualType; + }; + "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) { + User = i.virtualOwner; + }; + }; + }); + networks."40-${i.name}" = mkMerge [ (genericNetwork id) { + name = mkDefault i.name; + DHCP = mkForce (dhcpStr + (if i.useDHCP != null then i.useDHCP else false)); + address = forEach (interfaceIps i) + (ip: "${ip.address}/${toString ip.prefixLength}"); + routes = forEach (interfaceRoutes i) + (route: { + # Most of these route options have not been tested. + # Please fix or report any mistakes you may find. + routeConfig = + optionalAttrs (route.address != null && route.prefixLength != null) { + Destination = "${route.address}/${toString route.prefixLength}"; + } // + optionalAttrs (route.options ? fastopen_no_cookie) { + FastOpenNoCookie = route.options.fastopen_no_cookie; + } // + optionalAttrs (route.via != null) { + Gateway = route.via; + } // + optionalAttrs (route.type != null) { + Type = route.type; + } // + optionalAttrs (route.options ? onlink) { + GatewayOnLink = true; + } // + optionalAttrs (route.options ? initrwnd) { + InitialAdvertisedReceiveWindow = route.options.initrwnd; + } // + optionalAttrs (route.options ? initcwnd) { + InitialCongestionWindow = route.options.initcwnd; + } // + optionalAttrs (route.options ? pref) { + IPv6Preference = route.options.pref; + } // + optionalAttrs (route.options ? mtu) { + MTUBytes = route.options.mtu; + } // + optionalAttrs (route.options ? metric) { + Metric = route.options.metric; + } // + optionalAttrs (route.options ? src) { + PreferredSource = route.options.src; + } // + optionalAttrs (route.options ? protocol) { + Protocol = route.options.protocol; + } // + optionalAttrs (route.options ? quickack) { + QuickAck = route.options.quickack; + } // + optionalAttrs (route.options ? scope) { + Scope = route.options.scope; + } // + optionalAttrs (route.options ? from) { + Source = route.options.from; + } // + optionalAttrs (route.options ? table) { + Table = route.options.table; + } // + optionalAttrs (route.options ? advmss) { + TCPAdvertisedMaximumSegmentSize = route.options.advmss; + } // + optionalAttrs (route.options ? ttl-propagate) { + TTLPropagate = route.options.ttl-propagate == "enabled"; + }; + }); + networkConfig.IPv6PrivacyExtensions = "kernel"; + linkConfig = optionalAttrs (i.macAddress != null) { + MACAddress = i.macAddress; + } // optionalAttrs (i.mtu != null) { + MTUBytes = toString i.mtu; + }; + }]; + })); + in { + config = mkMerge [ - config = mkIf cfg.useNetworkd { + (mkIf config.boot.initrd.network.enable { + # Note this is if initrd.network.enable, not if + # initrd.systemd.network.enable. By setting the latter and not the + # former, the user retains full control over the configuration. + boot.initrd.systemd.network = mkMerge [(genericDhcpNetworks true) interfaceNetworks]; + }) + + (mkIf cfg.useNetworkd { assertions = [ { assertion = cfg.defaultGatewayWindowSize == null; @@ -54,149 +207,11 @@ in networking.dhcpcd.enable = mkDefault false; systemd.network = - let - domains = cfg.search ++ (optional (cfg.domain != null) cfg.domain); - genericNetwork = override: - let gateway = optional (cfg.defaultGateway != null && (cfg.defaultGateway.address or "") != "") cfg.defaultGateway.address - ++ optional (cfg.defaultGateway6 != null && (cfg.defaultGateway6.address or "") != "") cfg.defaultGateway6.address; - makeGateway = gateway: { - routeConfig = { - Gateway = gateway; - GatewayOnLink = false; - }; - }; - in optionalAttrs (gateway != [ ]) { - routes = override (map makeGateway gateway); - } // optionalAttrs (domains != [ ]) { - domains = override domains; - }; - in mkMerge [ { + mkMerge [ { enable = true; } - (mkIf cfg.useDHCP { - networks."99-ethernet-default-dhcp" = lib.mkIf cfg.useDHCP { - # We want to match physical ethernet interfaces as commonly - # found on laptops, desktops and servers, to provide an - # "out-of-the-box" setup that works for common cases. This - # heuristic isn't perfect (it could match interfaces with - # custom names that _happen_ to start with en or eth), but - # should be good enough to make the common case easy and can - # be overridden on a case-by-case basis using - # higher-priority networks or by disabling useDHCP. - - # Type=ether matches veth interfaces as well, and this is - # more likely to result in interfaces being configured to - # use DHCP when they shouldn't. - - # When wait-online.anyInterface is enabled, RequiredForOnline really - # means "sufficient for online", so we can enable it. - # Otherwise, don't block the network coming online because of default networks. - matchConfig.Name = ["en*" "eth*"]; - DHCP = "yes"; - linkConfig.RequiredForOnline = - lib.mkDefault config.systemd.network.wait-online.anyInterface; - networkConfig.IPv6PrivacyExtensions = "kernel"; - }; - networks."99-wireless-client-dhcp" = lib.mkIf cfg.useDHCP { - # Like above, but this is much more likely to be correct. - matchConfig.WLANInterfaceType = "station"; - DHCP = "yes"; - linkConfig.RequiredForOnline = - lib.mkDefault config.systemd.network.wait-online.anyInterface; - networkConfig.IPv6PrivacyExtensions = "kernel"; - # We also set the route metric to one more than the default - # of 1024, so that Ethernet is preferred if both are - # available. - dhcpV4Config.RouteMetric = 1025; - ipv6AcceptRAConfig.RouteMetric = 1025; - }; - }) - (mkMerge (forEach interfaces (i: { - netdevs = mkIf i.virtual ({ - "40-${i.name}" = { - netdevConfig = { - Name = i.name; - Kind = i.virtualType; - }; - "${i.virtualType}Config" = optionalAttrs (i.virtualOwner != null) { - User = i.virtualOwner; - }; - }; - }); - networks."40-${i.name}" = mkMerge [ (genericNetwork id) { - name = mkDefault i.name; - DHCP = mkForce (dhcpStr - (if i.useDHCP != null then i.useDHCP else false)); - address = forEach (interfaceIps i) - (ip: "${ip.address}/${toString ip.prefixLength}"); - routes = forEach (interfaceRoutes i) - (route: { - # Most of these route options have not been tested. - # Please fix or report any mistakes you may find. - routeConfig = - optionalAttrs (route.address != null && route.prefixLength != null) { - Destination = "${route.address}/${toString route.prefixLength}"; - } // - optionalAttrs (route.options ? fastopen_no_cookie) { - FastOpenNoCookie = route.options.fastopen_no_cookie; - } // - optionalAttrs (route.via != null) { - Gateway = route.via; - } // - optionalAttrs (route.type != null) { - Type = route.type; - } // - optionalAttrs (route.options ? onlink) { - GatewayOnLink = true; - } // - optionalAttrs (route.options ? initrwnd) { - InitialAdvertisedReceiveWindow = route.options.initrwnd; - } // - optionalAttrs (route.options ? initcwnd) { - InitialCongestionWindow = route.options.initcwnd; - } // - optionalAttrs (route.options ? pref) { - IPv6Preference = route.options.pref; - } // - optionalAttrs (route.options ? mtu) { - MTUBytes = route.options.mtu; - } // - optionalAttrs (route.options ? metric) { - Metric = route.options.metric; - } // - optionalAttrs (route.options ? src) { - PreferredSource = route.options.src; - } // - optionalAttrs (route.options ? protocol) { - Protocol = route.options.protocol; - } // - optionalAttrs (route.options ? quickack) { - QuickAck = route.options.quickack; - } // - optionalAttrs (route.options ? scope) { - Scope = route.options.scope; - } // - optionalAttrs (route.options ? from) { - Source = route.options.from; - } // - optionalAttrs (route.options ? table) { - Table = route.options.table; - } // - optionalAttrs (route.options ? advmss) { - TCPAdvertisedMaximumSegmentSize = route.options.advmss; - } // - optionalAttrs (route.options ? ttl-propagate) { - TTLPropagate = route.options.ttl-propagate == "enabled"; - }; - }); - networkConfig.IPv6PrivacyExtensions = "kernel"; - linkConfig = optionalAttrs (i.macAddress != null) { - MACAddress = i.macAddress; - } // optionalAttrs (i.mtu != null) { - MTUBytes = toString i.mtu; - }; - }]; - }))) + (genericDhcpNetworks false) + interfaceNetworks (mkMerge (flip mapAttrsToList cfg.bridges (name: bridge: { netdevs."40-${name}" = { netdevConfig = { @@ -437,6 +452,7 @@ in bindsTo = [ "systemd-networkd.service" ]; }; }; - }; + }) + ]; } diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix index 3503a6fff6a99..dacd700537c78 100644 --- a/nixos/modules/virtualisation/cri-o.nix +++ b/nixos/modules/virtualisation/cri-o.nix @@ -4,7 +4,10 @@ with lib; let cfg = config.virtualisation.cri-o; - crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; }); + crioPackage = pkgs.cri-o.override { + extraPackages = cfg.extraPackages + ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package; + }; format = pkgs.formats.toml { }; @@ -19,7 +22,7 @@ in enable = mkEnableOption (lib.mdDoc "Container Runtime Interface for OCI (CRI-O)"); storageDriver = mkOption { - type = types.enum [ "btrfs" "overlay" "vfs" ]; + type = types.enum [ "aufs" "btrfs" "devmapper" "overlay" "vfs" "zfs" ]; default = "overlay"; description = lib.mdDoc "Storage driver to be used"; }; diff --git a/nixos/modules/virtualisation/multipass.nix b/nixos/modules/virtualisation/multipass.nix index 6ef7de4b2bf50..b331b3be7ea58 100644 --- a/nixos/modules/virtualisation/multipass.nix +++ b/nixos/modules/virtualisation/multipass.nix @@ -33,8 +33,8 @@ in description = "Multipass orchestrates virtual Ubuntu instances."; wantedBy = [ "multi-user.target" ]; - wants = [ "network.target" ]; - after = [ "network.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; environment = { "XDG_DATA_HOME" = "/var/lib/multipass/data"; diff --git a/nixos/modules/virtualisation/nixos-containers.nix b/nixos/modules/virtualisation/nixos-containers.nix index 669981da59740..d54e2ed3f3ae1 100644 --- a/nixos/modules/virtualisation/nixos-containers.nix +++ b/nixos/modules/virtualisation/nixos-containers.nix @@ -170,11 +170,11 @@ let --setenv HOST_PORT="$HOST_PORT" \ --setenv PATH="$PATH" \ ${optionalString cfg.ephemeral "--ephemeral"} \ - ${if cfg.additionalCapabilities != null && cfg.additionalCapabilities != [] then - ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"'' else "" + ${optionalString (cfg.additionalCapabilities != null && cfg.additionalCapabilities != []) + ''--capability="${concatStringsSep "," cfg.additionalCapabilities}"'' } \ - ${if cfg.tmpfs != null && cfg.tmpfs != [] then - ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' else "" + ${optionalString (cfg.tmpfs != null && cfg.tmpfs != []) + ''--tmpfs=${concatStringsSep " --tmpfs=" cfg.tmpfs}'' } \ ${containerInit cfg} "''${SYSTEM_PATH:-/nix/var/nix/profiles/system}/init" ''; diff --git a/nixos/modules/virtualisation/parallels-guest.nix b/nixos/modules/virtualisation/parallels-guest.nix index 07a61bf208db3..dba8ce02b724c 100644 --- a/nixos/modules/virtualisation/parallels-guest.nix +++ b/nixos/modules/virtualisation/parallels-guest.nix @@ -87,7 +87,6 @@ in bindsTo = [ "cups.service" ]; path = [ prl-tools ]; serviceConfig = { - Type = "forking"; ExecStart = "${prl-tools}/bin/prlshprint"; WorkingDirectory = "${prl-tools}/bin"; }; diff --git a/nixos/modules/virtualisation/proxmox-image.nix b/nixos/modules/virtualisation/proxmox-image.nix index 6a4220fd265ca..c66a4f178ec73 100644 --- a/nixos/modules/virtualisation/proxmox-image.nix +++ b/nixos/modules/virtualisation/proxmox-image.nix @@ -135,10 +135,11 @@ with lib; cfgLine = name: value: '' ${name}: ${builtins.toString value} ''; + virtio0Storage = builtins.head (builtins.split ":" cfg.qemuConf.virtio0); cfgFile = fileName: properties: pkgs.writeTextDir fileName '' # generated by NixOS ${lib.concatStrings (lib.mapAttrsToList cfgLine properties)} - #qmdump#map:virtio0:drive-virtio0:local-lvm:raw: + #qmdump#map:virtio0:drive-virtio0:${virtio0Storage}:raw: ''; inherit (cfg) partitionTableType; supportEfi = partitionTableType == "efi" || partitionTableType == "hybrid"; diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 89772019284cb..cbc58344791d0 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -55,6 +55,11 @@ let }; + selectPartitionTableLayout = { useEFIBoot, useDefaultFilesystems }: + if useDefaultFilesystems then + if useEFIBoot then "efi" else "legacy" + else "none"; + driveCmdline = idx: { file, driveExtraOpts, deviceExtraOpts, ... }: let drvId = "drive${toString idx}"; @@ -98,7 +103,6 @@ let addDeviceNames = imap1 (idx: drive: drive // { device = driveDeviceName idx; }); - # Shell script to start the VM. startVM = '' @@ -111,8 +115,23 @@ let NIX_DISK_IMAGE=$(readlink -f "''${NIX_DISK_IMAGE:-${toString config.virtualisation.diskImage}}") || test -z "$NIX_DISK_IMAGE" if test -n "$NIX_DISK_IMAGE" && ! test -e "$NIX_DISK_IMAGE"; then - ${qemu}/bin/qemu-img create -f qcow2 "$NIX_DISK_IMAGE" \ - ${toString config.virtualisation.diskSize}M + echo "Disk image do not exist, creating the virtualisation disk image..." + # If we are using a bootloader and default filesystems layout. + # We have to reuse the system image layout as a backing image format (CoW) + # So we can write on the top of it. + + # If we are not using the default FS layout, potentially, we are interested into + # performing operations in postDeviceCommands or at early boot on the raw device. + # We can still boot through QEMU direct kernel boot feature. + + # CoW prevent size to be attributed to an image. + # FIXME: raise this issue to upstream. + ${qemu}/bin/qemu-img create \ + ${concatStringsSep " \\\n" ([ "-f qcow2" ] + ++ optional (cfg.useBootLoader && cfg.useDefaultFilesystems) "-F qcow2 -b ${systemImage}/nixos.qcow2" + ++ optional (!(cfg.useBootLoader && cfg.useDefaultFilesystems)) "-o size=${toString config.virtualisation.diskSize}M" + ++ [ "$NIX_DISK_IMAGE" ])} + echo "Virtualisation disk image created." fi # Create a directory for storing temporary data of the running VM. @@ -152,19 +171,13 @@ let ${lib.optionalString cfg.useBootLoader '' - if ${if !cfg.persistBootDevice then "true" else "! test -e $TMPDIR/disk.img"}; then - # Create a writable copy/snapshot of the boot disk. - # A writable boot disk can be booted from automatically. - ${qemu}/bin/qemu-img create -f qcow2 -F qcow2 -b ${bootDisk}/disk.img "$TMPDIR/disk.img" - fi - - NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${cfg.efiVars}}") + NIX_EFI_VARS=$(readlink -f "''${NIX_EFI_VARS:-${config.system.name}-efi-vars.fd}") ${lib.optionalString cfg.useEFIBoot '' # VM needs writable EFI vars if ! test -e "$NIX_EFI_VARS"; then - cp ${bootDisk}/efi-vars.fd "$NIX_EFI_VARS" + cp ${systemImage}/efi-vars.fd "$NIX_EFI_VARS" chmod 0644 "$NIX_EFI_VARS" fi ''} @@ -200,95 +213,29 @@ let regInfo = pkgs.closureInfo { rootPaths = config.virtualisation.additionalPaths; }; - - # Generate a hard disk image containing a /boot partition and GRUB - # in the MBR. Used when the `useBootLoader' option is set. - # Uses `runInLinuxVM` to create the image in a throwaway VM. - # See note [Disk layout with `useBootLoader`]. - # FIXME: use nixos/lib/make-disk-image.nix. - bootDisk = - pkgs.vmTools.runInLinuxVM ( - pkgs.runCommand "nixos-boot-disk" - { preVM = - '' - mkdir $out - diskImage=$out/disk.img - ${qemu}/bin/qemu-img create -f qcow2 $diskImage "120M" - ${if cfg.useEFIBoot then '' - efiVars=$out/efi-vars.fd - cp ${cfg.efi.variables} $efiVars - chmod 0644 $efiVars - '' else ""} - ''; - buildInputs = [ pkgs.util-linux ]; - QEMU_OPTS = "-nographic -serial stdio -monitor none" - + lib.optionalString cfg.useEFIBoot ( - " -drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}" - + " -drive if=pflash,format=raw,unit=1,file=$efiVars"); - } - '' - # Create a /boot EFI partition with 120M and arbitrary but fixed GUIDs for reproducibility - ${pkgs.gptfdisk}/bin/sgdisk \ - --set-alignment=1 --new=1:34:2047 --change-name=1:BIOSBootPartition --typecode=1:ef02 \ - --set-alignment=512 --largest-new=2 --change-name=2:EFISystem --typecode=2:ef00 \ - --attributes=1:set:1 \ - --attributes=2:set:2 \ - --disk-guid=97FD5997-D90B-4AA3-8D16-C1723AEA73C1 \ - --partition-guid=1:1C06F03B-704E-4657-B9CD-681A087A2FDC \ - --partition-guid=2:970C694F-AFD0-4B99-B750-CDB7A329AB6F \ - --hybrid 2 \ - --recompute-chs /dev/vda - - ${optionalString (config.boot.loader.grub.device != "/dev/vda") - # In this throwaway VM, we only have the /dev/vda disk, but the - # actual VM described by `config` (used by `switch-to-configuration` - # below) may set `boot.loader.grub.device` to a different device - # that's nonexistent in the throwaway VM. - # Create a symlink for that device, so that the `grub-install` - # by `switch-to-configuration` will hit /dev/vda anyway. - '' - ln -s /dev/vda ${config.boot.loader.grub.device} - '' - } - - ${pkgs.dosfstools}/bin/mkfs.fat -F16 /dev/vda2 - export MTOOLS_SKIP_CHECK=1 - ${pkgs.mtools}/bin/mlabel -i /dev/vda2 ::boot - - # Mount /boot; load necessary modules first. - ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_cp437.ko.xz || true - ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/nls/nls_iso8859-1.ko.xz || true - ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/fat.ko.xz || true - ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/fat/vfat.ko.xz || true - ${pkgs.kmod}/bin/insmod ${pkgs.linux}/lib/modules/*/kernel/fs/efivarfs/efivarfs.ko.xz || true - mkdir /boot - mount /dev/vda2 /boot - - ${optionalString config.boot.loader.efi.canTouchEfiVariables '' - mount -t efivarfs efivarfs /sys/firmware/efi/efivars - ''} - - # This is needed for GRUB 0.97, which doesn't know about virtio devices. - mkdir /boot/grub - echo '(hd0) /dev/vda' > /boot/grub/device.map - - # This is needed for systemd-boot to find ESP, and udev is not available here to create this - mkdir -p /dev/block - ln -s /dev/vda2 /dev/block/254:2 - - # Set up system profile (normally done by nixos-rebuild / nix-env --set) - mkdir -p /nix/var/nix/profiles - ln -s ${config.system.build.toplevel} /nix/var/nix/profiles/system-1-link - ln -s /nix/var/nix/profiles/system-1-link /nix/var/nix/profiles/system - - # Install bootloader - touch /etc/NIXOS - export NIXOS_INSTALL_BOOTLOADER=1 - ${config.system.build.toplevel}/bin/switch-to-configuration boot - - umount /boot - '' # */ - ); + # System image is akin to a complete NixOS install with + # a boot partition and root partition. + systemImage = import ../../lib/make-disk-image.nix { + inherit pkgs config lib; + additionalPaths = [ regInfo ]; + format = "qcow2"; + onlyNixStore = false; + partitionTableType = selectPartitionTableLayout { inherit (cfg) useDefaultFilesystems useEFIBoot; }; + # Bootloader should be installed on the system image only if we are booting through bootloaders. + # Though, if a user is not using our default filesystems, it is possible to not have any ESP + # or a strange partition table that's incompatible with GRUB configuration. + # As a consequence, this may lead to disk image creation failures. + # To avoid this, we prefer to let the user find out about how to install the bootloader on its ESP/disk. + # Usually, this can be through building your own disk image. + # TODO: If a user is interested into a more fine grained heuristic for `installBootLoader` + # by examining the actual contents of `cfg.fileSystems`, please send a PR. + installBootLoader = cfg.useBootLoader && cfg.useDefaultFilesystems; + touchEFIVars = cfg.useEFIBoot; + diskSize = "auto"; + additionalSpace = "0M"; + copyChannel = false; + OVMF = cfg.efi.OVMF; + }; storeImage = import ../../lib/make-disk-image.nix { inherit pkgs config lib; @@ -297,17 +244,42 @@ let onlyNixStore = true; partitionTableType = "none"; installBootLoader = false; + touchEFIVars = false; diskSize = "auto"; additionalSpace = "0M"; copyChannel = false; }; + bootConfiguration = + if cfg.useDefaultFilesystems + then + if cfg.useBootLoader + then + if cfg.useEFIBoot then "efi_bootloading_with_default_fs" + else "legacy_bootloading_with_default_fs" + else + "direct_boot_with_default_fs" + else + "custom"; + suggestedRootDevice = { + "efi_bootloading_with_default_fs" = "${cfg.bootLoaderDevice}2"; + "legacy_bootloading_with_default_fs" = "${cfg.bootLoaderDevice}1"; + "direct_boot_with_default_fs" = cfg.bootLoaderDevice; + # This will enforce a NixOS module type checking error + # to ask explicitly the user to set a rootDevice. + # As it will look like `rootDevice = lib.mkDefault null;` after + # all "computations". + "custom" = null; + }.${bootConfiguration}; in { imports = [ ../profiles/qemu-guest.nix (mkRenamedOptionModule [ "virtualisation" "pathsInNixDB" ] [ "virtualisation" "additionalPaths" ]) + (mkRemovedOptionModule [ "virtualisation" "bootDevice" ] "This option was renamed to `virtualisation.rootDevice`, as it was incorrectly named and misleading. Take the time to review what you want to do and look at the new options like `virtualisation.{bootLoaderDevice, bootPartition}`, open an issue in case of issues.") + (mkRemovedOptionModule [ "virtualisation" "efiVars" ] "This option was removed, it is possible to provide a template UEFI variable with `virtualisation.efi.variables` ; if this option is important to you, open an issue") + (mkRemovedOptionModule [ "virtualisation" "persistBootDevice" ] "Boot device is always persisted if you use a bootloader through the root disk image ; if this does not work for your usecase, please examine carefully what `virtualisation.{bootDevice, rootDevice, bootPartition}` options offer you and open an issue explaining your need.`") ]; options = { @@ -362,25 +334,48 @@ in ''; }; - virtualisation.bootDevice = + virtualisation.bootLoaderDevice = mkOption { type = types.path; + default = lookupDriveDeviceName "root" cfg.qemu.drives; + defaultText = literalExpression ''lookupDriveDeviceName "root" cfg.qemu.drives''; example = "/dev/vda"; description = lib.mdDoc '' - The disk to be used for the root filesystem. + The disk to be used for the boot filesystem. + By default, it is the same disk as the root filesystem. + ''; + }; + + virtualisation.bootPartition = + mkOption { + type = types.nullOr types.path; + default = if cfg.useEFIBoot then "${cfg.bootLoaderDevice}1" else null; + defaultText = literalExpression ''if cfg.useEFIBoot then "''${cfg.bootLoaderDevice}1" else null''; + example = "/dev/vda1"; + description = + lib.mdDoc '' + The boot partition to be used to mount /boot filesystem. + In legacy boots, this should be null. + By default, in EFI boot, it is the first partition of the boot device. ''; }; - virtualisation.persistBootDevice = + virtualisation.rootDevice = mkOption { - type = types.bool; - default = false; + type = types.nullOr types.path; + example = "/dev/vda2"; description = lib.mdDoc '' - If useBootLoader is specified, whether to recreate the boot device - on each instantiaton or allow it to persist. - ''; + The disk or partition to be used for the root filesystem. + By default (read the source code for more details): + + - under EFI with a bootloader: 2nd partition of the boot disk + - in legacy boot with a bootloader: 1st partition of the boot disk + - in direct boot (i.e. without a bootloader): whole disk + + In case you are not using a default boot device or a default filesystem, you have to set explicitly your root device. + ''; }; virtualisation.emptyDiskImages = @@ -749,10 +744,22 @@ in }; virtualisation.efi = { + OVMF = mkOption { + type = types.package; + default = (pkgs.OVMF.override { + secureBoot = cfg.useSecureBoot; + }).fd; + defaultText = ''(pkgs.OVMF.override { + secureBoot = cfg.useSecureBoot; + }).fd''; + description = + lib.mdDoc "OVMF firmware package, defaults to OVMF configured with secure boot if needed."; + }; + firmware = mkOption { type = types.path; - default = pkgs.OVMF.firmware; - defaultText = literalExpression "pkgs.OVMF.firmware"; + default = cfg.efi.OVMF.firmware; + defaultText = literalExpression "cfg.efi.OVMF.firmware"; description = lib.mdDoc '' Firmware binary for EFI implementation, defaults to OVMF. @@ -761,8 +768,8 @@ in variables = mkOption { type = types.path; - default = pkgs.OVMF.variables; - defaultText = literalExpression "pkgs.OVMF.variables"; + default = cfg.efi.OVMF.variables; + defaultText = literalExpression "cfg.efi.OVMF.variables"; description = lib.mdDoc '' Platform-specific flash binary for EFI variables, implementation-dependent to the EFI firmware. @@ -786,18 +793,17 @@ in ''; }; - virtualisation.efiVars = + virtualisation.useSecureBoot = mkOption { - type = types.str; - default = "./${config.system.name}-efi-vars.fd"; - defaultText = literalExpression ''"./''${config.system.name}-efi-vars.fd"''; + type = types.bool; + default = false; description = lib.mdDoc '' - Path to nvram image containing UEFI variables. The will be created - on startup if it does not exist. + Enable Secure Boot support in the EFI firmware. ''; }; + virtualisation.bios = mkOption { type = types.nullOr types.package; @@ -853,33 +859,18 @@ in ${opt.writableStore} = false; ''; - # Note [Disk layout with `useBootLoader`] - # - # If `useBootLoader = true`, we configure 2 drives: - # `/dev/?da` for the root disk, and `/dev/?db` for the boot disk - # which has the `/boot` partition and the boot loader. - # Concretely: - # - # * The second drive's image `disk.img` is created in `bootDisk = ...` - # using a throwaway VM. Note that there the disk is always `/dev/vda`, - # even though in the final VM it will be at `/dev/*b`. - # * The disks are attached in `virtualisation.qemu.drives`. - # Their order makes them appear as devices `a`, `b`, etc. - # * `fileSystems."/boot"` is adjusted to be on device `b`. - # * The disk.img is recreated each time the VM is booted unless - # virtualisation.persistBootDevice is set. - - # If `useBootLoader`, GRUB goes to the second disk, see - # note [Disk layout with `useBootLoader`]. - boot.loader.grub.device = mkVMOverride ( - if cfg.useBootLoader - then driveDeviceName 2 # second disk - else cfg.bootDevice - ); + # In UEFI boot, we use a EFI-only partition table layout, thus GRUB will fail when trying to install + # legacy and UEFI. In order to avoid this, we have to put "nodev" to force UEFI-only installs. + # Otherwise, we set the proper bootloader device for this. + # FIXME: make a sense of this mess wrt to multiple ESP present in the system, probably use boot.efiSysMountpoint? + boot.loader.grub.device = mkVMOverride (if cfg.useEFIBoot then "nodev" else cfg.bootLoaderDevice); boot.loader.grub.gfxmodeBios = with cfg.resolution; "${toString x}x${toString y}"; + virtualisation.rootDevice = mkDefault suggestedRootDevice; boot.initrd.kernelModules = optionals (cfg.useNixStoreImage && !cfg.writableStore) [ "erofs" ]; + boot.loader.supportsInitrdSecrets = mkIf (!cfg.useBootLoader) (mkVMOverride false); + boot.initrd.extraUtilsCommands = lib.mkIf (cfg.useDefaultFilesystems && !config.boot.initrd.systemd.enable) '' # We need mke2fs in the initrd. @@ -890,10 +881,10 @@ in '' # If the disk image appears to be empty, run mke2fs to # initialise. - FSTYPE=$(blkid -o value -s TYPE ${cfg.bootDevice} || true) - PARTTYPE=$(blkid -o value -s PTTYPE ${cfg.bootDevice} || true) + FSTYPE=$(blkid -o value -s TYPE ${cfg.rootDevice} || true) + PARTTYPE=$(blkid -o value -s PTTYPE ${cfg.rootDevice} || true) if test -z "$FSTYPE" -a -z "$PARTTYPE"; then - mke2fs -t ext4 ${cfg.bootDevice} + mke2fs -t ext4 ${cfg.rootDevice} fi ''; @@ -939,8 +930,6 @@ in optional cfg.writableStore "overlay" ++ optional (cfg.qemu.diskInterface == "scsi") "sym53c8xx"; - virtualisation.bootDevice = mkDefault (driveDeviceName 1); - virtualisation.additionalPaths = [ config.system.build.toplevel ]; virtualisation.sharedDirectories = { @@ -997,7 +986,7 @@ in ]) (mkIf cfg.useEFIBoot [ "-drive if=pflash,format=raw,unit=0,readonly=on,file=${cfg.efi.firmware}" - "-drive if=pflash,format=raw,unit=1,file=$NIX_EFI_VARS" + "-drive if=pflash,format=raw,unit=1,readonly=off,file=$NIX_EFI_VARS" ]) (mkIf (cfg.bios != null) [ "-bios ${cfg.bios}/bios.bin" @@ -1013,23 +1002,14 @@ in file = ''"$NIX_DISK_IMAGE"''; driveExtraOpts.cache = "writeback"; driveExtraOpts.werror = "report"; + deviceExtraOpts.bootindex = "1"; }]) (mkIf cfg.useNixStoreImage [{ name = "nix-store"; file = ''"$TMPDIR"/store.img''; - deviceExtraOpts.bootindex = if cfg.useBootLoader then "3" else "2"; + deviceExtraOpts.bootindex = "2"; driveExtraOpts.format = if cfg.writableStore then "qcow2" else "raw"; }]) - (mkIf cfg.useBootLoader [ - # The order of this list determines the device names, see - # note [Disk layout with `useBootLoader`]. - { - name = "boot"; - file = ''"$TMPDIR"/disk.img''; - driveExtraOpts.media = "disk"; - deviceExtraOpts.bootindex = "1"; - } - ]) (imap0 (idx: _: { file = "$(pwd)/empty${toString idx}.qcow2"; driveExtraOpts.werror = "report"; @@ -1065,7 +1045,7 @@ in device = "tmpfs"; fsType = "tmpfs"; } else { - device = cfg.bootDevice; + device = cfg.rootDevice; fsType = "ext4"; autoFormat = true; }); @@ -1086,9 +1066,8 @@ in options = [ "mode=0755" ]; neededForBoot = true; }; - # see note [Disk layout with `useBootLoader`] - "/boot" = lib.mkIf cfg.useBootLoader { - device = "${lookupDriveDeviceName "boot" cfg.qemu.drives}2"; # 2 for e.g. `vdb2`, as created in `bootDisk` + "/boot" = lib.mkIf (cfg.useBootLoader && cfg.bootPartition != null) { + device = cfg.bootPartition; # 1 for e.g. `vda1`, as created in `systemImage` fsType = "vfat"; noCheck = true; # fsck fails on a r/o filesystem }; |