diff options
Diffstat (limited to 'nixos/modules')
168 files changed, 4577 insertions, 2332 deletions
diff --git a/nixos/modules/config/ldso.nix b/nixos/modules/config/ldso.nix index 72ae3958d8869..bd6f0dc5a83b8 100644 --- a/nixos/modules/config/ldso.nix +++ b/nixos/modules/config/ldso.nix @@ -6,9 +6,9 @@ let libDir = pkgs.stdenv.hostPlatform.libDir; ldsoBasename = builtins.unsafeDiscardStringContext (last (splitString "/" pkgs.stdenv.cc.bintools.dynamicLinker)); - pkgs32 = pkgs.pkgsi686Linux; - libDir32 = pkgs32.stdenv.hostPlatform.libDir; - ldsoBasename32 = builtins.unsafeDiscardStringContext (last (splitString "/" pkgs32.stdenv.cc.bintools.dynamicLinker)); + # Hard-code to avoid creating another instance of nixpkgs. Also avoids eval errors in some cases. + libDir32 = "lib"; # pkgs.pkgsi686Linux.stdenv.hostPlatform.libDir + ldsoBasename32 = "ld-linux.so.2"; # last (splitString "/" pkgs.pkgsi686Linux.stdenv.cc.bintools.dynamicLinker) in { options = { environment.ldso = mkOption { diff --git a/nixos/modules/config/nix.nix b/nixos/modules/config/nix.nix index 2769d8b25ef6f..e6a74bbb73fcc 100644 --- a/nixos/modules/config/nix.nix +++ b/nixos/modules/config/nix.nix @@ -1,5 +1,5 @@ /* - Manages /etc/nix.conf. + Manages /etc/nix/nix.conf. See also - ./nix-channel.nix diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix index 2f763290e32dd..870b3fe77cca9 100644 --- a/nixos/modules/config/no-x-libs.nix +++ b/nixos/modules/config/no-x-libs.nix @@ -67,7 +67,7 @@ with lib; networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; }; pango = super.pango.override { x11Support = false; }; pinentry = super.pinentry.override { enabledFlavors = [ "curses" "tty" "emacs" ]; withLibsecret = false; }; - pipewire = super.pipewire.override { x11Support = false; }; + pipewire = super.pipewire.override { vulkanSupport = false; x11Support = false; }; pythonPackagesExtensions = super.pythonPackagesExtensions ++ [ (python-final: python-prev: { # tk feature requires wayland which fails to compile @@ -83,6 +83,7 @@ with lib; # translateManpages -> perlPackages.po4a -> texlive-combined-basic -> texlive-core-big -> libX11 util-linux = super.util-linux.override { translateManpages = false; }; vim-full = super.vim-full.override { guiSupport = false; }; + vte = super.vte.override { gtkVersion = null; }; zbar = super.zbar.override { enableVideo = false; withXorg = false; }; })); }; diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 967ad0846d75b..dd34771c0b42b 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -649,7 +649,6 @@ in { home = "/root"; shell = mkDefault cfg.defaultUserShell; group = "root"; - initialHashedPassword = mkDefault "!"; }; nobody = { uid = ids.uids.nobody; @@ -897,7 +896,26 @@ in { )); warnings = - builtins.filter (x: x != null) ( + flip concatMap (attrValues cfg.users) (user: let + unambiguousPasswordConfiguration = 1 >= length (filter (x: x != null) ([ + user.hashedPassword + user.hashedPasswordFile + user.password + ] ++ optionals cfg.mutableUsers [ + # For immutable users, initialHashedPassword is set to hashedPassword, + # so using these options would always trigger the assertion. + user.initialHashedPassword + user.initialPassword + ])); + in optional (!unambiguousPasswordConfiguration) '' + The user '${user.name}' has multiple of the options + `hashedPassword`, `password`, `hashedPasswordFile`, `initialPassword` + & `initialHashedPassword` set to a non-null value. + The options silently discard others by the order of precedence + given above which can lead to surprising results. To resolve this warning, + set at most one of the options above to a non-`null` value. + '') + ++ builtins.filter (x: x != null) ( flip mapAttrsToList cfg.users (_: user: # This regex matches a subset of the Modular Crypto Format (MCF)[1] # informal standard. Since this depends largely on the OS or the diff --git a/nixos/modules/config/vte.nix b/nixos/modules/config/vte.nix index a969607f6e0b0..48f85246560a9 100644 --- a/nixos/modules/config/vte.nix +++ b/nixos/modules/config/vte.nix @@ -1,5 +1,3 @@ -# VTE - { config, pkgs, lib, ... }: with lib; @@ -9,7 +7,7 @@ let vteInitSnippet = '' # Show current working directory in VTE terminals window title. # Supports both bash and zsh, requires interactive shell. - . ${pkgs.vte}/etc/profile.d/vte.sh + . ${pkgs.vte.override { gtkVersion = null; }}/etc/profile.d/vte.sh ''; in diff --git a/nixos/modules/config/xdg/portal.nix b/nixos/modules/config/xdg/portal.nix index 07d4fa76c2e8b..5aa23377f9ffb 100644 --- a/nixos/modules/config/xdg/portal.nix +++ b/nixos/modules/config/xdg/portal.nix @@ -119,19 +119,6 @@ in let cfg = config.xdg.portal; packages = [ pkgs.xdg-desktop-portal ] ++ cfg.extraPortals; - configPackages = cfg.configPackages; - - joinedPortals = pkgs.buildEnv { - name = "xdg-portals"; - paths = packages; - pathsToLink = [ "/share/xdg-desktop-portal/portals" "/share/applications" ]; - }; - - joinedPortalConfigs = pkgs.buildEnv { - name = "xdg-portal-configs"; - paths = configPackages; - pathsToLink = [ "/share/xdg-desktop-portal" ]; - }; in mkIf cfg.enable { warnings = lib.optional (cfg.configPackages == [ ] && cfg.config == { }) '' @@ -158,17 +145,18 @@ in systemd.packages = packages; environment = { - # fixes screen sharing on plasmawayland on non-chromium apps by linking - # share/applications/*.desktop files - # see https://github.com/NixOS/nixpkgs/issues/145174 - systemPackages = [ joinedPortals ]; - pathsToLink = [ "/share/applications" ]; + systemPackages = packages ++ cfg.configPackages; + pathsToLink = [ + # Portal definitions and upstream desktop environment portal configurations. + "/share/xdg-desktop-portal" + # .desktop files to register fallback icon and app name. + "/share/applications" + ]; sessionVariables = { GTK_USE_PORTAL = mkIf cfg.gtkUsePortal "1"; NIXOS_XDG_OPEN_USE_PORTAL = mkIf cfg.xdgOpenUsePortal "1"; - XDG_DESKTOP_PORTAL_DIR = "${joinedPortals}/share/xdg-desktop-portal/portals"; - NIXOS_XDG_DESKTOP_PORTAL_CONFIG_DIR = mkIf (cfg.configPackages != [ ]) "${joinedPortalConfigs}/share/xdg-desktop-portal"; + NIX_XDG_DESKTOP_PORTAL_DIR = "/run/current-system/sw/share/xdg-desktop-portal/portals"; }; etc = lib.concatMapAttrs diff --git a/nixos/modules/hardware/printers.nix b/nixos/modules/hardware/printers.nix index 846ff6f3fb4f5..4fb6a192cdd2c 100644 --- a/nixos/modules/hardware/printers.nix +++ b/nixos/modules/hardware/printers.nix @@ -2,18 +2,23 @@ with lib; let cfg = config.hardware.printers; - ppdOptionsString = options: optionalString (options != {}) - (concatStringsSep " " - (mapAttrsToList (name: value: "-o '${name}'='${value}'") options) - ); - ensurePrinter = p: '' - ${pkgs.cups}/bin/lpadmin -p '${p.name}' -E \ - ${optionalString (p.location != null) "-L '${p.location}'"} \ - ${optionalString (p.description != null) "-D '${p.description}'"} \ - -v '${p.deviceUri}' \ - -m '${p.model}' \ - ${ppdOptionsString p.ppdOptions} + + ensurePrinter = p: let + args = cli.toGNUCommandLineShell {} ({ + p = p.name; + v = p.deviceUri; + m = p.model; + } // optionalAttrs (p.location != null) { + L = p.location; + } // optionalAttrs (p.description != null) { + D = p.description; + } // optionalAttrs (p.ppdOptions != {}) { + o = mapAttrsToList (name: value: "'${name}'='${value}'") p.ppdOptions; + }); + in '' + ${pkgs.cups}/bin/lpadmin ${args} -E ''; + ensureDefaultPrinter = name: '' ${pkgs.cups}/bin/lpadmin -d '${name}' ''; diff --git a/nixos/modules/hardware/video/switcheroo-control.nix b/nixos/modules/hardware/video/switcheroo-control.nix index 982388f8e5f4e..967120d6744a1 100644 --- a/nixos/modules/hardware/video/switcheroo-control.nix +++ b/nixos/modules/hardware/video/switcheroo-control.nix @@ -1,18 +1,19 @@ { config, pkgs, lib, ... }: -with lib; let - pkg = [ pkgs.switcheroo-control ]; cfg = config.services.switcherooControl; in { options.services.switcherooControl = { - enable = mkEnableOption (lib.mdDoc "switcheroo-control, a D-Bus service to check the availability of dual-GPU"); + enable = lib.mkEnableOption "switcheroo-control, a D-Bus service to check the availability of dual-GPU"; + package = lib.mkPackageOption pkgs "switcheroo-control" { }; }; - config = mkIf cfg.enable { - services.dbus.packages = pkg; - environment.systemPackages = pkg; - systemd.packages = pkg; - systemd.targets.multi-user.wants = [ "switcheroo-control.service" ]; + config = lib.mkIf cfg.enable { + services.dbus.packages = [ cfg.package ]; + environment.systemPackages = [ cfg.package ]; + systemd = { + packages = [ cfg.package ]; + targets.multi-user.wants = [ "switcheroo-control.service" ]; + }; }; } diff --git a/nixos/modules/i18n/input-method/fcitx5.nix b/nixos/modules/i18n/input-method/fcitx5.nix index 530727f3f2928..2e87705c6dc2d 100644 --- a/nixos/modules/i18n/input-method/fcitx5.nix +++ b/nixos/modules/i18n/input-method/fcitx5.nix @@ -5,7 +5,10 @@ with lib; let im = config.i18n.inputMethod; cfg = im.fcitx5; - fcitx5Package = pkgs.fcitx5-with-addons.override { inherit (cfg) addons; }; + fcitx5Package = + if cfg.plasma6Support + then pkgs.qt6Packages.fcitx5-with-addons.override { inherit (cfg) addons; } + else pkgs.libsForQt5.fcitx5-with-addons.override { inherit (cfg) addons; }; settingsFormat = pkgs.formats.ini { }; in { @@ -27,6 +30,14 @@ in See [Using Fcitx 5 on Wayland](https://fcitx-im.org/wiki/Using_Fcitx_5_on_Wayland). ''; }; + plasma6Support = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Use qt6 versions of fcitx5 packages. + Required for configuring fcitx5 in KDE System Settings. + ''; + }; quickPhrase = mkOption { type = with types; attrsOf str; default = { }; diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma6.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma6.nix new file mode 100644 index 0000000000000..11118db3aae2a --- /dev/null +++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-calamares-plasma6.nix @@ -0,0 +1,46 @@ +# This module defines a NixOS installation CD that contains Plasma 6. + +{ pkgs, ... }: + +{ + imports = [ ./installation-cd-graphical-calamares.nix ]; + + isoImage.edition = "plasma6"; + + services.xserver = { + desktopManager.plasma6.enable = true; + + # Automatically login as nixos. + displayManager = { + sddm.enable = true; + autoLogin = { + enable = true; + user = "nixos"; + }; + }; + }; + + environment.systemPackages = [ + # FIXME: using Qt5 builds of Maliit as upstream has not ported to Qt6 yet + pkgs.maliit-framework + pkgs.maliit-keyboard + ]; + + system.activationScripts.installerDesktop = let + + # Comes from documentation.nix when xserver and nixos.enable are true. + manualDesktopFile = "/run/current-system/sw/share/applications/nixos-manual.desktop"; + + homeDir = "/home/nixos/"; + desktopDir = homeDir + "Desktop/"; + + in '' + mkdir -p ${desktopDir} + chown nixos ${homeDir} ${desktopDir} + + ln -sfT ${manualDesktopFile} ${desktopDir + "nixos-manual.desktop"} + ln -sfT ${pkgs.gparted}/share/applications/gparted.desktop ${desktopDir + "gparted.desktop"} + ln -sfT ${pkgs.calamares-nixos}/share/applications/io.calamares.calamares.desktop ${desktopDir + "io.calamares.calamares.desktop"} + ''; + +} diff --git a/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix b/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix index 9d09cdbe0206d..fc3cb08bdbbb3 100644 --- a/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix +++ b/nixos/modules/installer/cd-dvd/installation-cd-minimal-new-kernel-no-zfs.nix @@ -1,15 +1,7 @@ -{ pkgs, ... }: +{ lib, ... }: { imports = [ ./installation-cd-minimal-new-kernel.nix ]; - # Makes `availableOn` fail for zfs, see <nixos/modules/profiles/base.nix>. - # This is a workaround since we cannot remove the `"zfs"` string from `supportedFilesystems`. - # The proper fix would be to make `supportedFilesystems` an attrset with true/false which we - # could then `lib.mkForce false` - nixpkgs.overlays = [(final: super: { - zfs = super.zfs.overrideAttrs(_: { - meta.platforms = []; - }); - })]; + boot.supportedFilesystems.zfs = lib.mkForce false; } diff --git a/nixos/modules/installer/netboot/netboot.nix b/nixos/modules/installer/netboot/netboot.nix index a50f22cbe471d..028a2d74041e6 100644 --- a/nixos/modules/installer/netboot/netboot.nix +++ b/nixos/modules/installer/netboot/netboot.nix @@ -62,19 +62,12 @@ with lib; }; fileSystems."/nix/store" = mkImageMediaOverride - { fsType = "overlay"; - device = "overlay"; - options = [ - "lowerdir=/nix/.ro-store" - "upperdir=/nix/.rw-store/store" - "workdir=/nix/.rw-store/work" - ]; - - depends = [ - "/nix/.ro-store" - "/nix/.rw-store/store" - "/nix/.rw-store/work" - ]; + { overlay = { + lowerdir = [ "/nix/.ro-store" ]; + upperdir = "/nix/.rw-store/store"; + workdir = "/nix/.rw-store/work"; + }; + neededForBoot = true; }; boot.initrd.availableKernelModules = [ "squashfs" "overlay" ]; diff --git a/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix index 0e50559602945..da5410057887e 100644 --- a/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix +++ b/nixos/modules/installer/sd-card/sd-image-aarch64-new-kernel-no-zfs-installer.nix @@ -1,15 +1,7 @@ -{ pkgs, ... }: +{ lib, ... }: { imports = [ ./sd-image-aarch64-new-kernel-installer.nix ]; - # Makes `availableOn` fail for zfs, see <nixos/modules/profiles/base.nix>. - # This is a workaround since we cannot remove the `"zfs"` string from `supportedFilesystems`. - # The proper fix would be to make `supportedFilesystems` an attrset with true/false which we - # could then `lib.mkForce false` - nixpkgs.overlays = [(final: super: { - zfs = super.zfs.overrideAttrs(_: { - meta.platforms = []; - }); - })]; + boot.supportedFilesystems.zfs = lib.mkForce false; } diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix index 5af7284ac71af..cfa98c838af52 100644 --- a/nixos/modules/misc/ids.nix +++ b/nixos/modules/misc/ids.nix @@ -313,7 +313,7 @@ in kanboard = 281; # pykms = 282; # DynamicUser = true kodi = 283; - restya-board = 284; + # restya-board = 284; # removed 2024-01-22 mighttpd2 = 285; hass = 286; #monero = 287; # dynamically allocated as of 2021-05-08 @@ -623,7 +623,7 @@ in kanboard = 281; # pykms = 282; # DynamicUser = true kodi = 283; - restya-board = 284; + # restya-board = 284; # removed 2024-01-22 mighttpd2 = 285; hass = 286; # monero = 287; # dynamically allocated as of 2021-05-08 diff --git a/nixos/modules/misc/nixpkgs-flake.nix b/nixos/modules/misc/nixpkgs-flake.nix new file mode 100644 index 0000000000000..8bfe05ca19949 --- /dev/null +++ b/nixos/modules/misc/nixpkgs-flake.nix @@ -0,0 +1,105 @@ +{ config, options, lib, pkgs, ... }: + +with lib; + +let + cfg = config.nixpkgs.flake; +in +{ + options.nixpkgs.flake = { + source = mkOption { + # In newer Nix versions, particularly with lazy trees, outPath of + # flakes becomes a Nix-language path object. We deliberately allow this + # to gracefully come through the interface in discussion with @roberth. + # + # See: https://github.com/NixOS/nixpkgs/pull/278522#discussion_r1460292639 + type = types.nullOr (types.either types.str types.path); + + default = null; + defaultText = "if (using nixpkgsFlake.lib.nixosSystem) then self.outPath else null"; + + example = ''builtins.fetchTarball { name = "source"; sha256 = "${lib.fakeHash}"; url = "https://github.com/nixos/nixpkgs/archive/somecommit.tar.gz"; }''; + + description = mdDoc '' + The path to the nixpkgs sources used to build the system. This is automatically set up to be + the store path of the nixpkgs flake used to build the system if using + `nixpkgs.lib.nixosSystem`, and is otherwise null by default. + + This can also be optionally set if the NixOS system is not built with a flake but still uses + pinned sources: set this to the store path for the nixpkgs sources used to build the system, + as may be obtained by `builtins.fetchTarball`, for example. + + Note: the name of the store path must be "source" due to + <https://github.com/NixOS/nix/issues/7075>. + ''; + }; + + setNixPath = mkOption { + type = types.bool; + + default = cfg.source != null; + defaultText = "config.nixpkgs.flake.source != null"; + + description = mdDoc '' + Whether to set {env}`NIX_PATH` to include `nixpkgs=flake:nixpkgs` such that `<nixpkgs>` + lookups receive the version of nixpkgs that the system was built with, in concert with + {option}`nixpkgs.flake.setFlakeRegistry`. + + This is on by default for NixOS configurations built with flakes. + + This makes {command}`nix-build '<nixpkgs>' -A hello` work out of the box on flake systems. + + Note that this option makes the NixOS closure depend on the nixpkgs sources, which may add + undesired closure size if the system will not have any nix commands run on it. + ''; + }; + + setFlakeRegistry = mkOption { + type = types.bool; + + default = cfg.source != null; + defaultText = "config.nixpkgs.flake.source != null"; + + description = mdDoc '' + Whether to pin nixpkgs in the system-wide flake registry (`/etc/nix/registry.json`) to the + store path of the sources of nixpkgs used to build the NixOS system. + + This is on by default for NixOS configurations built with flakes. + + This option makes {command}`nix run nixpkgs#hello` reuse dependencies from the system, avoid + refetching nixpkgs, and have a consistent result every time. + + Note that this option makes the NixOS closure depend on the nixpkgs sources, which may add + undesired closure size if the system will not have any nix commands run on it. + ''; + }; + }; + + config = mkIf (cfg.source != null) (mkMerge [ + { + assertions = [ + { + assertion = cfg.setNixPath -> cfg.setFlakeRegistry; + message = '' + Setting `nixpkgs.flake.setNixPath` requires that `nixpkgs.flake.setFlakeRegistry` also + be set, since it is implemented in terms of indirection through the flake registry. + ''; + } + ]; + } + (mkIf cfg.setFlakeRegistry { + nix.registry.nixpkgs.to = mkDefault { + type = "path"; + path = cfg.source; + }; + }) + (mkIf cfg.setNixPath { + # N.B. This does not include nixos-config in NIX_PATH unlike modules/config/nix-channel.nix + # because we would need some kind of evil shim taking the *calling* flake's self path, + # perhaps, to ever make that work (in order to know where the Nix expr for the system came + # from and how to call it). + nix.nixPath = mkDefault ([ "nixpkgs=flake:nixpkgs" ] + ++ optional config.nix.channel.enable "/nix/var/nix/profiles/per-user/root/channels"); + }) + ]); +} diff --git a/nixos/modules/misc/nixpkgs.nix b/nixos/modules/misc/nixpkgs.nix index da321a9234493..10f800cd741a0 100644 --- a/nixos/modules/misc/nixpkgs.nix +++ b/nixos/modules/misc/nixpkgs.nix @@ -208,7 +208,11 @@ in example = { system = "x86_64-linux"; }; # Make sure that the final value has all fields for sake of other modules # referring to this. - apply = lib.systems.elaborate; + apply = inputBuildPlatform: + let elaborated = lib.systems.elaborate inputBuildPlatform; + in if lib.systems.equals elaborated cfg.hostPlatform + then cfg.hostPlatform # make identical, so that `==` equality works; see https://github.com/NixOS/nixpkgs/issues/278001 + else elaborated; defaultText = literalExpression ''config.nixpkgs.hostPlatform''; description = lib.mdDoc '' diff --git a/nixos/modules/misc/nixpkgs/test.nix b/nixos/modules/misc/nixpkgs/test.nix index 0536cfc9624a2..be9a88a077887 100644 --- a/nixos/modules/misc/nixpkgs/test.nix +++ b/nixos/modules/misc/nixpkgs/test.nix @@ -12,6 +12,10 @@ let nixpkgs.hostPlatform = "aarch64-linux"; nixpkgs.buildPlatform = "aarch64-darwin"; }; + withSameHostAndBuild = eval { + nixpkgs.hostPlatform = "aarch64-linux"; + nixpkgs.buildPlatform = "aarch64-linux"; + }; ambiguous = { _file = "ambiguous.nix"; nixpkgs.hostPlatform = "aarch64-linux"; @@ -81,6 +85,8 @@ lib.recurseIntoAttrs { assert withHost._module.args.pkgs.stdenv.buildPlatform.system == "aarch64-linux"; assert withHostAndBuild._module.args.pkgs.stdenv.hostPlatform.system == "aarch64-linux"; assert withHostAndBuild._module.args.pkgs.stdenv.buildPlatform.system == "aarch64-darwin"; + assert withSameHostAndBuild.config.nixpkgs.buildPlatform == withSameHostAndBuild.config.nixpkgs.hostPlatform; + assert withSameHostAndBuild._module.args.pkgs.stdenv.buildPlatform == withSameHostAndBuild._module.args.pkgs.stdenv.hostPlatform; assert builtins.trace (lib.head (getErrors ambiguous)) getErrors ambiguous == ['' diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix index c929c3b37285b..79b95ac654d55 100644 --- a/nixos/modules/misc/version.nix +++ b/nixos/modules/misc/version.nix @@ -5,34 +5,39 @@ let opt = options.system.nixos; inherit (lib) - concatStringsSep mapAttrsToList toLower + concatStringsSep mapAttrsToList toLower optionalString literalExpression mkRenamedOptionModule mkDefault mkOption trivial types; needsEscaping = s: null != builtins.match "[a-zA-Z0-9]+" s; escapeIfNecessary = s: if needsEscaping s then s else ''"${lib.escape [ "\$" "\"" "\\" "\`" ] s}"''; attrsToText = attrs: - concatStringsSep "\n" ( - mapAttrsToList (n: v: ''${n}=${escapeIfNecessary (toString v)}'') attrs - ) + "\n"; - - osReleaseContents = { - NAME = "${cfg.distroName}"; - ID = "${cfg.distroId}"; - VERSION = "${cfg.release} (${cfg.codeName})"; - VERSION_CODENAME = toLower cfg.codeName; - VERSION_ID = cfg.release; - BUILD_ID = cfg.version; - PRETTY_NAME = "${cfg.distroName} ${cfg.release} (${cfg.codeName})"; - LOGO = "nix-snowflake"; - HOME_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/"; - DOCUMENTATION_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/learn.html"; - SUPPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/community.html"; - BUG_REPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://github.com/NixOS/nixpkgs/issues"; - IMAGE_ID = lib.optionalString (config.system.image.id != null) config.system.image.id; - IMAGE_VERSION = lib.optionalString (config.system.image.version != null) config.system.image.version; - } // lib.optionalAttrs (cfg.variant_id != null) { - VARIANT_ID = cfg.variant_id; - }; + concatStringsSep "\n" + (mapAttrsToList (n: v: ''${n}=${escapeIfNecessary (toString v)}'') attrs) + + "\n"; + + osReleaseContents = + let + isNixos = cfg.distroId == "nixos"; + in + { + NAME = "${cfg.distroName}"; + ID = "${cfg.distroId}"; + VERSION = "${cfg.release} (${cfg.codeName})"; + VERSION_CODENAME = toLower cfg.codeName; + VERSION_ID = cfg.release; + BUILD_ID = cfg.version; + PRETTY_NAME = "${cfg.distroName} ${cfg.release} (${cfg.codeName})"; + LOGO = "nix-snowflake"; + HOME_URL = optionalString isNixos "https://nixos.org/"; + DOCUMENTATION_URL = optionalString isNixos "https://nixos.org/learn.html"; + SUPPORT_URL = optionalString isNixos "https://nixos.org/community.html"; + BUG_REPORT_URL = optionalString isNixos "https://github.com/NixOS/nixpkgs/issues"; + ANSI_COLOR = optionalString isNixos "1;34"; + IMAGE_ID = optionalString (config.system.image.id != null) config.system.image.id; + IMAGE_VERSION = optionalString (config.system.image.version != null) config.system.image.version; + } // lib.optionalAttrs (cfg.variant_id != null) { + VARIANT_ID = cfg.variant_id; + }; initrdReleaseContents = (removeAttrs osReleaseContents [ "BUILD_ID" ]) // { PRETTY_NAME = "${osReleaseContents.PRETTY_NAME} (Initrd)"; @@ -56,60 +61,61 @@ in }; options.system = { + nixos = { + version = mkOption { + internal = true; + type = types.str; + description = lib.mdDoc "The full NixOS version (e.g. `16.03.1160.f2d4ee1`)."; + }; - nixos.version = mkOption { - internal = true; - type = types.str; - description = lib.mdDoc "The full NixOS version (e.g. `16.03.1160.f2d4ee1`)."; - }; - - nixos.release = mkOption { - readOnly = true; - type = types.str; - default = trivial.release; - description = lib.mdDoc "The NixOS release (e.g. `16.03`)."; - }; + release = mkOption { + readOnly = true; + type = types.str; + default = trivial.release; + description = lib.mdDoc "The NixOS release (e.g. `16.03`)."; + }; - nixos.versionSuffix = mkOption { - internal = true; - type = types.str; - default = trivial.versionSuffix; - description = lib.mdDoc "The NixOS version suffix (e.g. `1160.f2d4ee1`)."; - }; + versionSuffix = mkOption { + internal = true; + type = types.str; + default = trivial.versionSuffix; + description = lib.mdDoc "The NixOS version suffix (e.g. `1160.f2d4ee1`)."; + }; - nixos.revision = mkOption { - internal = true; - type = types.nullOr types.str; - default = trivial.revisionWithDefault null; - description = lib.mdDoc "The Git revision from which this NixOS configuration was built."; - }; + revision = mkOption { + internal = true; + type = types.nullOr types.str; + default = trivial.revisionWithDefault null; + description = lib.mdDoc "The Git revision from which this NixOS configuration was built."; + }; - nixos.codeName = mkOption { - readOnly = true; - type = types.str; - default = trivial.codeName; - description = lib.mdDoc "The NixOS release code name (e.g. `Emu`)."; - }; + codeName = mkOption { + readOnly = true; + type = types.str; + default = trivial.codeName; + description = lib.mdDoc "The NixOS release code name (e.g. `Emu`)."; + }; - nixos.distroId = mkOption { - internal = true; - type = types.str; - default = "nixos"; - description = lib.mdDoc "The id of the operating system"; - }; + distroId = mkOption { + internal = true; + type = types.str; + default = "nixos"; + description = lib.mdDoc "The id of the operating system"; + }; - nixos.distroName = mkOption { - internal = true; - type = types.str; - default = "NixOS"; - description = lib.mdDoc "The name of the operating system"; - }; + distroName = mkOption { + internal = true; + type = types.str; + default = "NixOS"; + description = lib.mdDoc "The name of the operating system"; + }; - nixos.variant_id = mkOption { - type = types.nullOr (types.strMatching "^[a-z0-9._-]+$"); - default = null; - description = lib.mdDoc "A lower-case string identifying a specific variant or edition of the operating system"; - example = "installer"; + variant_id = mkOption { + type = types.nullOr (types.strMatching "^[a-z0-9._-]+$"); + default = null; + description = lib.mdDoc "A lower-case string identifying a specific variant or edition of the operating system"; + example = "installer"; + }; }; image = { diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 23a761041bf48..627427262da63 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -133,6 +133,7 @@ ./misc/meta.nix ./misc/nixops-autoluks.nix ./misc/nixpkgs.nix + ./misc/nixpkgs-flake.nix ./misc/passthru.nix ./misc/version.nix ./misc/wordlist.nix @@ -241,6 +242,7 @@ ./programs/proxychains.nix ./programs/qdmr.nix ./programs/qt5ct.nix + ./programs/quark-goldleaf.nix ./programs/regreet.nix ./programs/rog-control-center.nix ./programs/rust-motd.nix @@ -317,7 +319,6 @@ ./security/oath.nix ./security/pam.nix ./security/pam_mount.nix - ./security/pam_usb.nix ./security/please.nix ./security/polkit.nix ./security/rngd.nix @@ -411,7 +412,6 @@ ./services/continuous-integration/buildbot/worker.nix ./services/continuous-integration/buildkite-agents.nix ./services/continuous-integration/gitea-actions-runner.nix - ./services/continuous-integration/github-runner.nix ./services/continuous-integration/github-runners.nix ./services/continuous-integration/gitlab-runner.nix ./services/continuous-integration/gocd-agent/default.nix @@ -512,6 +512,7 @@ ./services/editors/infinoted.nix ./services/finance/odoo.nix ./services/games/archisteamfarm.nix + ./services/games/armagetronad.nix ./services/games/crossfire-server.nix ./services/games/deliantra-server.nix ./services/games/factorio.nix @@ -547,6 +548,8 @@ ./services/hardware/kanata.nix ./services/hardware/lcd.nix ./services/hardware/lirc.nix + ./services/hardware/nvidia-container-toolkit-cdi-generator + ./services/hardware/monado.nix ./services/hardware/nvidia-optimus.nix ./services/hardware/openrgb.nix ./services/hardware/pcscd.nix @@ -579,8 +582,10 @@ ./services/home-automation/ebusd.nix ./services/home-automation/esphome.nix ./services/home-automation/evcc.nix + ./services/home-automation/govee2mqtt.nix ./services/home-automation/home-assistant.nix ./services/home-automation/homeassistant-satellite.nix + ./services/home-automation/matter-server.nix ./services/home-automation/zigbee2mqtt.nix ./services/home-automation/zwave-js.nix ./services/logging/SystemdJournal2Gelf.nix @@ -782,6 +787,7 @@ ./services/misc/tiddlywiki.nix ./services/misc/tp-auto-kbbl.nix ./services/misc/tuxclocker.nix + ./services/misc/transfer-sh.nix ./services/misc/tzupdate.nix ./services/misc/uhub.nix ./services/misc/weechat.nix @@ -840,6 +846,7 @@ ./services/monitoring/riemann.nix ./services/monitoring/rustdesk-server.nix ./services/monitoring/scollector.nix + ./services/monitoring/scrutiny.nix ./services/monitoring/smartd.nix ./services/monitoring/snmpd.nix ./services/monitoring/statsd.nix @@ -898,7 +905,6 @@ ./services/networking/autossh.nix ./services/networking/avahi-daemon.nix ./services/networking/babeld.nix - ./services/networking/bee-clef.nix ./services/networking/bee.nix ./services/networking/biboumi.nix ./services/networking/bind.nix @@ -963,6 +969,7 @@ ./services/networking/gns3-server.nix ./services/networking/gnunet.nix ./services/networking/go-autoconfig.nix + ./services/networking/go-camo.nix ./services/networking/go-neb.nix ./services/networking/go-shadowsocks2.nix ./services/networking/gobgpd.nix @@ -1320,6 +1327,7 @@ ./services/web-apps/mastodon.nix ./services/web-apps/matomo.nix ./services/web-apps/mattermost.nix + ./services/web-apps/mealie.nix ./services/web-apps/mediawiki.nix ./services/web-apps/meme-bingo-web.nix ./services/web-apps/microbin.nix @@ -1348,7 +1356,6 @@ ./services/web-apps/powerdns-admin.nix ./services/web-apps/pretalx.nix ./services/web-apps/prosody-filer.nix - ./services/web-apps/restya-board.nix ./services/web-apps/rimgo.nix ./services/web-apps/sftpgo.nix ./services/web-apps/suwayomi-server.nix @@ -1402,7 +1409,6 @@ ./services/web-servers/unit/default.nix ./services/web-servers/uwsgi.nix ./services/web-servers/varnish/default.nix - ./services/web-servers/zope2.nix ./services/x11/clight.nix ./services/x11/colord.nix ./services/x11/desktop-managers/default.nix @@ -1527,6 +1533,7 @@ ./tasks/filesystems/jfs.nix ./tasks/filesystems/nfs.nix ./tasks/filesystems/ntfs.nix + ./tasks/filesystems/overlayfs.nix ./tasks/filesystems/reiserfs.nix ./tasks/filesystems/sshfs.nix ./tasks/filesystems/squashfs.nix diff --git a/nixos/modules/programs/ccache.nix b/nixos/modules/programs/ccache.nix index 567c853e8c7de..7972b2ac4a569 100644 --- a/nixos/modules/programs/ccache.nix +++ b/nixos/modules/programs/ccache.nix @@ -1,35 +1,43 @@ { config, pkgs, lib, ... }: -with lib; let cfg = config.programs.ccache; in { options.programs.ccache = { # host configuration - enable = mkEnableOption (lib.mdDoc "CCache"); - cacheDir = mkOption { - type = types.path; + enable = lib.mkEnableOption (lib.mdDoc "CCache"); + cacheDir = lib.mkOption { + type = lib.types.path; description = lib.mdDoc "CCache directory"; default = "/var/cache/ccache"; }; # target configuration - packageNames = mkOption { - type = types.listOf types.str; + packageNames = lib.mkOption { + type = lib.types.listOf lib.types.str; description = lib.mdDoc "Nix top-level packages to be compiled using CCache"; default = []; example = [ "wxGTK32" "ffmpeg" "libav_all" ]; }; + owner = lib.mkOption { + type = lib.types.str; + default = "root"; + description = lib.mdDoc "Owner of CCache directory"; + }; + group = lib.mkOption { + type = lib.types.str; + default = "nixbld"; + description = lib.mdDoc "Group owner of CCache directory"; + }; }; - config = mkMerge [ + config = lib.mkMerge [ # host configuration - (mkIf cfg.enable { - systemd.tmpfiles.rules = [ "d ${cfg.cacheDir} 0770 root nixbld -" ]; + (lib.mkIf cfg.enable { + systemd.tmpfiles.rules = [ "d ${cfg.cacheDir} 0770 ${cfg.owner} ${cfg.group} -" ]; # "nix-ccache --show-stats" and "nix-ccache --clear" security.wrappers.nix-ccache = { - owner = "root"; - group = "nixbld"; + inherit (cfg) owner group; setuid = false; setgid = true; source = pkgs.writeScript "nix-ccache.pl" '' @@ -50,9 +58,9 @@ in { }) # target configuration - (mkIf (cfg.packageNames != []) { + (lib.mkIf (cfg.packageNames != []) { nixpkgs.overlays = [ - (self: super: genAttrs cfg.packageNames (pn: super.${pn}.override { stdenv = builtins.trace "with ccache: ${pn}" self.ccacheStdenv; })) + (self: super: lib.genAttrs cfg.packageNames (pn: super.${pn}.override { stdenv = builtins.trace "with ccache: ${pn}" self.ccacheStdenv; })) (self: super: { ccacheWrapper = super.ccacheWrapper.override { @@ -65,7 +73,7 @@ in { echo "Directory '$CCACHE_DIR' does not exist" echo "Please create it with:" echo " sudo mkdir -m0770 '$CCACHE_DIR'" - echo " sudo chown root:nixbld '$CCACHE_DIR'" + echo " sudo chown ${cfg.owner}:${cfg.group} '$CCACHE_DIR'" echo "=====" exit 1 fi diff --git a/nixos/modules/programs/chromium.nix b/nixos/modules/programs/chromium.nix index 4024f337dfcdf..45a9e9e2a6895 100644 --- a/nixos/modules/programs/chromium.nix +++ b/nixos/modules/programs/chromium.nix @@ -1,4 +1,4 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: with lib; @@ -21,8 +21,12 @@ in programs.chromium = { enable = mkEnableOption (lib.mdDoc "{command}`chromium` policies"); + enablePlasmaBrowserIntegration = mkEnableOption (lib.mdDoc "Native Messaging Host for Plasma Browser Integration"); + + plasmaBrowserIntegrationPackage = mkPackageOption pkgs [ "plasma5Packages" "plasma-browser-integration" ] { }; + extensions = mkOption { - type = types.listOf types.str; + type = with types; nullOr (listOf str); description = lib.mdDoc '' List of chromium extensions to install. For list of plugins ids see id in url of extensions on @@ -33,7 +37,7 @@ in [ExtensionInstallForcelist](https://cloud.google.com/docs/chrome-enterprise/policies/?policy=ExtensionInstallForcelist) for additional details. ''; - default = []; + default = null; example = literalExpression '' [ "chlffgpmiacpedhhbkiomidkjlcfhogd" # pushbullet @@ -62,16 +66,14 @@ in type = types.nullOr types.str; description = lib.mdDoc "Chromium default search provider url."; default = null; - example = - "https://encrypted.google.com/search?q={searchTerms}&{google:RLZ}{google:originalQueryForSuggestion}{google:assistedQueryStats}{google:searchFieldtrialParameter}{google:searchClient}{google:sourceId}{google:instantExtendedEnabledParameter}ie={inputEncoding}"; + example = "https://encrypted.google.com/search?q={searchTerms}&{google:RLZ}{google:originalQueryForSuggestion}{google:assistedQueryStats}{google:searchFieldtrialParameter}{google:searchClient}{google:sourceId}{google:instantExtendedEnabledParameter}ie={inputEncoding}"; }; defaultSearchProviderSuggestURL = mkOption { type = types.nullOr types.str; description = lib.mdDoc "Chromium default search provider url for suggestions."; default = null; - example = - "https://encrypted.google.com/complete/search?output=chrome&q={searchTerms}"; + example = "https://encrypted.google.com/complete/search?output=chrome&q={searchTerms}"; }; extraOpts = mkOption { @@ -90,9 +92,9 @@ in "PasswordManagerEnabled" = false; "SpellcheckEnabled" = true; "SpellcheckLanguage" = [ - "de" - "en-US" - ]; + "de" + "en-US" + ]; } ''; }; @@ -101,15 +103,21 @@ in ###### implementation - config = lib.mkIf cfg.enable { - # for chromium - environment.etc."chromium/policies/managed/default.json".text = builtins.toJSON defaultProfile; - environment.etc."chromium/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts; - # for google-chrome https://www.chromium.org/administrators/linux-quick-start - environment.etc."opt/chrome/policies/managed/default.json".text = builtins.toJSON defaultProfile; - environment.etc."opt/chrome/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts; - # for brave - environment.etc."brave/policies/managed/default.json".text = builtins.toJSON defaultProfile; - environment.etc."brave/policies/managed/extra.json".text = builtins.toJSON cfg.extraOpts; + config = { + environment.etc = lib.mkIf cfg.enable { + # for chromium + "chromium/native-messaging-hosts/org.kde.plasma.browser_integration.json" = lib.mkIf cfg.enablePlasmaBrowserIntegration + { source = "${cfg.plasmaBrowserIntegrationPackage}/etc/chromium/native-messaging-hosts/org.kde.plasma.browser_integration.json"; }; + "chromium/policies/managed/default.json" = lib.mkIf (defaultProfile != {}) { text = builtins.toJSON defaultProfile; }; + "chromium/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != {}) { text = builtins.toJSON cfg.extraOpts; }; + # for google-chrome https://www.chromium.org/administrators/linux-quick-start + "opt/chrome/native-messaging-hosts/org.kde.plasma.browser_integration.json" = lib.mkIf cfg.enablePlasmaBrowserIntegration + { source = "${cfg.plasmaBrowserIntegrationPackage}/etc/opt/chrome/native-messaging-hosts/org.kde.plasma.browser_integration.json"; }; + "opt/chrome/policies/managed/default.json" = lib.mkIf (defaultProfile != {}) { text = builtins.toJSON defaultProfile; }; + "opt/chrome/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != {}) { text = builtins.toJSON cfg.extraOpts; }; + # for brave + "brave/policies/managed/default.json" = lib.mkIf (defaultProfile != {}) { text = builtins.toJSON defaultProfile; }; + "brave/policies/managed/extra.json" = lib.mkIf (cfg.extraOpts != {}) { text = builtins.toJSON cfg.extraOpts; }; + }; }; } diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix index 8f82de0336667..179d2de87cc58 100644 --- a/nixos/modules/programs/gnupg.nix +++ b/nixos/modules/programs/gnupg.nix @@ -15,6 +15,7 @@ let defaultPinentryFlavor = if xserverCfg.desktopManager.lxqt.enable || xserverCfg.desktopManager.plasma5.enable + || xserverCfg.desktopManager.plasma6.enable || xserverCfg.desktopManager.deepin.enable then "qt" else if xserverCfg.desktopManager.xfce.enable then diff --git a/nixos/modules/programs/quark-goldleaf.nix b/nixos/modules/programs/quark-goldleaf.nix new file mode 100644 index 0000000000000..71aadc8c594e1 --- /dev/null +++ b/nixos/modules/programs/quark-goldleaf.nix @@ -0,0 +1,18 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.programs.quark-goldleaf; +in +{ + options = { + programs.quark-goldleaf = { + enable = lib.mkEnableOption "quark-goldleaf with udev rules applied"; + }; + }; + + config = lib.mkIf cfg.enable { + environment.systemPackages = [ pkgs.quark-goldleaf ]; + services.udev.packages = [ pkgs.quark-goldleaf ]; + }; + + meta.maintainers = pkgs.quark-goldleaf.meta.maintainers; +} diff --git a/nixos/modules/programs/steam.nix b/nixos/modules/programs/steam.nix index 29c449c16946c..c7f1e622f7baf 100644 --- a/nixos/modules/programs/steam.nix +++ b/nixos/modules/programs/steam.nix @@ -82,6 +82,14 @@ in { ''; }; + localNetworkGameTransfers.openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Open ports in the firewall for Steam Local Network Game Transfers. + ''; + }; + gamescopeSession = mkOption { description = mdDoc "Run a GameScope driven Steam session from your display-manager"; default = {}; @@ -139,15 +147,23 @@ in { ] ++ lib.optional cfg.gamescopeSession.enable steam-gamescope; networking.firewall = lib.mkMerge [ + (mkIf (cfg.remotePlay.openFirewall || cfg.localNetworkGameTransfers.openFirewall) { + allowedUDPPorts = [ 27036 ]; # Peer discovery + }) + (mkIf cfg.remotePlay.openFirewall { allowedTCPPorts = [ 27036 ]; - allowedUDPPortRanges = [ { from = 27031; to = 27036; } ]; + allowedUDPPortRanges = [ { from = 27031; to = 27035; } ]; }) (mkIf cfg.dedicatedServer.openFirewall { allowedTCPPorts = [ 27015 ]; # SRCDS Rcon port allowedUDPPorts = [ 27015 ]; # Gameplay traffic }) + + (mkIf cfg.localNetworkGameTransfers.openFirewall { + allowedTCPPorts = [ 27040 ]; # Data transfers + }) ]; }; diff --git a/nixos/modules/programs/wayland/sway.nix b/nixos/modules/programs/wayland/sway.nix index 57ee629b28810..ca2503ae5da77 100644 --- a/nixos/modules/programs/wayland/sway.nix +++ b/nixos/modules/programs/wayland/sway.nix @@ -119,10 +119,10 @@ in { extraPackages = mkOption { type = with types; listOf package; default = with pkgs; [ - swaylock swayidle foot dmenu + swaylock swayidle foot dmenu wmenu ]; defaultText = literalExpression '' - with pkgs; [ swaylock swayidle foot dmenu ]; + with pkgs; [ swaylock swayidle foot dmenu wmenu ]; ''; example = literalExpression '' with pkgs; [ diff --git a/nixos/modules/programs/yazi.nix b/nixos/modules/programs/yazi.nix index 273a7eeed05fd..338eddb60d80b 100644 --- a/nixos/modules/programs/yazi.nix +++ b/nixos/modules/programs/yazi.nix @@ -22,7 +22,7 @@ in description = lib.mdDoc '' Configuration included in `${name}.toml`. - See https://github.com/sxyazi/yazi/blob/v${cfg.package.version}/config/docs/${name}.md for documentation. + See https://yazi-rs.github.io/docs/configuration/${name}/ for documentation. ''; })) names); @@ -47,7 +47,5 @@ in }; meta = { maintainers = with lib.maintainers; [ linsui ]; - # The version of the package is used in the doc. - buildDocsInSandbox = false; }; } diff --git a/nixos/modules/rename.nix b/nixos/modules/rename.nix index 3fab863adb7f6..0a975fcd98c8c 100644 --- a/nixos/modules/rename.nix +++ b/nixos/modules/rename.nix @@ -112,6 +112,7 @@ in (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 [ "services" "prayer" ] "The corresponding package was removed from nixpkgs.") + (mkRemovedOptionModule [ "services" "restya-board" ] "The corresponding package was removed from nixpkgs.") (mkRemovedOptionModule [ "i18n" "inputMethod" "fcitx" ] "The fcitx module has been removed. Please use fcitx5 instead") (mkRemovedOptionModule [ "services" "dhcpd4" ] '' diff --git a/nixos/modules/security/ca.nix b/nixos/modules/security/ca.nix index 3cd56bff04d18..ae188ea709dd5 100644 --- a/nixos/modules/security/ca.nix +++ b/nixos/modules/security/ca.nix @@ -11,7 +11,8 @@ let extraCertificateFiles = cfg.certificateFiles; extraCertificateStrings = cfg.certificates; }; - caBundle = "${cacertPackage}/etc/ssl/certs/ca-bundle.crt"; + caBundleName = if cfg.useCompatibleBundle then "ca-no-trust-rules-bundle.crt" else "ca-bundle.crt"; + caBundle = "${cacertPackage}/etc/ssl/certs/${caBundleName}"; in @@ -23,6 +24,17 @@ in internal = true; }; + security.pki.useCompatibleBundle = mkEnableOption ''usage of a compatibility bundle. + + Such a bundle consist exclusively of `BEGIN CERTIFICATE` and no `BEGIN TRUSTED CERTIFICATE`, + which is a OpenSSL specific PEM format. + + It is known to be incompatible with certain software stacks. + + Nevertheless, enabling this will strip all additional trust rules provided by the + certificates themselves, this can have security consequences depending on your usecases. + ''; + security.pki.certificateFiles = mkOption { type = types.listOf types.path; default = []; diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index f809848fd4289..560e5eff5c39a 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -96,6 +96,10 @@ let pamOpts = { config, name, ... }: let cfg = config; in let config = parentConfig; in { + imports = [ + (lib.mkRenamedOptionModule [ "enableKwallet" ] [ "kwallet" "enable" ]) + ]; + options = { name = mkOption { @@ -205,17 +209,6 @@ let }; }; - usbAuth = mkOption { - default = config.security.pam.usb.enable; - defaultText = literalExpression "config.security.pam.usb.enable"; - type = types.bool; - description = lib.mdDoc '' - If set, users listed in - {file}`/etc/pamusb.conf` are able to log in - with the associated USB key. - ''; - }; - otpwAuth = mkOption { default = config.security.pam.enableOTPW; defaultText = literalExpression "config.security.pam.enableOTPW"; @@ -473,16 +466,23 @@ let ''; }; - enableKwallet = mkOption { - default = false; - type = types.bool; - description = lib.mdDoc '' - If enabled, pam_wallet will attempt to automatically unlock the - user's default KDE wallet upon login. If the user has no wallet named - "kdewallet", or the login password does not match their wallet - password, KDE will prompt separately after login. - ''; + kwallet = { + enable = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc '' + If enabled, pam_wallet will attempt to automatically unlock the + user's default KDE wallet upon login. If the user has no wallet named + "kdewallet", or the login password does not match their wallet + password, KDE will prompt separately after login. + ''; + }; + + package = mkPackageOption pkgs.plasma5Packages "kwallet-pam" { + pkgsText = "pkgs.plasma5Packages"; + }; }; + sssdStrictAccess = mkOption { default = false; type = types.bool; @@ -665,7 +665,6 @@ let authfile = u2f.authFile; appid = u2f.appId; }; }) - { name = "usb"; enable = cfg.usbAuth; control = "sufficient"; modulePath = "${pkgs.pam_usb}/lib/security/pam_usb.so"; } (let ussh = config.security.pam.ussh; in { name = "ussh"; enable = config.security.pam.ussh.enable && cfg.usshAuth; control = ussh.control; modulePath = "${pkgs.pam_ussh}/lib/security/pam_ussh.so"; settings = { ca_file = ussh.caFile; authorized_principals = ussh.authorizedPrincipals; @@ -698,7 +697,7 @@ let (config.security.pam.enableEcryptfs || config.security.pam.enableFscrypt || cfg.pamMount - || cfg.enableKwallet + || cfg.kwallet.enable || cfg.enableGnomeKeyring || config.services.intune.enable || cfg.googleAuthenticator.enable @@ -723,9 +722,7 @@ let { name = "mount"; enable = cfg.pamMount; control = "optional"; modulePath = "${pkgs.pam_mount}/lib/security/pam_mount.so"; settings = { disable_interactive = true; }; } - { name = "kwallet5"; enable = cfg.enableKwallet; control = "optional"; modulePath = "${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so"; settings = { - kwalletd = "${pkgs.plasma5Packages.kwallet.bin}/bin/kwalletd5"; - }; } + { name = "kwallet"; enable = cfg.kwallet.enable; control = "optional"; modulePath = "${cfg.kwallet.package}/lib/security/pam_kwallet5.so"; } { name = "gnome_keyring"; enable = cfg.enableGnomeKeyring; control = "optional"; modulePath = "${pkgs.gnome.gnome-keyring}/lib/security/pam_gnome_keyring.so"; } { name = "intune"; enable = config.services.intune.enable; control = "optional"; modulePath = "${pkgs.intune-portal}/lib/security/pam_intune.so"; } { name = "gnupg"; enable = cfg.gnupg.enable; control = "optional"; modulePath = "${pkgs.pam_gnupg}/lib/security/pam_gnupg.so"; settings = { @@ -860,9 +857,7 @@ let order = "user,group,default"; debug = true; }; } - { name = "kwallet5"; enable = cfg.enableKwallet; control = "optional"; modulePath = "${pkgs.plasma5Packages.kwallet-pam}/lib/security/pam_kwallet5.so"; settings = { - kwalletd = "${pkgs.plasma5Packages.kwallet.bin}/bin/kwalletd5"; - }; } + { name = "kwallet"; enable = cfg.kwallet.enable; control = "optional"; modulePath = "${cfg.kwallet.package}/lib/security/pam_kwallet5.so"; } { name = "gnome_keyring"; enable = cfg.enableGnomeKeyring; control = "optional"; modulePath = "${pkgs.gnome.gnome-keyring}/lib/security/pam_gnome_keyring.so"; settings = { auto_start = true; }; } @@ -1470,9 +1465,9 @@ in ''; } { - assertion = config.security.pam.zfs.enable -> (config.boot.zfs.enabled || config.boot.zfs.enableUnstable); + assertion = config.security.pam.zfs.enable -> config.boot.zfs.enabled; message = '' - `security.pam.zfs.enable` requires enabling ZFS (`boot.zfs.enabled` or `boot.zfs.enableUnstable`). + `security.pam.zfs.enable` requires enabling ZFS (`boot.zfs.enabled`). ''; } { diff --git a/nixos/modules/security/pam_usb.nix b/nixos/modules/security/pam_usb.nix deleted file mode 100644 index 4275c26c6bdaa..0000000000000 --- a/nixos/modules/security/pam_usb.nix +++ /dev/null @@ -1,51 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - - cfg = config.security.pam.usb; - - anyUsbAuth = any (attrByPath ["usbAuth"] false) (attrValues config.security.pam.services); - -in - -{ - options = { - - security.pam.usb = { - enable = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Enable USB login for all login systems that support it. For - more information, visit <https://github.com/aluzzardi/pam_usb/wiki/Getting-Started#setting-up-devices-and-users>. - ''; - }; - - }; - - }; - - config = mkIf (cfg.enable || anyUsbAuth) { - - # Make sure pmount and pumount are setuid wrapped. - security.wrappers = { - pmount = - { setuid = true; - owner = "root"; - group = "root"; - source = "${pkgs.pmount.out}/bin/pmount"; - }; - pumount = - { setuid = true; - owner = "root"; - group = "root"; - source = "${pkgs.pmount.out}/bin/pumount"; - }; - }; - - environment.systemPackages = [ pkgs.pmount ]; - - }; -} diff --git a/nixos/modules/services/cluster/kubernetes/default.nix b/nixos/modules/services/cluster/kubernetes/default.nix index 3fb916c769715..a920b6cb12682 100644 --- a/nixos/modules/services/cluster/kubernetes/default.nix +++ b/nixos/modules/services/cluster/kubernetes/default.nix @@ -285,7 +285,7 @@ in { systemd.tmpfiles.rules = [ "d /opt/cni/bin 0755 root root -" "d /run/kubernetes 0755 kubernetes kubernetes -" - "d /var/lib/kubernetes 0755 kubernetes kubernetes -" + "d ${cfg.dataDir} 0755 kubernetes kubernetes -" ]; users.users.kubernetes = { @@ -294,6 +294,7 @@ in { group = "kubernetes"; home = cfg.dataDir; createHome = true; + homeMode = "755"; }; users.groups.kubernetes.gid = config.ids.gids.kubernetes; diff --git a/nixos/modules/services/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix index fd2dce7ee6a25..313dbe2340182 100644 --- a/nixos/modules/services/cluster/kubernetes/kubelet.nix +++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix @@ -33,6 +33,41 @@ let kubeconfig = top.lib.mkKubeConfig "kubelet" cfg.kubeconfig; + # Flag based settings are deprecated, use the `--config` flag with a + # `KubeletConfiguration` struct. + # https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/ + # + # NOTE: registerWithTaints requires a []core/v1.Taint, therefore requires + # additional work to be put in config format. + # + kubeletConfig = pkgs.writeText "kubelet-config" (builtins.toJSON ({ + apiVersion = "kubelet.config.k8s.io/v1beta1"; + kind = "KubeletConfiguration"; + address = cfg.address; + port = cfg.port; + authentication = { + x509 = lib.optionalAttrs (cfg.clientCaFile != null) { clientCAFile = cfg.clientCaFile; }; + webhook = { + enabled = true; + cacheTTL = "10s"; + }; + }; + authorization = { + mode = "Webhook"; + }; + cgroupDriver = "systemd"; + hairpinMode = "hairpin-veth"; + registerNode = cfg.registerNode; + containerRuntimeEndpoint = cfg.containerRuntimeEndpoint; + healthzPort = cfg.healthz.port; + healthzBindAddress = cfg.healthz.bind; + } // lib.optionalAttrs (cfg.tlsCertFile != null) { tlsCertFile = cfg.tlsCertFile; } + // lib.optionalAttrs (cfg.tlsKeyFile != null) { tlsPrivateKeyFile = cfg.tlsKeyFile; } + // lib.optionalAttrs (cfg.clusterDomain != "") { clusterDomain = cfg.clusterDomain; } + // lib.optionalAttrs (cfg.clusterDns != "") { clusterDNS = [ cfg.clusterDns ] ; } + // lib.optionalAttrs (cfg.featureGates != []) { featureGates = cfg.featureGates; } + )); + manifestPath = "kubernetes/manifests"; taintOptions = with lib.types; { name, ... }: { @@ -294,21 +329,7 @@ in Restart = "on-failure"; RestartSec = "1000ms"; ExecStart = ''${top.package}/bin/kubelet \ - --address=${cfg.address} \ - --authentication-token-webhook \ - --authentication-token-webhook-cache-ttl="10s" \ - --authorization-mode=Webhook \ - ${optionalString (cfg.clientCaFile != null) - "--client-ca-file=${cfg.clientCaFile}"} \ - ${optionalString (cfg.clusterDns != "") - "--cluster-dns=${cfg.clusterDns}"} \ - ${optionalString (cfg.clusterDomain != "") - "--cluster-domain=${cfg.clusterDomain}"} \ - ${optionalString (cfg.featureGates != []) - "--feature-gates=${concatMapStringsSep "," (feature: "${feature}=true") cfg.featureGates}"} \ - --hairpin-mode=hairpin-veth \ - --healthz-bind-address=${cfg.healthz.bind} \ - --healthz-port=${toString cfg.healthz.port} \ + --config=${kubeletConfig} \ --hostname-override=${cfg.hostname} \ --kubeconfig=${kubeconfig} \ ${optionalString (cfg.nodeIp != null) @@ -316,18 +337,10 @@ in --pod-infra-container-image=pause \ ${optionalString (cfg.manifests != {}) "--pod-manifest-path=/etc/${manifestPath}"} \ - --port=${toString cfg.port} \ - --register-node=${boolToString cfg.registerNode} \ ${optionalString (taints != "") "--register-with-taints=${taints}"} \ --root-dir=${top.dataDir} \ - ${optionalString (cfg.tlsCertFile != null) - "--tls-cert-file=${cfg.tlsCertFile}"} \ - ${optionalString (cfg.tlsKeyFile != null) - "--tls-private-key-file=${cfg.tlsKeyFile}"} \ ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \ - --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \ - --cgroup-driver=systemd \ ${cfg.extraOpts} ''; WorkingDirectory = top.dataDir; diff --git a/nixos/modules/services/cluster/kubernetes/pki.nix b/nixos/modules/services/cluster/kubernetes/pki.nix index 4b7a86c44a0cc..a4b5cb8eda865 100644 --- a/nixos/modules/services/cluster/kubernetes/pki.nix +++ b/nixos/modules/services/cluster/kubernetes/pki.nix @@ -174,7 +174,7 @@ in '') (optionalString cfg.genCfsslAPIToken '' if [ ! -f "${cfsslAPITokenPath}" ]; then - install -u cfssl -m 400 <(head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ') "${cfsslAPITokenPath}" + install -o cfssl -m 400 <(head -c ${toString (cfsslAPITokenLength / 2)} /dev/urandom | od -An -t x | tr -d ' ') "${cfsslAPITokenPath}" fi '')]); @@ -192,7 +192,8 @@ in mkdir -p "$(dirname "${certmgrAPITokenPath}")" if [ -f "${cfsslAPITokenPath}" ]; then ln -fs "${cfsslAPITokenPath}" "${certmgrAPITokenPath}" - else + elif [ ! -f "${certmgrAPITokenPath}" ]; then + # Don't remove the token if it already exists install -m 600 /dev/null "${certmgrAPITokenPath}" fi '' @@ -219,7 +220,6 @@ in inherit (cert) action; authority = { inherit remote; - file.path = cert.caCert; root_ca = cert.caCert; profile = "default"; auth_key_file = certmgrAPITokenPath; diff --git a/nixos/modules/services/continuous-integration/github-runner.nix b/nixos/modules/services/continuous-integration/github-runner.nix deleted file mode 100644 index 27cfee92c75a3..0000000000000 --- a/nixos/modules/services/continuous-integration/github-runner.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ config -, pkgs -, lib -, ... -}@args: - -with lib; - -let - cfg = config.services.github-runner; -in - -{ - options.services.github-runner = import ./github-runner/options.nix (args // { - # Users don't need to specify options.services.github-runner.name; it will default - # to the hostname. - includeNameDefault = true; - }); - - config = mkIf cfg.enable { - services.github-runners.${cfg.name} = cfg; - }; - - meta.maintainers = with maintainers; [ veehaitch newam thomasjm ]; -} diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix index b9b1ea05e9672..193261fc2a9f8 100644 --- a/nixos/modules/services/continuous-integration/github-runner/options.nix +++ b/nixos/modules/services/continuous-integration/github-runner/options.nix @@ -1,213 +1,266 @@ -{ config -, lib +{ lib , pkgs -, includeNameDefault , ... }: with lib; - { - enable = mkOption { - default = false; - example = true; - description = lib.mdDoc '' - Whether to enable GitHub Actions runner. - - Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here: - [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners). - ''; - type = lib.types.bool; - }; - - url = mkOption { - type = types.str; - description = lib.mdDoc '' - Repository to add the runner to. - - Changing this option triggers a new runner registration. - - IMPORTANT: If your token is org-wide (not per repository), you need to - provide a github org link, not a single repository, so do it like this - `https://github.com/nixos`, not like this - `https://github.com/nixos/nixpkgs`. - Otherwise, you are going to get a `404 NotFound` - from `POST https://api.github.com/actions/runner-registration` - in the configure script. - ''; - example = "https://github.com/nixos/nixpkgs"; - }; - - tokenFile = mkOption { - type = types.path; - description = lib.mdDoc '' - The full path to a file which contains either - - * a fine-grained personal access token (PAT), - * a classic PAT - * or a runner registration token - - Changing this option or the `tokenFile`’s content triggers a new runner registration. - - We suggest using the fine-grained PATs. A runner registration token is valid - only for 1 hour after creation, so the next time the runner configuration changes - this will give you hard-to-debug HTTP 404 errors in the configure step. - - The file should contain exactly one line with the token without any newline. - (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.) - - If the file contains a PAT, the service creates a new registration token - on startup as needed. - If a registration token is given, it can be used to re-register a runner of the same - name but is time-limited as noted above. - - For fine-grained PATs: - - Give it "Read and Write access to organization/repository self hosted runners", - depending on whether it is organization wide or per-repository. You might have to - experiment a little, fine-grained PATs are a `beta` Github feature and still subject - to change; nonetheless they are the best option at the moment. - - For classic PATs: - - Make sure the PAT has a scope of `admin:org` for organization-wide registrations - or a scope of `repo` for a single repository. - - For runner registration tokens: - - Nothing special needs to be done, but updating will break after one hour, - so these are not recommended. - ''; - example = "/run/secrets/github-runner/nixos.token"; - }; - - name = let - # Same pattern as for `networking.hostName` - baseType = types.strMatching "^$|^[[:alnum:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$"; - in mkOption { - type = if includeNameDefault then baseType else types.nullOr baseType; - description = lib.mdDoc '' - Name of the runner to configure. Defaults to the hostname. - - Changing this option triggers a new runner registration. - ''; - example = "nixos"; - } // (if includeNameDefault then { - default = config.networking.hostName; - defaultText = literalExpression "config.networking.hostName"; - } else { - default = null; - }); - - runnerGroup = mkOption { - type = types.nullOr types.str; - description = lib.mdDoc '' - Name of the runner group to add this runner to (defaults to the default runner group). - - Changing this option triggers a new runner registration. - ''; - default = null; - }; - - extraLabels = mkOption { - type = types.listOf types.str; - description = lib.mdDoc '' - Extra labels in addition to the default (`["self-hosted", "Linux", "X64"]`). - - Changing this option triggers a new runner registration. - ''; - example = literalExpression ''[ "nixos" ]''; - default = [ ]; - }; - - replace = mkOption { - type = types.bool; - description = lib.mdDoc '' - Replace any existing runner with the same name. - - Without this flag, registering a new runner with the same name fails. - ''; - default = false; - }; - - extraPackages = mkOption { - type = types.listOf types.package; - description = lib.mdDoc '' - Extra packages to add to `PATH` of the service to make them available to workflows. - ''; - default = [ ]; - }; - - extraEnvironment = mkOption { - type = types.attrs; - description = lib.mdDoc '' - Extra environment variables to set for the runner, as an attrset. - ''; - example = { - GIT_CONFIG = "/path/to/git/config"; - }; - default = {}; - }; - - serviceOverrides = mkOption { - type = types.attrs; - description = lib.mdDoc '' - Modify the systemd service. Can be used to, e.g., adjust the sandboxing options. - See {manpage}`systemd.exec(5)` for more options. + options.services.github-runners = mkOption { + description = mdDoc '' + Multiple GitHub Runners. ''; example = { - ProtectHome = false; - RestrictAddressFamilies = [ "AF_PACKET" ]; + runner1 = { + enable = true; + url = "https://github.com/owner/repo"; + name = "runner1"; + tokenFile = "/secrets/token1"; + }; + + runner2 = { + enable = true; + url = "https://github.com/owner/repo"; + name = "runner2"; + tokenFile = "/secrets/token2"; + }; }; - default = {}; - }; - - package = mkPackageOption pkgs "github-runner" { }; - - ephemeral = mkOption { - type = types.bool; - description = lib.mdDoc '' - If enabled, causes the following behavior: - - - Passes the `--ephemeral` flag to the runner configuration script - - De-registers and stops the runner with GitHub after it has processed one job - - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option) - - Restarts the service after its successful exit - - On start, wipes the state directory and configures a new runner - - You should only enable this option if `tokenFile` points to a file which contains a - personal access token (PAT). If you're using the option with a registration token, restarting the - service will fail as soon as the registration token expired. - ''; - default = false; - }; - - user = mkOption { - type = types.nullOr types.str; - description = lib.mdDoc '' - User under which to run the service. If null, will use a systemd dynamic user. - ''; - default = null; - defaultText = literalExpression "username"; - }; - - workDir = mkOption { - type = with types; nullOr str; - description = lib.mdDoc '' - Working directory, available as `$GITHUB_WORKSPACE` during workflow runs - and used as a default for [repository checkouts](https://github.com/actions/checkout). - The service cleans this directory on every service start. - - A value of `null` will default to the systemd `RuntimeDirectory`. - ''; - default = null; - }; - - nodeRuntimes = mkOption { - type = with types; nonEmptyListOf (enum [ "node16" "node20" ]); - default = [ "node20" ]; - description = mdDoc '' - List of Node.js runtimes the runner should support. - ''; + default = { }; + type = types.attrsOf (types.submodule ({ name, ... }: { + options = { + enable = mkOption { + default = false; + example = true; + description = mdDoc '' + Whether to enable GitHub Actions runner. + + Note: GitHub recommends using self-hosted runners with private repositories only. Learn more here: + [About self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners). + ''; + type = types.bool; + }; + + url = mkOption { + type = types.str; + description = mdDoc '' + Repository to add the runner to. + + Changing this option triggers a new runner registration. + + IMPORTANT: If your token is org-wide (not per repository), you need to + provide a github org link, not a single repository, so do it like this + `https://github.com/nixos`, not like this + `https://github.com/nixos/nixpkgs`. + Otherwise, you are going to get a `404 NotFound` + from `POST https://api.github.com/actions/runner-registration` + in the configure script. + ''; + example = "https://github.com/nixos/nixpkgs"; + }; + + tokenFile = mkOption { + type = types.path; + description = mdDoc '' + The full path to a file which contains either + + * a fine-grained personal access token (PAT), + * a classic PAT + * or a runner registration token + + Changing this option or the `tokenFile`’s content triggers a new runner registration. + + We suggest using the fine-grained PATs. A runner registration token is valid + only for 1 hour after creation, so the next time the runner configuration changes + this will give you hard-to-debug HTTP 404 errors in the configure step. + + The file should contain exactly one line with the token without any newline. + (Use `echo -n '…token…' > …token file…` to make sure no newlines sneak in.) + + If the file contains a PAT, the service creates a new registration token + on startup as needed. + If a registration token is given, it can be used to re-register a runner of the same + name but is time-limited as noted above. + + For fine-grained PATs: + + Give it "Read and Write access to organization/repository self hosted runners", + depending on whether it is organization wide or per-repository. You might have to + experiment a little, fine-grained PATs are a `beta` Github feature and still subject + to change; nonetheless they are the best option at the moment. + + For classic PATs: + + Make sure the PAT has a scope of `admin:org` for organization-wide registrations + or a scope of `repo` for a single repository. + + For runner registration tokens: + + Nothing special needs to be done, but updating will break after one hour, + so these are not recommended. + ''; + example = "/run/secrets/github-runner/nixos.token"; + }; + + name = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Name of the runner to configure. If null, defaults to the hostname. + + Changing this option triggers a new runner registration. + ''; + example = "nixos"; + default = name; + }; + + runnerGroup = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Name of the runner group to add this runner to (defaults to the default runner group). + + Changing this option triggers a new runner registration. + ''; + default = null; + }; + + extraLabels = mkOption { + type = types.listOf types.str; + description = mdDoc '' + Extra labels in addition to the default (unless disabled through the `noDefaultLabels` option). + + Changing this option triggers a new runner registration. + ''; + example = literalExpression ''[ "nixos" ]''; + default = [ ]; + }; + + noDefaultLabels = mkOption { + type = types.bool; + description = mdDoc '' + Disables adding the default labels. Also see the `extraLabels` option. + + Changing this option triggers a new runner registration. + ''; + default = false; + }; + + replace = mkOption { + type = types.bool; + description = mdDoc '' + Replace any existing runner with the same name. + + Without this flag, registering a new runner with the same name fails. + ''; + default = false; + }; + + extraPackages = mkOption { + type = types.listOf types.package; + description = mdDoc '' + Extra packages to add to `PATH` of the service to make them available to workflows. + ''; + default = [ ]; + }; + + extraEnvironment = mkOption { + type = types.attrs; + description = mdDoc '' + Extra environment variables to set for the runner, as an attrset. + ''; + example = { + GIT_CONFIG = "/path/to/git/config"; + }; + default = { }; + }; + + serviceOverrides = mkOption { + type = types.attrs; + description = mdDoc '' + Modify the systemd service. Can be used to, e.g., adjust the sandboxing options. + See {manpage}`systemd.exec(5)` for more options. + ''; + example = { + ProtectHome = false; + RestrictAddressFamilies = [ "AF_PACKET" ]; + }; + default = { }; + }; + + package = mkPackageOption pkgs "github-runner" { }; + + ephemeral = mkOption { + type = types.bool; + description = mdDoc '' + If enabled, causes the following behavior: + + - Passes the `--ephemeral` flag to the runner configuration script + - De-registers and stops the runner with GitHub after it has processed one job + - On stop, systemd wipes the runtime directory (this always happens, even without using the ephemeral option) + - Restarts the service after its successful exit + - On start, wipes the state directory and configures a new runner + + You should only enable this option if `tokenFile` points to a file which contains a + personal access token (PAT). If you're using the option with a registration token, restarting the + service will fail as soon as the registration token expired. + + Changing this option triggers a new runner registration. + ''; + default = false; + }; + + user = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + User under which to run the service. + + If this option and the `group` option is set to `null`, + the service runs as a dynamically allocated user. + + Also see the `group` option for an overview on the effects of the `user` and `group` settings. + ''; + default = null; + defaultText = literalExpression "username"; + }; + + group = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Group under which to run the service. + + The effect of this option depends on the value of the `user` option: + + - `group == null` and `user == null`: + The service runs with a dynamically allocated user and group. + - `group == null` and `user != null`: + The service runs as the given user and its default group. + - `group != null` and `user == null`: + This configuration is invalid. In this case, the service would use the given group + but run as root implicitly. If this is really what you want, set `user = "root"` explicitly. + ''; + default = null; + defaultText = literalExpression "groupname"; + }; + + workDir = mkOption { + type = with types; nullOr str; + description = mdDoc '' + Working directory, available as `$GITHUB_WORKSPACE` during workflow runs + and used as a default for [repository checkouts](https://github.com/actions/checkout). + The service cleans this directory on every service start. + + A value of `null` will default to the systemd `RuntimeDirectory`. + + Changing this option triggers a new runner registration. + ''; + default = null; + }; + + nodeRuntimes = mkOption { + type = with types; nonEmptyListOf (enum [ "node20" ]); + default = [ "node20" ]; + description = mdDoc '' + List of Node.js runtimes the runner should support. + ''; + }; + }; + })); }; } diff --git a/nixos/modules/services/continuous-integration/github-runner/service.nix b/nixos/modules/services/continuous-integration/github-runner/service.nix index 535df7f68e076..fccdcc116a21a 100644 --- a/nixos/modules/services/continuous-integration/github-runner/service.nix +++ b/nixos/modules/services/continuous-integration/github-runner/service.nix @@ -1,268 +1,299 @@ { config , lib , pkgs - -, cfg ? config.services.github-runner -, svcName - -, systemdDir ? "${svcName}/${cfg.name}" - # %t: Runtime directory root (usually /run); see systemd.unit(5) -, runtimeDir ? "%t/${systemdDir}" - # %S: State directory root (usually /var/lib); see systemd.unit(5) -, stateDir ? "%S/${systemdDir}" - # %L: Log directory root (usually /var/log); see systemd.unit(5) -, logsDir ? "%L/${systemdDir}" - # Name of file stored in service state directory -, currentConfigTokenFilename ? ".current-token" - , ... }: with lib; - -let - workDir = if cfg.workDir == null then runtimeDir else cfg.workDir; - package = cfg.package.override { inherit (cfg) nodeRuntimes; }; -in { - description = "GitHub Actions runner"; + config.assertions = flatten ( + flip mapAttrsToList config.services.github-runners (name: cfg: map (mkIf cfg.enable) [ + { + assertion = !cfg.noDefaultLabels || (cfg.extraLabels != [ ]); + message = "`services.github-runners.${name}`: The `extraLabels` option is mandatory if `noDefaultLabels` is set"; + } + { + assertion = cfg.group == null || cfg.user != null; + message = ''`services.github-runners.${name}`: Setting `group` while leaving `user` unset runs the service as `root`. If this is really what you want, set `user = "root"` explicitly''; + } + ]) + ); - wantedBy = [ "multi-user.target" ]; - wants = [ "network-online.target" ]; - after = [ "network.target" "network-online.target" ]; + config.systemd.services = flip mapAttrs' config.services.github-runners (name: cfg: + let + svcName = "github-runner-${name}"; + systemdDir = "github-runner/${name}"; - environment = { - HOME = workDir; - RUNNER_ROOT = stateDir; - } // cfg.extraEnvironment; + # %t: Runtime directory root (usually /run); see systemd.unit(5) + runtimeDir = "%t/${systemdDir}"; + # %S: State directory root (usually /var/lib); see systemd.unit(5) + stateDir = "%S/${systemdDir}"; + # %L: Log directory root (usually /var/log); see systemd.unit(5) + logsDir = "%L/${systemdDir}"; + # Name of file stored in service state directory + currentConfigTokenFilename = ".current-token"; - path = (with pkgs; [ - bash - coreutils - git - gnutar - gzip - ]) ++ [ - config.nix.package - ] ++ cfg.extraPackages; + workDir = if cfg.workDir == null then runtimeDir else cfg.workDir; + # Support old github-runner versions which don't have the `nodeRuntimes` arg yet. + package = cfg.package.override (old: optionalAttrs (hasAttr "nodeRuntimes" old) { inherit (cfg) nodeRuntimes; }); + in + nameValuePair svcName { + description = "GitHub Actions runner"; - serviceConfig = mkMerge [ - { - ExecStart = "${package}/bin/Runner.Listener run --startuptype service"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network.target" "network-online.target" ]; - # Does the following, sequentially: - # - If the module configuration or the token has changed, purge the state directory, - # and create the current and the new token file with the contents of the configured - # token. While both files have the same content, only the later is accessible by - # the service user. - # - Configure the runner using the new token file. When finished, delete it. - # - Set up the directory structure by creating the necessary symlinks. - ExecStartPre = - let - # Wrapper script which expects the full path of the state, working and logs - # directory as arguments. Overrides the respective systemd variables to provide - # unambiguous directory names. This becomes relevant, for example, if the - # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory= - # to contain more than one directory. This causes systemd to set the respective - # environment variables with the path of all of the given directories, separated - # by a colon. - writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" '' - set -euo pipefail + environment = { + HOME = workDir; + RUNNER_ROOT = stateDir; + } // cfg.extraEnvironment; - STATE_DIRECTORY="$1" - WORK_DIRECTORY="$2" - LOGS_DIRECTORY="$3" + path = (with pkgs; [ + bash + coreutils + git + gnutar + gzip + ]) ++ [ + config.nix.package + ] ++ cfg.extraPackages; - ${lines} - ''; - runnerRegistrationConfig = getAttrs [ "name" "tokenFile" "url" "runnerGroup" "extraLabels" "ephemeral" "workDir" ] cfg; - newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig); - currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json"; - newConfigTokenPath = "$STATE_DIRECTORY/.new-token"; - currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}"; + serviceConfig = mkMerge [ + { + ExecStart = "${package}/bin/Runner.Listener run --startuptype service"; - runnerCredFiles = [ - ".credentials" - ".credentials_rsaparams" - ".runner" - ]; - unconfigureRunner = writeScript "unconfigure" '' - copy_tokens() { - # Copy the configured token file to the state dir and allow the service user to read the file - install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}" - # Also copy current file to allow for a diff on the next start - install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}" - } - clean_state() { - find "$STATE_DIRECTORY/" -mindepth 1 -delete - copy_tokens - } - diff_config() { - changed=0 - # Check for module config changes - [[ -f "${currentConfigPath}" ]] \ - && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \ - || changed=1 - # Also check the content of the token file - [[ -f "${currentConfigTokenPath}" ]] \ - && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \ - || changed=1 - # If the config has changed, remove old state and copy tokens - if [[ "$changed" -eq 1 ]]; then - echo "Config has changed, removing old runner state." - echo "The old runner will still appear in the GitHub Actions UI." \ - "You have to remove it manually." - clean_state - fi - } - if [[ "${optionalString cfg.ephemeral "1"}" ]]; then - # In ephemeral mode, we always want to start with a clean state - clean_state - elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then - # There are state files from a previous run; diff them to decide if we need a new registration - diff_config - else - # The state directory is entirely empty which indicates a first start - copy_tokens - fi - # Always clean workDir - find -H "$WORK_DIRECTORY" -mindepth 1 -delete - ''; - configureRunner = writeScript "configure" '' - if [[ -e "${newConfigTokenPath}" ]]; then - echo "Configuring GitHub Actions Runner" - args=( - --unattended - --disableupdate - --work "$WORK_DIRECTORY" - --url ${escapeShellArg cfg.url} - --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} - --name ${escapeShellArg cfg.name} - ${optionalString cfg.replace "--replace"} - ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"} - ${optionalString cfg.ephemeral "--ephemeral"} - ) - # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option, - # if it is not a PAT, we assume it contains a registration token and use the --token option - token=$(<"${newConfigTokenPath}") - if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then - args+=(--pat "$token") - else - args+=(--token "$token") - fi - ${package}/bin/Runner.Listener configure "''${args[@]}" - # Move the automatically created _diag dir to the logs dir - mkdir -p "$STATE_DIRECTORY/_diag" - cp -r "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/" - rm -rf "$STATE_DIRECTORY/_diag/" - # Cleanup token from config - rm "${newConfigTokenPath}" - # Symlink to new config - ln -s '${newConfigPath}' "${currentConfigPath}" - fi - ''; - setupWorkDir = writeScript "setup-work-dirs" '' - # Link _diag dir - ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag" + # Does the following, sequentially: + # - If the module configuration or the token has changed, purge the state directory, + # and create the current and the new token file with the contents of the configured + # token. While both files have the same content, only the later is accessible by + # the service user. + # - Configure the runner using the new token file. When finished, delete it. + # - Set up the directory structure by creating the necessary symlinks. + ExecStartPre = + let + # Wrapper script which expects the full path of the state, working and logs + # directory as arguments. Overrides the respective systemd variables to provide + # unambiguous directory names. This becomes relevant, for example, if the + # caller overrides any of the StateDirectory=, RuntimeDirectory= or LogDirectory= + # to contain more than one directory. This causes systemd to set the respective + # environment variables with the path of all of the given directories, separated + # by a colon. + writeScript = name: lines: pkgs.writeShellScript "${svcName}-${name}.sh" '' + set -euo pipefail - # Link the runner credentials to the work dir - ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/" - ''; - in - map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [ - "+${unconfigureRunner}" # runs as root - configureRunner - setupWorkDir - ]; + STATE_DIRECTORY="$1" + WORK_DIRECTORY="$2" + LOGS_DIRECTORY="$3" - # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner) - # to trigger a fresh registration. - Restart = if cfg.ephemeral then "on-success" else "no"; - # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service: - # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146 - RestartForceExitStatus = [ 2 ]; + ${lines} + ''; + runnerRegistrationConfig = getAttrs [ + "ephemeral" + "extraLabels" + "name" + "noDefaultLabels" + "runnerGroup" + "tokenFile" + "url" + "workDir" + ] + cfg; + newConfigPath = builtins.toFile "${svcName}-config.json" (builtins.toJSON runnerRegistrationConfig); + currentConfigPath = "$STATE_DIRECTORY/.nixos-current-config.json"; + newConfigTokenPath = "$STATE_DIRECTORY/.new-token"; + currentConfigTokenPath = "$STATE_DIRECTORY/${currentConfigTokenFilename}"; - # Contains _diag - LogsDirectory = [ systemdDir ]; - # Default RUNNER_ROOT which contains ephemeral Runner data - RuntimeDirectory = [ systemdDir ]; - # Home of persistent runner data, e.g., credentials - StateDirectory = [ systemdDir ]; - StateDirectoryMode = "0700"; - WorkingDirectory = workDir; + runnerCredFiles = [ + ".credentials" + ".credentials_rsaparams" + ".runner" + ]; + unconfigureRunner = writeScript "unconfigure" '' + copy_tokens() { + # Copy the configured token file to the state dir and allow the service user to read the file + install --mode=666 ${escapeShellArg cfg.tokenFile} "${newConfigTokenPath}" + # Also copy current file to allow for a diff on the next start + install --mode=600 ${escapeShellArg cfg.tokenFile} "${currentConfigTokenPath}" + } + clean_state() { + find "$STATE_DIRECTORY/" -mindepth 1 -delete + copy_tokens + } + diff_config() { + changed=0 + # Check for module config changes + [[ -f "${currentConfigPath}" ]] \ + && ${pkgs.diffutils}/bin/diff -q '${newConfigPath}' "${currentConfigPath}" >/dev/null 2>&1 \ + || changed=1 + # Also check the content of the token file + [[ -f "${currentConfigTokenPath}" ]] \ + && ${pkgs.diffutils}/bin/diff -q "${currentConfigTokenPath}" ${escapeShellArg cfg.tokenFile} >/dev/null 2>&1 \ + || changed=1 + # If the config has changed, remove old state and copy tokens + if [[ "$changed" -eq 1 ]]; then + echo "Config has changed, removing old runner state." + echo "The old runner will still appear in the GitHub Actions UI." \ + "You have to remove it manually." + clean_state + fi + } + if [[ "${optionalString cfg.ephemeral "1"}" ]]; then + # In ephemeral mode, we always want to start with a clean state + clean_state + elif [[ "$(ls -A "$STATE_DIRECTORY")" ]]; then + # There are state files from a previous run; diff them to decide if we need a new registration + diff_config + else + # The state directory is entirely empty which indicates a first start + copy_tokens + fi + # Always clean workDir + find -H "$WORK_DIRECTORY" -mindepth 1 -delete + ''; + configureRunner = writeScript "configure" '' + if [[ -e "${newConfigTokenPath}" ]]; then + echo "Configuring GitHub Actions Runner" + args=( + --unattended + --disableupdate + --work "$WORK_DIRECTORY" + --url ${escapeShellArg cfg.url} + --labels ${escapeShellArg (concatStringsSep "," cfg.extraLabels)} + ${optionalString (cfg.name != null ) "--name ${escapeShellArg cfg.name}"} + ${optionalString cfg.replace "--replace"} + ${optionalString (cfg.runnerGroup != null) "--runnergroup ${escapeShellArg cfg.runnerGroup}"} + ${optionalString cfg.ephemeral "--ephemeral"} + ${optionalString cfg.noDefaultLabels "--no-default-labels"} + ) + # If the token file contains a PAT (i.e., it starts with "ghp_" or "github_pat_"), we have to use the --pat option, + # if it is not a PAT, we assume it contains a registration token and use the --token option + token=$(<"${newConfigTokenPath}") + if [[ "$token" =~ ^ghp_* ]] || [[ "$token" =~ ^github_pat_* ]]; then + args+=(--pat "$token") + else + args+=(--token "$token") + fi + ${package}/bin/Runner.Listener configure "''${args[@]}" + # Move the automatically created _diag dir to the logs dir + mkdir -p "$STATE_DIRECTORY/_diag" + cp -r "$STATE_DIRECTORY/_diag/." "$LOGS_DIRECTORY/" + rm -rf "$STATE_DIRECTORY/_diag/" + # Cleanup token from config + rm "${newConfigTokenPath}" + # Symlink to new config + ln -s '${newConfigPath}' "${currentConfigPath}" + fi + ''; + setupWorkDir = writeScript "setup-work-dirs" '' + # Link _diag dir + ln -s "$LOGS_DIRECTORY" "$WORK_DIRECTORY/_diag" - InaccessiblePaths = [ - # Token file path given in the configuration, if visible to the service - "-${cfg.tokenFile}" - # Token file in the state directory - "${stateDir}/${currentConfigTokenFilename}" - ]; + # Link the runner credentials to the work dir + ln -s "$STATE_DIRECTORY"/{${lib.concatStringsSep "," runnerCredFiles}} "$WORK_DIRECTORY/" + ''; + in + map (x: "${x} ${escapeShellArgs [ stateDir workDir logsDir ]}") [ + "+${unconfigureRunner}" # runs as root + configureRunner + setupWorkDir + ]; - KillSignal = "SIGINT"; + # If running in ephemeral mode, restart the service on-exit (i.e., successful de-registration of the runner) + # to trigger a fresh registration. + Restart = if cfg.ephemeral then "on-success" else "no"; + # If the runner exits with `ReturnCode.RetryableError = 2`, always restart the service: + # https://github.com/actions/runner/blob/40ed7f8/src/Runner.Common/Constants.cs#L146 + RestartForceExitStatus = [ 2 ]; - # Hardening (may overlap with DynamicUser=) - # The following options are only for optimizing: - # systemd-analyze security github-runner - AmbientCapabilities = mkBefore [ "" ]; - CapabilityBoundingSet = mkBefore [ "" ]; - # ProtectClock= adds DeviceAllow=char-rtc r - DeviceAllow = mkBefore [ "" ]; - NoNewPrivileges = mkDefault true; - PrivateDevices = mkDefault true; - PrivateMounts = mkDefault true; - PrivateTmp = mkDefault true; - PrivateUsers = mkDefault true; - ProtectClock = mkDefault true; - ProtectControlGroups = mkDefault true; - ProtectHome = mkDefault true; - ProtectHostname = mkDefault true; - ProtectKernelLogs = mkDefault true; - ProtectKernelModules = mkDefault true; - ProtectKernelTunables = mkDefault true; - ProtectSystem = mkDefault "strict"; - RemoveIPC = mkDefault true; - RestrictNamespaces = mkDefault true; - RestrictRealtime = mkDefault true; - RestrictSUIDSGID = mkDefault true; - UMask = mkDefault "0066"; - ProtectProc = mkDefault "invisible"; - SystemCallFilter = mkBefore [ - "~@clock" - "~@cpu-emulation" - "~@module" - "~@mount" - "~@obsolete" - "~@raw-io" - "~@reboot" - "~capset" - "~setdomainname" - "~sethostname" - ]; - RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ]; + # Contains _diag + LogsDirectory = [ systemdDir ]; + # Default RUNNER_ROOT which contains ephemeral Runner data + RuntimeDirectory = [ systemdDir ]; + # Home of persistent runner data, e.g., credentials + StateDirectory = [ systemdDir ]; + StateDirectoryMode = "0700"; + WorkingDirectory = workDir; - BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ]; + InaccessiblePaths = [ + # Token file path given in the configuration, if visible to the service + "-${cfg.tokenFile}" + # Token file in the state directory + "${stateDir}/${currentConfigTokenFilename}" + ]; + + KillSignal = "SIGINT"; + + # Hardening (may overlap with DynamicUser=) + # The following options are only for optimizing: + # systemd-analyze security github-runner + AmbientCapabilities = mkBefore [ "" ]; + CapabilityBoundingSet = mkBefore [ "" ]; + # ProtectClock= adds DeviceAllow=char-rtc r + DeviceAllow = mkBefore [ "" ]; + NoNewPrivileges = mkDefault true; + PrivateDevices = mkDefault true; + PrivateMounts = mkDefault true; + PrivateTmp = mkDefault true; + PrivateUsers = mkDefault true; + ProtectClock = mkDefault true; + ProtectControlGroups = mkDefault true; + ProtectHome = mkDefault true; + ProtectHostname = mkDefault true; + ProtectKernelLogs = mkDefault true; + ProtectKernelModules = mkDefault true; + ProtectKernelTunables = mkDefault true; + ProtectSystem = mkDefault "strict"; + RemoveIPC = mkDefault true; + RestrictNamespaces = mkDefault true; + RestrictRealtime = mkDefault true; + RestrictSUIDSGID = mkDefault true; + UMask = mkDefault "0066"; + ProtectProc = mkDefault "invisible"; + SystemCallFilter = mkBefore [ + "~@clock" + "~@cpu-emulation" + "~@module" + "~@mount" + "~@obsolete" + "~@raw-io" + "~@reboot" + "~capset" + "~setdomainname" + "~sethostname" + ]; + RestrictAddressFamilies = mkBefore [ "AF_INET" "AF_INET6" "AF_UNIX" "AF_NETLINK" ]; + + BindPaths = lib.optionals (cfg.workDir != null) [ cfg.workDir ]; - # Needs network access - PrivateNetwork = mkDefault false; - # Cannot be true due to Node - MemoryDenyWriteExecute = mkDefault false; + # Needs network access + PrivateNetwork = mkDefault false; + # Cannot be true due to Node + MemoryDenyWriteExecute = mkDefault false; - # The more restrictive "pid" option makes `nix` commands in CI emit - # "GC Warning: Couldn't read /proc/stat" - # You may want to set this to "pid" if not using `nix` commands - ProcSubset = mkDefault "all"; - # Coverage programs for compiled code such as `cargo-tarpaulin` disable - # ASLR (address space layout randomization) which requires the - # `personality` syscall - # You may want to set this to `true` if not using coverage tooling on - # compiled code - LockPersonality = mkDefault false; + # The more restrictive "pid" option makes `nix` commands in CI emit + # "GC Warning: Couldn't read /proc/stat" + # You may want to set this to "pid" if not using `nix` commands + ProcSubset = mkDefault "all"; + # Coverage programs for compiled code such as `cargo-tarpaulin` disable + # ASLR (address space layout randomization) which requires the + # `personality` syscall + # You may want to set this to `true` if not using coverage tooling on + # compiled code + LockPersonality = mkDefault false; - # Note that this has some interactions with the User setting; so you may - # want to consult the systemd docs if using both. - DynamicUser = mkDefault true; + DynamicUser = mkDefault true; + } + (mkIf (cfg.user != null) { + DynamicUser = false; + User = cfg.user; + }) + (mkIf (cfg.group != null) { + DynamicUser = false; + Group = cfg.group; + }) + cfg.serviceOverrides + ]; } - (mkIf (cfg.user != null) { User = cfg.user; }) - cfg.serviceOverrides - ]; + ); } diff --git a/nixos/modules/services/continuous-integration/github-runners.nix b/nixos/modules/services/continuous-integration/github-runners.nix index 66ace9580eca5..4a4608c2e4f89 100644 --- a/nixos/modules/services/continuous-integration/github-runners.nix +++ b/nixos/modules/services/continuous-integration/github-runners.nix @@ -1,58 +1,10 @@ -{ config -, pkgs -, lib -, ... -}@args: - -with lib; - -let - cfg = config.services.github-runners; - -in - +{ lib, ... }: { - options.services.github-runners = mkOption { - default = {}; - type = with types; attrsOf (submodule { options = import ./github-runner/options.nix (args // { - # services.github-runners.${name}.name doesn't have a default; it falls back to ${name} below. - includeNameDefault = false; - }); }); - example = { - runner1 = { - enable = true; - url = "https://github.com/owner/repo"; - name = "runner1"; - tokenFile = "/secrets/token1"; - }; - - runner2 = { - enable = true; - url = "https://github.com/owner/repo"; - name = "runner2"; - tokenFile = "/secrets/token2"; - }; - }; - description = lib.mdDoc '' - Multiple GitHub Runners. - ''; - }; - - config = { - systemd.services = flip mapAttrs' cfg (n: v: - let - svcName = "github-runner-${n}"; - in - nameValuePair svcName - (import ./github-runner/service.nix (args // { - inherit svcName; - cfg = v // { - name = if v.name != null then v.name else n; - }; - systemdDir = "github-runner/${n}"; - })) - ); - }; + imports = [ + (lib.mkRemovedOptionModule [ "services" "github-runner" ] "Use `services.github-runners.*` instead") + ./github-runner/options.nix + ./github-runner/service.nix + ]; - meta.maintainers = with maintainers; [ veehaitch newam ]; + meta.maintainers = with lib.maintainers; [ veehaitch newam ]; } diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix index 54bbe69703f95..b1d44e67658bd 100644 --- a/nixos/modules/services/continuous-integration/hydra/default.nix +++ b/nixos/modules/services/continuous-integration/hydra/default.nix @@ -39,7 +39,7 @@ let hydra-package = let - makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set \"${key}\" \"${value}\"") hydraEnv); + makeWrapperArgs = concatStringsSep " " (mapAttrsToList (key: value: "--set-default \"${key}\" \"${value}\"") hydraEnv); in pkgs.buildEnv rec { name = "hydra-env"; nativeBuildInputs = [ pkgs.makeWrapper ]; diff --git a/nixos/modules/services/databases/mysql.nix b/nixos/modules/services/databases/mysql.nix index 128bb0862175d..a6d71cca88de4 100644 --- a/nixos/modules/services/databases/mysql.nix +++ b/nixos/modules/services/databases/mysql.nix @@ -7,6 +7,9 @@ let cfg = config.services.mysql; isMariaDB = lib.getName cfg.package == lib.getName pkgs.mariadb; + isOracle = lib.getName cfg.package == lib.getName pkgs.mysql80; + # Oracle MySQL has supported "notify" service type since 8.0 + hasNotify = isMariaDB || (isOracle && versionAtLeast cfg.package.version "8.0"); mysqldOptions = "--user=${cfg.user} --datadir=${cfg.dataDir} --basedir=${cfg.package}"; @@ -377,19 +380,11 @@ in # The super user account to use on *first* run of MySQL server superUser = if isMariaDB then cfg.user else "root"; in '' - ${optionalString (!isMariaDB) '' + ${optionalString (!hasNotify) '' # Wait until the MySQL server is available for use - count=0 while [ ! -e /run/mysqld/mysqld.sock ] do - if [ $count -eq 30 ] - then - echo "Tried 30 times, giving up..." - exit 1 - fi - echo "MySQL daemon not yet started. Waiting for 1 second..." - count=$((count++)) sleep 1 done ''} @@ -477,7 +472,7 @@ in serviceConfig = mkMerge [ { - Type = if isMariaDB then "notify" else "simple"; + Type = if hasNotify then "notify" else "simple"; Restart = "on-abort"; RestartSec = "5s"; diff --git a/nixos/modules/services/databases/pgbouncer.nix b/nixos/modules/services/databases/pgbouncer.nix index 65b287e84442b..157d49c131617 100644 --- a/nixos/modules/services/databases/pgbouncer.nix +++ b/nixos/modules/services/databases/pgbouncer.nix @@ -66,9 +66,6 @@ let ${optionalString (cfg.adminUsers != null) "admin_users = ${cfg.adminUsers}"} ${optionalString (cfg.statsUsers != null) "stats_users = ${cfg.statsUsers}"} - # linux - pidfile = /run/pgbouncer/pgbouncer.pid - # extra ${cfg.extraConfig} ''; @@ -96,10 +93,9 @@ in { logFile = mkOption { type = types.nullOr types.str; - default = "pgbouncer.log"; + default = null; description = lib.mdDoc '' - Specifies the log file. - Either this or syslog has to be specified. + Specifies a log file in addition to journald. ''; }; @@ -601,22 +597,21 @@ in { systemd.services.pgbouncer = { description = "PgBouncer - PostgreSQL connection pooler"; - wants = [ "postgresql.service" ]; - after = [ "postgresql.service" ]; + wants = [ "network-online.target" ] ++ lib.optional config.services.postgresql.enable "postgresql.service"; + after = [ "network-online.target" ] ++ lib.optional config.services.postgresql.enable "postgresql.service"; wantedBy = [ "multi-user.target" ]; serviceConfig = { - Type = "forking"; + Type = "notify"; User = cfg.user; Group = cfg.group; - ExecStart = "${pkgs.pgbouncer}/bin/pgbouncer -d ${confFile}"; + ExecStart = "${lib.getExe pkgs.pgbouncer} ${confFile}"; ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID"; RuntimeDirectory = "pgbouncer"; - PIDFile = "/run/pgbouncer/pgbouncer.pid"; LimitNOFILE = cfg.openFilesLimit; }; }; - networking.firewall.allowedTCPPorts = optional cfg.openFirewall cfg.port; + networking.firewall.allowedTCPPorts = optional cfg.openFirewall cfg.listenPort; }; diff --git a/nixos/modules/services/desktops/pipewire/pipewire.nix b/nixos/modules/services/desktops/pipewire/pipewire.nix index da409030b3a35..09448833620c6 100644 --- a/nixos/modules/services/desktops/pipewire/pipewire.nix +++ b/nixos/modules/services/desktops/pipewire/pipewire.nix @@ -1,11 +1,15 @@ -# pipewire service. +# PipeWire service. { config, lib, pkgs, ... }: with lib; let json = pkgs.formats.json {}; - mapToFiles = location: config: concatMapAttrs (name: value: { "pipewire/${location}.conf.d/${name}.conf".source = json.generate "${name}" value;}) config; + mapToFiles = location: config: concatMapAttrs (name: value: { "share/pipewire/${location}.conf.d/${name}.conf" = json.generate "${name}" value; }) config; + extraConfigPkgFromFiles = locations: filesSet: pkgs.runCommand "pipewire-extra-config" { } '' + mkdir -p ${lib.concatMapStringsSep " " (l: "$out/share/pipewire/${l}.conf.d") locations} + ${lib.concatMapStringsSep ";" ({name, value}: "ln -s ${value} $out/${name}") (lib.attrsToList filesSet)} + ''; cfg = config.services.pipewire; enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 @@ -19,13 +23,48 @@ let mkdir -p "$out/lib" ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire" ''; + + configPackages = cfg.configPackages; + + extraConfigPkg = extraConfigPkgFromFiles + [ "pipewire" "client" "client-rt" "jack" "pipewire-pulse" ] + ( + mapToFiles "pipewire" cfg.extraConfig.pipewire + // mapToFiles "client" cfg.extraConfig.client + // mapToFiles "client-rt" cfg.extraConfig.client-rt + // mapToFiles "jack" cfg.extraConfig.jack + // mapToFiles "pipewire-pulse" cfg.extraConfig.pipewire-pulse + ); + + configs = pkgs.buildEnv { + name = "pipewire-configs"; + paths = configPackages + ++ [ extraConfigPkg ] + ++ lib.optionals cfg.wireplumber.enable cfg.wireplumber.configPackages; + pathsToLink = [ "/share/pipewire" ]; + }; + + requiredLv2Packages = lib.flatten + ( + lib.concatMap + (p: + lib.attrByPath ["passthru" "requiredLv2Packages"] [] p + ) + configPackages + ); + + lv2Plugins = pkgs.buildEnv { + name = "pipewire-lv2-plugins"; + paths = cfg.extraLv2Packages ++ requiredLv2Packages; + pathsToLink = [ "/lib/lv2" ]; + }; in { meta.maintainers = teams.freedesktop.members ++ [ lib.maintainers.k900 ]; ###### interface options = { services.pipewire = { - enable = mkEnableOption (lib.mdDoc "pipewire service"); + enable = mkEnableOption (lib.mdDoc "PipeWire service"); package = mkPackageOption pkgs "pipewire" { }; @@ -33,7 +72,7 @@ in { default = true; type = types.bool; description = lib.mdDoc '' - Automatically run pipewire when connections are made to the pipewire socket. + Automatically run PipeWire when connections are made to the PipeWire socket. ''; }; @@ -200,6 +239,34 @@ in { ''; }; }; + + configPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = []; + description = lib.mdDoc '' + List of packages that provide PipeWire configuration, in the form of + `share/pipewire/*/*.conf` files. + + LV2 dependencies will be picked up from config packages automatically + via `passthru.requiredLv2Packages`. + ''; + }; + + extraLv2Packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = []; + example = lib.literalExpression "[ pkgs.lsp-plugins ]"; + description = lib.mdDoc '' + List of packages that provide LV2 plugins in `lib/lv2` that should + be made available to PipeWire for [filter chains][wiki-filter-chain]. + + Config packages have their required LV2 plugins added automatically, + so they don't need to be specified here. Config packages need to set + `passthru.requiredLv2Packages` for this to work. + + [wiki-filter-chain]: https://docs.pipewire.org/page_module_filter_chain.html + ''; + }; }; }; @@ -230,6 +297,18 @@ in { assertion = (cfg.alsa.enable || cfg.pulse.enable) -> cfg.audio.enable; message = "Using PipeWire's ALSA/PulseAudio compatibility layers requires running PipeWire as the sound server. Set `services.pipewire.audio.enable` to true."; } + { + assertion = builtins.length + (builtins.attrNames + ( + lib.filterAttrs + (name: value: + lib.hasPrefix "pipewire/" name || name == "pipewire" + ) + config.environment.etc + )) == 1; + message = "Using `environment.etc.\"pipewire<...>\"` directly is no longer supported in 24.05. Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` instead."; + } ]; environment.systemPackages = [ cfg.package ] @@ -249,6 +328,9 @@ in { systemd.user.sockets.pipewire.enable = !cfg.systemWide; systemd.user.services.pipewire.enable = !cfg.systemWide; + systemd.services.pipewire.environment.LV2_PATH = lib.mkIf cfg.systemWide "${lv2Plugins}/lib/lv2"; + systemd.user.services.pipewire.environment.LV2_PATH = lib.mkIf (!cfg.systemWide) "${lv2Plugins}/lib/lv2"; + # Mask pw-pulse if it's not wanted systemd.user.services.pipewire-pulse.enable = cfg.pulse.enable; systemd.user.sockets.pipewire-pulse.enable = cfg.pulse.enable; @@ -283,12 +365,8 @@ in { "alsa/conf.d/99-pipewire-default.conf" = mkIf cfg.alsa.enable { source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf"; }; - } - // mapToFiles "pipewire" cfg.extraConfig.pipewire - // mapToFiles "client" cfg.extraConfig.client - // mapToFiles "client-rt" cfg.extraConfig.client-rt - // mapToFiles "jack" cfg.extraConfig.jack - // mapToFiles "pipewire-pulse" cfg.extraConfig.pipewire-pulse; + pipewire.source = "${configs}/share/pipewire"; + }; environment.sessionVariables.LD_LIBRARY_PATH = lib.mkIf cfg.jack.enable [ "${cfg.package.jack}/lib" ]; @@ -301,7 +379,7 @@ in { "audio" "video" ] ++ lib.optional config.security.rtkit.enable "rtkit"; - description = "Pipewire system service user"; + description = "PipeWire system service user"; isSystemUser = true; home = "/var/lib/pipewire"; createHome = true; diff --git a/nixos/modules/services/desktops/pipewire/wireplumber.nix b/nixos/modules/services/desktops/pipewire/wireplumber.nix index 95a7ece26c5d2..009d68bd4f28d 100644 --- a/nixos/modules/services/desktops/pipewire/wireplumber.nix +++ b/nixos/modules/services/desktops/pipewire/wireplumber.nix @@ -14,60 +14,127 @@ in type = lib.types.bool; default = config.services.pipewire.enable; defaultText = lib.literalExpression "config.services.pipewire.enable"; - description = lib.mdDoc "Whether to enable Wireplumber, a modular session / policy manager for PipeWire"; + description = lib.mdDoc "Whether to enable WirePlumber, a modular session / policy manager for PipeWire"; }; package = lib.mkOption { type = lib.types.package; default = pkgs.wireplumber; defaultText = lib.literalExpression "pkgs.wireplumber"; - description = lib.mdDoc "The wireplumber derivation to use."; + description = lib.mdDoc "The WirePlumber derivation to use."; }; - }; - }; - config = lib.mkIf cfg.enable { - assertions = [ - { - assertion = !config.hardware.bluetooth.hsphfpd.enable; - message = "Using Wireplumber conflicts with hsphfpd, as it provides the same functionality. `hardware.bluetooth.hsphfpd.enable` needs be set to false"; - } - ]; + configPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = lib.mdDoc '' + List of packages that provide WirePlumber configuration, in the form of + `share/wireplumber/*/*.lua` files. + + LV2 dependencies will be picked up from config packages automatically + via `passthru.requiredLv2Packages`. + ''; + }; + + extraLv2Packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = []; + example = lib.literalExpression "[ pkgs.lsp-plugins ]"; + description = lib.mdDoc '' + List of packages that provide LV2 plugins in `lib/lv2` that should + be made available to WirePlumber for [filter chains][wiki-filter-chain]. + + Config packages have their required LV2 plugins added automatically, + so they don't need to be specified here. Config packages need to set + `passthru.requiredLv2Packages` for this to work. - environment.systemPackages = [ cfg.package ]; + [wiki-filter-chain]: https://docs.pipewire.org/page_module_filter_chain.html + ''; + }; + }; + }; - environment.etc."wireplumber/main.lua.d/80-nixos.lua" = lib.mkIf (!pwUsedForAudio) { - text = '' - -- Pipewire is not used for audio, so prevent it from grabbing audio devices + config = + let + pwNotForAudioConfigPkg = pkgs.writeTextDir "share/wireplumber/main.lua.d/80-pw-not-for-audio.lua" '' + -- PipeWire is not used for audio, so prevent it from grabbing audio devices alsa_monitor.enable = function() end ''; - }; - environment.etc."wireplumber/main.lua.d/80-systemwide.lua" = lib.mkIf config.services.pipewire.systemWide { - text = '' + systemwideConfigPkg = pkgs.writeTextDir "share/wireplumber/main.lua.d/80-systemwide.lua" '' -- When running system-wide, these settings need to be disabled (they -- use functions that aren't available on the system dbus). alsa_monitor.properties["alsa.reserve"] = false default_access.properties["enable-flatpak-portal"] = false ''; - }; - environment.etc."wireplumber/bluetooth.lua.d/80-systemwide.lua" = lib.mkIf config.services.pipewire.systemWide { - text = '' + systemwideBluetoothConfigPkg = pkgs.writeTextDir "share/wireplumber/bluetooth.lua.d/80-systemwide.lua" '' -- When running system-wide, logind-integration needs to be disabled. bluez_monitor.properties["with-logind"] = false ''; - }; - systemd.packages = [ cfg.package ]; + configPackages = cfg.configPackages + ++ lib.optional (!pwUsedForAudio) pwNotForAudioConfigPkg + ++ lib.optionals config.services.pipewire.systemWide [ systemwideConfigPkg systemwideBluetoothConfigPkg ]; - systemd.services.wireplumber.enable = config.services.pipewire.systemWide; - systemd.user.services.wireplumber.enable = !config.services.pipewire.systemWide; + configs = pkgs.buildEnv { + name = "wireplumber-configs"; + paths = configPackages; + pathsToLink = [ "/share/wireplumber" ]; + }; + + requiredLv2Packages = lib.flatten + ( + lib.concatMap + (p: + lib.attrByPath ["passthru" "requiredLv2Packages"] [] p + ) + configPackages + ); + + lv2Plugins = pkgs.buildEnv { + name = "wireplumber-lv2-plugins"; + paths = cfg.extraLv2Packages ++ requiredLv2Packages; + pathsToLink = [ "/lib/lv2" ]; + }; + in + lib.mkIf cfg.enable { + assertions = [ + { + assertion = !config.hardware.bluetooth.hsphfpd.enable; + message = "Using WirePlumber conflicts with hsphfpd, as it provides the same functionality. `hardware.bluetooth.hsphfpd.enable` needs be set to false"; + } + { + assertion = builtins.length + (builtins.attrNames + ( + lib.filterAttrs + (name: value: + lib.hasPrefix "wireplumber/" name || name == "wireplumber" + ) + config.environment.etc + )) == 1; + message = "Using `environment.etc.\"wireplumber<...>\"` directly is no longer supported in 24.05. Use `services.pipewire.wireplumber.configPackages` instead."; + } + ]; + + environment.systemPackages = [ cfg.package ]; + + environment.etc.wireplumber.source = "${configs}/share/wireplumber"; - systemd.services.wireplumber.wantedBy = [ "pipewire.service" ]; - systemd.user.services.wireplumber.wantedBy = [ "pipewire.service" ]; + systemd.packages = [ cfg.package ]; - systemd.services.wireplumber.environment = lib.mkIf config.services.pipewire.systemWide { - # Force wireplumber to use system dbus. - DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/dbus/system_bus_socket"; + systemd.services.wireplumber.enable = config.services.pipewire.systemWide; + systemd.user.services.wireplumber.enable = !config.services.pipewire.systemWide; + + systemd.services.wireplumber.wantedBy = [ "pipewire.service" ]; + systemd.user.services.wireplumber.wantedBy = [ "pipewire.service" ]; + + systemd.services.wireplumber.environment = lib.mkIf config.services.pipewire.systemWide { + # Force WirePlumber to use system dbus. + DBUS_SESSION_BUS_ADDRESS = "unix:path=/run/dbus/system_bus_socket"; + LV2_PATH = "${lv2Plugins}/lib/lv2"; + }; + + systemd.user.services.wireplumber.environment.LV2_PATH = + lib.mkIf (!config.services.pipewire.systemWide) "${lv2Plugins}/lib/lv2"; }; - }; } diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix index 74f56f5890fce..df3d814d74441 100644 --- a/nixos/modules/services/development/lorri.nix +++ b/nixos/modules/services/development/lorri.nix @@ -44,8 +44,7 @@ in { serviceConfig = { ExecStart = "${cfg.package}/bin/lorri daemon"; PrivateTmp = true; - ProtectSystem = "strict"; - ProtectHome = "read-only"; + ProtectSystem = "full"; Restart = "on-failure"; }; }; diff --git a/nixos/modules/services/display-managers/greetd.nix b/nixos/modules/services/display-managers/greetd.nix index 2212f97a9ffe2..c2d345152de93 100644 --- a/nixos/modules/services/display-managers/greetd.nix +++ b/nixos/modules/services/display-managers/greetd.nix @@ -78,7 +78,7 @@ in serviceConfig = { ExecStart = "${pkgs.greetd.greetd}/bin/greetd --config ${settingsFormat.generate "greetd.toml" cfg.settings}"; - Restart = mkIf cfg.restart "always"; + Restart = mkIf cfg.restart "on-success"; # Defaults from greetd upstream configuration IgnoreSIGPIPE = false; diff --git a/nixos/modules/services/games/archisteamfarm.nix b/nixos/modules/services/games/archisteamfarm.nix index c00ae8116b394..4bb7234f430f2 100644 --- a/nixos/modules/services/games/archisteamfarm.nix +++ b/nixos/modules/services/games/archisteamfarm.nix @@ -270,6 +270,6 @@ in meta = { buildDocsInSandbox = false; - maintainers = with lib.maintainers; [ lom SuperSandro2000 ]; + maintainers = with lib.maintainers; [ SuperSandro2000 ]; }; } diff --git a/nixos/modules/services/games/armagetronad.nix b/nixos/modules/services/games/armagetronad.nix new file mode 100644 index 0000000000000..f79818e0e53b5 --- /dev/null +++ b/nixos/modules/services/games/armagetronad.nix @@ -0,0 +1,268 @@ +{ config, lib, pkgs, ... }: +let + inherit (lib) mkEnableOption mkIf mkOption mkMerge literalExpression; + inherit (lib) mapAttrsToList filterAttrs unique recursiveUpdate types; + + mkValueStringArmagetron = with lib; v: + if isInt v then toString v + else if isFloat v then toString v + else if isString v then v + else if true == v then "1" + else if false == v then "0" + else if null == v then "" + else throw "unsupported type: ${builtins.typeOf v}: ${(lib.generators.toPretty {} v)}"; + + settingsFormat = pkgs.formats.keyValue { + mkKeyValue = lib.generators.mkKeyValueDefault + { + mkValueString = mkValueStringArmagetron; + } " "; + listsAsDuplicateKeys = true; + }; + + cfg = config.services.armagetronad; + enabledServers = lib.filterAttrs (n: v: v.enable) cfg.servers; + nameToId = serverName: "armagetronad-${serverName}"; + getStateDirectory = serverName: "armagetronad/${serverName}"; + getServerRoot = serverName: "/var/lib/${getStateDirectory serverName}"; +in +{ + options = { + services.armagetronad = { + servers = mkOption { + description = lib.mdDoc "Armagetron server definitions."; + default = { }; + type = types.attrsOf (types.submodule { + options = { + enable = mkEnableOption (lib.mdDoc "armagetronad"); + + package = lib.mkPackageOptionMD pkgs "armagetronad-dedicated" { + example = '' + pkgs.armagetronad."0.2.9-sty+ct+ap".dedicated + ''; + extraDescription = '' + Ensure that you use a derivation which contains the path `bin/armagetronad-dedicated`. + ''; + }; + + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = lib.mdDoc "Host to listen on. Used for SERVER_IP."; + }; + + port = mkOption { + type = types.port; + default = 4534; + description = lib.mdDoc "Port to listen on. Used for SERVER_PORT."; + }; + + dns = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc "DNS address to use for this server. Optional."; + }; + + openFirewall = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Set to true to open the configured UDP port for Armagetron Advanced."; + }; + + name = mkOption { + type = types.str; + description = "The name of this server."; + }; + + settings = mkOption { + type = settingsFormat.type; + default = { }; + description = lib.mdDoc '' + Armagetron Advanced server rules configuration. Refer to: + <https://wiki.armagetronad.org/index.php?title=Console_Commands> + or `armagetronad-dedicated --doc` for a list. + + This attrset is used to populate `settings_custom.cfg`; see: + <https://wiki.armagetronad.org/index.php/Configuration_Files> + ''; + example = literalExpression '' + { + CYCLE_RUBBER = 40; + } + ''; + }; + + roundSettings = mkOption { + type = settingsFormat.type; + default = { }; + description = lib.mdDoc '' + Armagetron Advanced server per-round configuration. Refer to: + <https://wiki.armagetronad.org/index.php?title=Console_Commands> + or `armagetronad-dedicated --doc` for a list. + + This attrset is used to populate `everytime.cfg`; see: + <https://wiki.armagetronad.org/index.php/Configuration_Files> + ''; + example = literalExpression '' + { + SAY = [ + "Hosted on NixOS" + "https://nixos.org" + "iD Tech High Rubber rul3z!! Happy New Year 2008!!1" + ]; + } + ''; + }; + }; + }); + }; + }; + }; + + config = mkIf (enabledServers != { }) { + systemd.tmpfiles.settings = mkMerge (mapAttrsToList + (serverName: serverCfg: + let + serverId = nameToId serverName; + serverRoot = getServerRoot serverName; + serverInfo = ( + { + SERVER_IP = serverCfg.host; + SERVER_PORT = serverCfg.port; + SERVER_NAME = serverCfg.name; + } // (lib.optionalAttrs (serverCfg.dns != null) { SERVER_DNS = serverCfg.dns; }) + ); + customSettings = serverCfg.settings; + everytimeSettings = serverCfg.roundSettings; + + serverInfoCfg = settingsFormat.generate "server_info.${serverName}.cfg" serverInfo; + customSettingsCfg = settingsFormat.generate "settings_custom.${serverName}.cfg" customSettings; + everytimeSettingsCfg = settingsFormat.generate "everytime.${serverName}.cfg" everytimeSettings; + in + { + "10-armagetronad-${serverId}" = { + "${serverRoot}/data" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/settings" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/var" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/resource" = { + d = { + group = serverId; + user = serverId; + mode = "0750"; + }; + }; + "${serverRoot}/input" = { + "f+" = { + group = serverId; + user = serverId; + mode = "0640"; + }; + }; + "${serverRoot}/settings/server_info.cfg" = { + "L+" = { + argument = "${serverInfoCfg}"; + }; + }; + "${serverRoot}/settings/settings_custom.cfg" = { + "L+" = { + argument = "${customSettingsCfg}"; + }; + }; + "${serverRoot}/settings/everytime.cfg" = { + "L+" = { + argument = "${everytimeSettingsCfg}"; + }; + }; + }; + } + ) + enabledServers + ); + + systemd.services = mkMerge (mapAttrsToList + (serverName: serverCfg: + let + serverId = nameToId serverName; + in + { + "armagetronad-${serverName}" = { + description = "Armagetron Advanced Dedicated Server for ${serverName}"; + wants = [ "basic.target" ]; + after = [ "basic.target" "network.target" "multi-user.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = + let + serverRoot = getServerRoot serverName; + in + { + Type = "simple"; + StateDirectory = getStateDirectory serverName; + ExecStart = "${lib.getExe serverCfg.package} --daemon --input ${serverRoot}/input --userdatadir ${serverRoot}/data --userconfigdir ${serverRoot}/settings --vardir ${serverRoot}/var --autoresourcedir ${serverRoot}/resource"; + Restart = "on-failure"; + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + User = serverId; + Group = serverId; + }; + }; + }) + enabledServers + ); + + networking.firewall.allowedUDPPorts = + unique (mapAttrsToList (serverName: serverCfg: serverCfg.port) (filterAttrs (serverName: serverCfg: serverCfg.openFirewall) enabledServers)); + + users.users = mkMerge (mapAttrsToList + (serverName: serverCfg: + { + ${nameToId serverName} = { + group = nameToId serverName; + description = "Armagetron Advanced dedicated user for server ${serverName}"; + isSystemUser = true; + }; + }) + enabledServers + ); + + users.groups = mkMerge (mapAttrsToList + (serverName: serverCfg: + { + ${nameToId serverName} = { }; + }) + enabledServers + ); + }; +} diff --git a/nixos/modules/services/games/teeworlds.nix b/nixos/modules/services/games/teeworlds.nix index bd0df1ffca578..04b611fb3cb14 100644 --- a/nixos/modules/services/games/teeworlds.nix +++ b/nixos/modules/services/games/teeworlds.nix @@ -6,13 +6,86 @@ let cfg = config.services.teeworlds; register = cfg.register; + bool = b: if b != null && b then "1" else "0"; + optionalSetting = s: setting: optionalString (s != null) "${setting} ${s}"; + lookup = attrs: key: default: if attrs ? key then attrs."${key}" else default; + + inactivePenaltyOptions = { + "spectator" = "1"; + "spectator/kick" = "2"; + "kick" = "3"; + }; + skillLevelOptions = { + "casual" = "0"; + "normal" = "1"; + "competitive" = "2"; + }; + tournamentModeOptions = { + "disable" = "0"; + "enable" = "1"; + "restrictSpectators" = "2"; + }; + teeworldsConf = pkgs.writeText "teeworlds.cfg" '' sv_port ${toString cfg.port} - sv_register ${if cfg.register then "1" else "0"} - ${optionalString (cfg.name != null) "sv_name ${cfg.name}"} - ${optionalString (cfg.motd != null) "sv_motd ${cfg.motd}"} - ${optionalString (cfg.password != null) "password ${cfg.password}"} - ${optionalString (cfg.rconPassword != null) "sv_rcon_password ${cfg.rconPassword}"} + sv_register ${bool cfg.register} + sv_name ${cfg.name} + ${optionalSetting cfg.motd "sv_motd"} + ${optionalSetting cfg.password "password"} + ${optionalSetting cfg.rconPassword "sv_rcon_password"} + + ${optionalSetting cfg.server.bindAddr "bindaddr"} + ${optionalSetting cfg.server.hostName "sv_hostname"} + sv_high_bandwidth ${bool cfg.server.enableHighBandwidth} + sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"} + sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators} + sv_inactivekick_time ${toString cfg.server.inactiveTime} + sv_max_clients ${toString cfg.server.maxClients} + sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP} + sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"} + sv_spamprotection ${bool cfg.server.enableSpamProtection} + + sv_gametype ${cfg.game.gameType} + sv_map ${cfg.game.map} + sv_match_swap ${bool cfg.game.swapTeams} + sv_player_ready_mode ${bool cfg.game.enableReadyMode} + sv_player_slots ${toString cfg.game.playerSlots} + sv_powerups ${bool cfg.game.enablePowerups} + sv_scorelimit ${toString cfg.game.scoreLimit} + sv_strict_spectate_mode ${bool cfg.game.restrictSpectators} + sv_teamdamage ${bool cfg.game.enableTeamDamage} + sv_timelimit ${toString cfg.game.timeLimit} + sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"} + sv_vote_kick ${bool cfg.game.enableVoteKick} + sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime} + sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers} + + ${optionalSetting cfg.server.bindAddr "bindaddr"} + ${optionalSetting cfg.server.hostName "sv_hostname"} + sv_high_bandwidth ${bool cfg.server.enableHighBandwidth} + sv_inactivekick ${lookup inactivePenaltyOptions cfg.server.inactivePenalty "spectator/kick"} + sv_inactivekick_spec ${bool cfg.server.kickInactiveSpectators} + sv_inactivekick_time ${toString cfg.server.inactiveTime} + sv_max_clients ${toString cfg.server.maxClients} + sv_max_clients_per_ip ${toString cfg.server.maxClientsPerIP} + sv_skill_level ${lookup skillLevelOptions cfg.server.skillLevel "normal"} + sv_spamprotection ${bool cfg.server.enableSpamProtection} + + sv_gametype ${cfg.game.gameType} + sv_map ${cfg.game.map} + sv_match_swap ${bool cfg.game.swapTeams} + sv_player_ready_mode ${bool cfg.game.enableReadyMode} + sv_player_slots ${toString cfg.game.playerSlots} + sv_powerups ${bool cfg.game.enablePowerups} + sv_scorelimit ${toString cfg.game.scoreLimit} + sv_strict_spectate_mode ${bool cfg.game.restrictSpectators} + sv_teamdamage ${bool cfg.game.enableTeamDamage} + sv_timelimit ${toString cfg.game.timeLimit} + sv_tournament_mode ${lookup tournamentModeOptions cfg.server.tournamentMode "disable"} + sv_vote_kick ${bool cfg.game.enableVoteKick} + sv_vote_kick_bantime ${toString cfg.game.voteKickBanTime} + sv_vote_kick_min ${toString cfg.game.voteKickMinimumPlayers} + ${concatStringsSep "\n" cfg.extraOptions} ''; @@ -22,17 +95,19 @@ in services.teeworlds = { enable = mkEnableOption (lib.mdDoc "Teeworlds Server"); + package = mkPackageOptionMD pkgs "teeworlds-server" { }; + openPorts = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether to open firewall ports for Teeworlds"; + description = lib.mdDoc "Whether to open firewall ports for Teeworlds."; }; name = mkOption { - type = types.nullOr types.str; - default = null; + type = types.str; + default = "unnamed server"; description = lib.mdDoc '' - Name of the server. Defaults to 'unnamed server'. + Name of the server. ''; }; @@ -41,7 +116,7 @@ in example = true; default = false; description = lib.mdDoc '' - Whether the server registers as public server in the global server list. This is disabled by default because of privacy. + Whether the server registers as a public server in the global server list. This is disabled by default for privacy reasons. ''; }; @@ -49,7 +124,7 @@ in type = types.nullOr types.str; default = null; description = lib.mdDoc '' - Set the server message of the day text. + The server's message of the day text. ''; }; @@ -85,6 +160,217 @@ in ''; example = [ "sv_map dm1" "sv_gametype dm" ]; }; + + server = { + bindAddr = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + The address the server will bind to. + ''; + }; + + enableHighBandwidth = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable high bandwidth mode on LAN servers. This will double the amount of bandwidth required for running the server. + ''; + }; + + hostName = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Hostname for the server. + ''; + }; + + inactivePenalty = mkOption { + type = types.enum [ "spectator" "spectator/kick" "kick" ]; + example = "spectator"; + default = "spectator/kick"; + description = lib.mdDoc '' + Specify what to do when a client goes inactive (see [](#opt-services.teeworlds.server.inactiveTime)). + + - `spectator`: send the client into spectator mode + + - `spectator/kick`: send the client into a free spectator slot, otherwise kick the client + + - `kick`: kick the client + ''; + }; + + kickInactiveSpectators = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to kick inactive spectators. + ''; + }; + + inactiveTime = mkOption { + type = types.ints.unsigned; + default = 3; + description = lib.mdDoc '' + The amount of minutes a client has to idle before it is considered inactive. + ''; + }; + + maxClients = mkOption { + type = types.ints.unsigned; + default = 12; + description = lib.mdDoc '' + The maximum amount of clients that can be connected to the server at the same time. + ''; + }; + + maxClientsPerIP = mkOption { + type = types.ints.unsigned; + default = 12; + description = lib.mdDoc '' + The maximum amount of clients with the same IP address that can be connected to the server at the same time. + ''; + }; + + skillLevel = mkOption { + type = types.enum [ "casual" "normal" "competitive" ]; + default = "normal"; + description = lib.mdDoc '' + The skill level shown in the server browser. + ''; + }; + + enableSpamProtection = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable chat spam protection. + ''; + }; + }; + + game = { + gameType = mkOption { + type = types.str; + example = "ctf"; + default = "dm"; + description = lib.mdDoc '' + The game type to use on the server. + + The default gametypes are `dm`, `tdm`, `ctf`, `lms`, and `lts`. + ''; + }; + + map = mkOption { + type = types.str; + example = "ctf5"; + default = "dm1"; + description = lib.mdDoc '' + The map to use on the server. + ''; + }; + + swapTeams = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to swap teams each round. + ''; + }; + + enableReadyMode = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable "ready mode"; where players can pause/unpause the game + and start the game in warmup, using their ready state. + ''; + }; + + playerSlots = mkOption { + type = types.ints.unsigned; + default = 8; + description = lib.mdDoc '' + The amount of slots to reserve for players (as opposed to spectators). + ''; + }; + + enablePowerups = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to allow powerups such as the ninja. + ''; + }; + + scoreLimit = mkOption { + type = types.ints.unsigned; + example = 400; + default = 20; + description = lib.mdDoc '' + The score limit needed to win a round. + ''; + }; + + restrictSpectators = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to restrict access to information such as health, ammo and armour in spectator mode. + ''; + }; + + enableTeamDamage = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable team damage; whether to allow team mates to inflict damage on one another. + ''; + }; + + timeLimit = mkOption { + type = types.ints.unsigned; + default = 0; + description = lib.mdDoc '' + Time limit of the game. In cases of equal points, there will be sudden death. + Setting this to 0 disables a time limit. + ''; + }; + + tournamentMode = mkOption { + type = types.enum [ "disable" "enable" "restrictSpectators" ]; + default = "disable"; + description = lib.mdDoc '' + Whether to enable tournament mode. In tournament mode, players join as spectators. + If this is set to `restrictSpectators`, tournament mode is enabled but spectator chat is restricted. + ''; + }; + + enableVoteKick = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable voting to kick players. + ''; + }; + + voteKickBanTime = mkOption { + type = types.ints.unsigned; + default = 5; + description = lib.mdDoc '' + The amount of minutes that a player is banned for if they get kicked by a vote. + ''; + }; + + voteKickMinimumPlayers = mkOption { + type = types.ints.unsigned; + default = 5; + description = lib.mdDoc '' + The minimum amount of players required to start a kick vote. + ''; + }; + }; }; }; @@ -100,7 +386,7 @@ in serviceConfig = { DynamicUser = true; - ExecStart = "${pkgs.teeworlds-server}/bin/teeworlds_srv -f ${teeworldsConf}"; + ExecStart = "${cfg.package}/bin/teeworlds_srv -f ${teeworldsConf}"; # Hardening CapabilityBoundingSet = false; diff --git a/nixos/modules/services/hardware/asusd.nix b/nixos/modules/services/hardware/asusd.nix index ebbdea26c0514..ff9a751e5be87 100644 --- a/nixos/modules/services/hardware/asusd.nix +++ b/nixos/modules/services/hardware/asusd.nix @@ -8,6 +8,8 @@ in services.asusd = { enable = lib.mkEnableOption (lib.mdDoc "the asusd service for ASUS ROG laptops"); + package = lib.mkPackageOption pkgs "asusctl" { }; + enableUserService = lib.mkOption { type = lib.types.bool; default = false; @@ -73,7 +75,7 @@ in }; config = lib.mkIf cfg.enable { - environment.systemPackages = [ pkgs.asusctl ]; + environment.systemPackages = [ cfg.package ]; environment.etc = let @@ -92,9 +94,9 @@ in }; services.dbus.enable = true; - systemd.packages = [ pkgs.asusctl ]; - services.dbus.packages = [ pkgs.asusctl ]; - services.udev.packages = [ pkgs.asusctl ]; + systemd.packages = [ cfg.package ]; + services.dbus.packages = [ cfg.package ]; + services.udev.packages = [ cfg.package ]; services.supergfxd.enable = lib.mkDefault true; systemd.user.services.asusd-user.enable = cfg.enableUserService; diff --git a/nixos/modules/services/hardware/bolt.nix b/nixos/modules/services/hardware/bolt.nix index 6990a9ea63b37..3bdf67cc17581 100644 --- a/nixos/modules/services/hardware/bolt.nix +++ b/nixos/modules/services/hardware/bolt.nix @@ -1,14 +1,13 @@ -# Thunderbolt 3 device manager - { config, lib, pkgs, ...}: with lib; +let + cfg = config.services.hardware.bolt; +in { options = { - services.hardware.bolt = { - enable = mkOption { type = types.bool; default = false; @@ -20,15 +19,13 @@ with lib; ''; }; + package = mkPackageOption pkgs "bolt" { }; }; - }; - config = mkIf config.services.hardware.bolt.enable { - - environment.systemPackages = [ pkgs.bolt ]; - services.udev.packages = [ pkgs.bolt ]; - systemd.packages = [ pkgs.bolt ]; - + config = mkIf cfg.enable { + environment.systemPackages = [ cfg.package ]; + services.udev.packages = [ cfg.package ]; + systemd.packages = [ cfg.package ]; }; } diff --git a/nixos/modules/services/hardware/brltty.nix b/nixos/modules/services/hardware/brltty.nix index 3133804f485f6..f96760e92c576 100644 --- a/nixos/modules/services/hardware/brltty.nix +++ b/nixos/modules/services/hardware/brltty.nix @@ -34,6 +34,7 @@ in { users.users.brltty = { description = "BRLTTY daemon user"; group = "brltty"; + isSystemUser = true; }; users.groups = { brltty = { }; diff --git a/nixos/modules/services/hardware/fwupd.nix b/nixos/modules/services/hardware/fwupd.nix index 6fbcbe6764600..8a9e38d0547bc 100644 --- a/nixos/modules/services/hardware/fwupd.nix +++ b/nixos/modules/services/hardware/fwupd.nix @@ -51,7 +51,9 @@ let # to install it because it would create a cyclic dependency between # the outputs. We also need to enable the remote, # which should not be done by default. - lib.optionalAttrs cfg.enableTestRemote (enableRemote cfg.package.installedTests "fwupd-tests") + lib.optionalAttrs + (cfg.daemonSettings.TestDevices or false) + (enableRemote cfg.package.installedTests "fwupd-tests") ); in { @@ -86,15 +88,6 @@ in { ''; }; - enableTestRemote = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Whether to enable test remote. This is used by - [installed tests](https://github.com/fwupd/fwupd/blob/master/data/installed-tests/README.md). - ''; - }; - package = mkPackageOption pkgs "fwupd" { }; daemonSettings = mkOption { @@ -128,6 +121,16 @@ in { or if this partition is not mounted at /boot/efi, /boot, or /efi ''; }; + + TestDevices = mkOption { + internal = true; + type = types.bool; + default = false; + description = lib.mdDoc '' + Create virtual test devices and remote for validating daemon flows. + This is only intended for CI testing and development purposes. + ''; + }; }; }; default = {}; @@ -153,13 +156,13 @@ in { (mkRenamedOptionModule [ "services" "fwupd" "blacklistPlugins"] [ "services" "fwupd" "daemonSettings" "DisabledPlugins" ]) (mkRenamedOptionModule [ "services" "fwupd" "disabledDevices" ] [ "services" "fwupd" "daemonSettings" "DisabledDevices" ]) (mkRenamedOptionModule [ "services" "fwupd" "disabledPlugins" ] [ "services" "fwupd" "daemonSettings" "DisabledPlugins" ]) + (mkRemovedOptionModule [ "services" "fwupd" "enableTestRemote" ] "This option was removed after being removed upstream. It only provided a method for testing fwupd functionality, and should not have been exposed for use outside of nix tests.") ]; ###### implementation config = mkIf cfg.enable { # Disable test related plug-ins implicitly so that users do not have to care about them. services.fwupd.daemonSettings = { - DisabledPlugins = cfg.package.defaultDisabledPlugins; EspLocation = config.boot.loader.efi.efiSysMountPoint; }; diff --git a/nixos/modules/services/hardware/hddfancontrol.nix b/nixos/modules/services/hardware/hddfancontrol.nix index f472b5774cbf6..746154e7aa17e 100644 --- a/nixos/modules/services/hardware/hddfancontrol.nix +++ b/nixos/modules/services/hardware/hddfancontrol.nix @@ -60,6 +60,10 @@ in systemd.services.hddfancontrol = { wantedBy = [ "multi-user.target" ]; environment.HDDFANCONTROL_ARGS = lib.escapeShellArgs args; + serviceConfig = { + # Hardening + PrivateNetwork = true; + }; }; } ); diff --git a/nixos/modules/services/hardware/monado.nix b/nixos/modules/services/hardware/monado.nix new file mode 100644 index 0000000000000..9f9c6c39a0b49 --- /dev/null +++ b/nixos/modules/services/hardware/monado.nix @@ -0,0 +1,102 @@ +{ config +, lib +, pkgs +, ... +}: +let + inherit (lib) mkDefault mkEnableOption mkIf mkOption mkPackageOption types; + + cfg = config.services.monado; + +in +{ + options.services.monado = { + enable = mkEnableOption "Monado user service"; + + package = mkPackageOption pkgs "monado" { }; + + defaultRuntime = mkOption { + type = types.bool; + description = '' + Whether to enable Monado as the default OpenXR runtime on the system. + + Note that applications can bypass this option by setting an active + runtime in a writable XDG_CONFIG_DIRS location like `~/.config`. + ''; + default = false; + example = true; + }; + + highPriority = mkEnableOption "high priority capability for monado-service" + // mkOption { default = true; }; + }; + + config = mkIf cfg.enable { + security.wrappers."monado-service" = mkIf cfg.highPriority { + setuid = false; + owner = "root"; + group = "root"; + # cap_sys_nice needed for asynchronous reprojection + capabilities = "cap_sys_nice+eip"; + source = lib.getExe' cfg.package "monado-service"; + }; + + services.udev.packages = with pkgs; [ xr-hardware ]; + + systemd.user = { + services.monado = { + description = "Monado XR runtime service module"; + requires = [ "monado.socket" ]; + conflicts = [ "monado-dev.service" ]; + + unitConfig.ConditionUser = "!root"; + + environment = { + # Default options + # https://gitlab.freedesktop.org/monado/monado/-/blob/4548e1738591d0904f8db4df8ede652ece889a76/src/xrt/targets/service/monado.in.service#L12 + XRT_COMPOSITOR_LOG = mkDefault "debug"; + XRT_PRINT_OPTIONS = mkDefault "on"; + IPC_EXIT_ON_DISCONNECT = mkDefault "off"; + }; + + serviceConfig = { + ExecStart = + if cfg.highPriority + then "${config.security.wrapperDir}/monado-service" + else lib.getExe' cfg.package "monado-service"; + Restart = "no"; + }; + + restartTriggers = [ cfg.package ]; + }; + + sockets.monado = { + description = "Monado XR service module connection socket"; + conflicts = [ "monado-dev.service" ]; + + unitConfig.ConditionUser = "!root"; + + socketConfig = { + ListenStream = "%t/monado_comp_ipc"; + RemoveOnStop = true; + + # If Monado crashes while starting up, we want to close incoming OpenXR connections + FlushPending = true; + }; + + restartTriggers = [ cfg.package ]; + + wantedBy = [ "sockets.target" ]; + }; + }; + + environment.systemPackages = [ cfg.package ]; + environment.pathsToLink = [ "/share/openxr" ]; + + environment.etc."xdg/openxr/1/active_runtime.json" = mkIf cfg.defaultRuntime { + source = "${cfg.package}/share/openxr/1/openxr_monado.json"; + }; + }; + + meta.maintainers = with lib.maintainers; [ Scrumplex ]; +} diff --git a/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix new file mode 100644 index 0000000000000..a90d234f65c0c --- /dev/null +++ b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/cdi-generate.nix @@ -0,0 +1,39 @@ +{ config, lib, pkgs }: let + mountOptions = { options = ["ro" "nosuid" "nodev" "bind"]; }; + mounts = [ + { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-cuda-mps-control"; + containerPath = "/usr/bin/nvidia-cuda-mps-control"; } + { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-cuda-mps-server"; + containerPath = "/usr/bin/nvidia-cuda-mps-server"; } + { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-debugdump"; + containerPath = "/usr/bin/nvidia-debugdump"; } + { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-powerd"; + containerPath = "/usr/bin/nvidia-powerd"; } + { hostPath = "${lib.getBin config.hardware.nvidia.package}/bin/nvidia-smi"; + containerPath = "/usr/bin/nvidia-smi"; } + { hostPath = "${pkgs.nvidia-container-toolkit}/bin/nvidia-ctk"; + containerPath = "/usr/bin/nvidia-ctk"; } + { hostPath = "${pkgs.glibc}/lib"; + containerPath = "${pkgs.glibc}/lib"; } + { hostPath = "${pkgs.glibc}/lib64"; + containerPath = "${pkgs.glibc}/lib64"; } + ]; + jqAddMountExpression = ".containerEdits.mounts[.containerEdits.mounts | length] |= . +"; + mountsToJq = lib.concatMap + (mount: + ["${pkgs.jq}/bin/jq '${jqAddMountExpression} ${builtins.toJSON (mount // mountOptions)}'"]) + mounts; +in '' +#! ${pkgs.runtimeShell} + +function cdiGenerate { + ${pkgs.nvidia-container-toolkit}/bin/nvidia-ctk cdi generate \ + --format json \ + --ldconfig-path ${pkgs.glibc.bin}/bin/ldconfig \ + --library-search-path ${config.hardware.nvidia.package}/lib \ + --nvidia-ctk-path ${pkgs.nvidia-container-toolkit}/bin/nvidia-ctk +} + +cdiGenerate | \ + ${lib.concatStringsSep " | " mountsToJq} > $RUNTIME_DIRECTORY/nvidia-container-toolkit.json +'' diff --git a/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix new file mode 100644 index 0000000000000..3c96e9c41be52 --- /dev/null +++ b/nixos/modules/services/hardware/nvidia-container-toolkit-cdi-generator/default.nix @@ -0,0 +1,38 @@ +{ config, lib, pkgs, ... }: + +{ + + options = { + + hardware.nvidia-container-toolkit-cdi-generator.enable = lib.mkOption { + default = false; + internal = true; + visible = false; + type = lib.types.bool; + description = lib.mdDoc '' + Enable dynamic CDI configuration for NVidia devices by running + nvidia-container-toolkit on boot. + ''; + }; + + }; + + config = { + + systemd.services.nvidia-container-toolkit-cdi-generator = lib.mkIf config.hardware.nvidia-container-toolkit-cdi-generator.enable { + description = "Container Device Interface (CDI) for Nvidia generator"; + wantedBy = [ "multi-user.target" ]; + after = [ "systemd-udev-settle.service" ]; + serviceConfig = { + RuntimeDirectory = "cdi"; + RemainAfterExit = true; + ExecStart = let + script = (pkgs.writeScriptBin "nvidia-cdi-generator" + (import ./cdi-generate.nix { inherit config lib pkgs; })); in (lib.getExe script); + Type = "oneshot"; + }; + }; + + }; + +} diff --git a/nixos/modules/services/hardware/pcscd.nix b/nixos/modules/services/hardware/pcscd.nix index 85accd8335f78..77c2d9b53f03d 100644 --- a/nixos/modules/services/hardware/pcscd.nix +++ b/nixos/modules/services/hardware/pcscd.nix @@ -3,6 +3,7 @@ with lib; let + cfg = config.services.pcscd; cfgFile = pkgs.writeText "reader.conf" config.services.pcscd.readerConfig; package = if config.security.polkit.enable @@ -41,13 +42,19 @@ in See {manpage}`reader.conf(5)` for valid options. ''; }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = lib.mdDoc "Extra command line arguments to be passed to the PCSC daemon."; + }; }; config = mkIf config.services.pcscd.enable { environment.etc."reader.conf".source = cfgFile; - environment.systemPackages = [ package.out ]; - systemd.packages = [ (getBin package) ]; + environment.systemPackages = [ package ]; + systemd.packages = [ package ]; services.pcscd.plugins = [ pkgs.ccid ]; @@ -64,7 +71,7 @@ in # around it, we force the path to the cfgFile. # # https://github.com/NixOS/nixpkgs/issues/121088 - serviceConfig.ExecStart = [ "" "${getBin package}/bin/pcscd -f -x -c ${cfgFile}" ]; + serviceConfig.ExecStart = [ "" "${lib.getExe package} -f -x -c ${cfgFile} ${lib.escapeShellArgs cfg.extraArgs}" ]; }; }; } diff --git a/nixos/modules/services/hardware/thinkfan.nix b/nixos/modules/services/hardware/thinkfan.nix index cca35f492b8e3..b62fb5e9f8c96 100644 --- a/nixos/modules/services/hardware/thinkfan.nix +++ b/nixos/modules/services/hardware/thinkfan.nix @@ -217,8 +217,13 @@ in { systemd.services = { thinkfan.environment.THINKFAN_ARGS = escapeShellArgs ([ "-c" configFile ] ++ cfg.extraArgs); - thinkfan.serviceConfig.Restart = "on-failure"; - thinkfan.serviceConfig.RestartSec = "30s"; + thinkfan.serviceConfig = { + Restart = "on-failure"; + RestartSec = "30s"; + + # Hardening + PrivateNetwork = true; + }; # must be added manually, see issue #81138 thinkfan.wantedBy = [ "multi-user.target" ]; diff --git a/nixos/modules/services/hardware/undervolt.nix b/nixos/modules/services/hardware/undervolt.nix index 67d8171587bb2..c4d4c6791a21b 100644 --- a/nixos/modules/services/hardware/undervolt.nix +++ b/nixos/modules/services/hardware/undervolt.nix @@ -12,6 +12,7 @@ let inherit (cfg) verbose temp + turbo ; # `core` and `cache` are both intentionally set to `cfg.coreOffset` as according to the undervolt docs: # @@ -105,6 +106,14 @@ in ''; }; + turbo = mkOption { + type = types.nullOr types.int; + default = null; + description = lib.mdDoc '' + Changes the Intel Turbo feature status (1 is disabled and 0 is enabled). + ''; + }; + p1.limit = mkOption { type = with types; nullOr int; default = null; diff --git a/nixos/modules/services/home-automation/govee2mqtt.nix b/nixos/modules/services/home-automation/govee2mqtt.nix new file mode 100644 index 0000000000000..1dee5999fa3be --- /dev/null +++ b/nixos/modules/services/home-automation/govee2mqtt.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.govee2mqtt; +in { + meta.maintainers = with lib.maintainers; [ SuperSandro2000 ]; + + options.services.govee2mqtt = { + enable = lib.mkEnableOption "Govee2MQTT"; + + package = lib.mkPackageOption pkgs "govee2mqtt" { }; + + user = lib.mkOption { + type = lib.types.str; + default = "govee2mqtt"; + description = "User under which Govee2MQTT should run."; + }; + + group = lib.mkOption { + type = lib.types.str; + default = "govee2mqtt"; + description = "Group under which Govee2MQTT should run."; + }; + + environmentFile = lib.mkOption { + type = lib.types.path; + example = "/var/lib/govee2mqtt/govee2mqtt.env"; + description = '' + Environment file as defined in {manpage}`systemd.exec(5)`. + + See upstream documentation <https://github.com/wez/govee2mqtt/blob/main/docs/CONFIG.md>. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + users = { + groups.${cfg.group} = { }; + users.${cfg.user} = { + description = "Govee2MQTT service user"; + inherit (cfg) group; + isSystemUser = true; + }; + }; + + systemd.services.govee2mqtt = { + description = "Govee2MQTT Service"; + wantedBy = [ "multi-user.target" ]; + after = [ "networking.target" ]; + serviceConfig = { + CacheDirectory = "govee2mqtt"; + Environment = [ + "GOVEE_CACHE_DIR=/var/cache/govee2mqtt" + ]; + EnvironmentFile = cfg.environmentFile; + ExecStart = "${lib.getExe cfg.package} serve --govee-iot-key=/var/lib/govee2mqtt/iot.key --govee-iot-cert=/var/lib/govee2mqtt/iot.cert" + + " --amazon-root-ca=${pkgs.cacert.unbundled}/etc/ssl/certs/Amazon_Root_CA_1:66c9fcf99bf8c0a39e2f0788a43e696365bca.crt"; + Group = cfg.group; + Restart = "on-failure"; + StateDirectory = "govee2mqtt"; + User = cfg.user; + + # Hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + LockPersonality = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateMounts = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + }; + }; + }; +} diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix index a01628968966e..3423eebe9ed64 100644 --- a/nixos/modules/services/home-automation/home-assistant.nix +++ b/nixos/modules/services/home-automation/home-assistant.nix @@ -52,7 +52,7 @@ let hasAttrByPath (splitString "." component) cfg.config || useComponentPlatform component || useExplicitComponent component - || builtins.elem component cfg.extraComponents; + || builtins.elem component (cfg.extraComponents ++ cfg.defaultIntegrations); # Final list of components passed into the package to include required dependencies extraComponents = filter useComponent availableComponents; @@ -103,6 +103,45 @@ in { description = lib.mdDoc "The config directory, where your {file}`configuration.yaml` is located."; }; + defaultIntegrations = mkOption { + type = types.listOf (types.enum availableComponents); + # https://github.com/home-assistant/core/blob/dev/homeassistant/bootstrap.py#L109 + default = [ + "application_credentials" + "frontend" + "hardware" + "logger" + "network" + "system_health" + + # key features + "automation" + "person" + "scene" + "script" + "tag" + "zone" + + # built-in helpers + "counter" + "input_boolean" + "input_button" + "input_datetime" + "input_number" + "input_select" + "input_text" + "schedule" + "timer" + + # non-supervisor + "backup" + ]; + readOnly = true; + description = '' + List of integrations set are always set up, unless in recovery mode. + ''; + }; + extraComponents = mkOption { type = types.listOf (types.enum availableComponents); default = [ @@ -533,6 +572,7 @@ in { "inkbird" "improv_ble" "keymitt_ble" + "leaone-ble" "led_ble" "medcom_ble" "melnor" diff --git a/nixos/modules/services/home-automation/matter-server.nix b/nixos/modules/services/home-automation/matter-server.nix new file mode 100644 index 0000000000000..864ef9e200837 --- /dev/null +++ b/nixos/modules/services/home-automation/matter-server.nix @@ -0,0 +1,125 @@ +{ lib +, pkgs +, config +, ... +}: + +with lib; + +let + cfg = config.services.matter-server; + storageDir = "matter-server"; + storagePath = "/var/lib/${storageDir}"; + vendorId = "4939"; # home-assistant vendor ID +in + +{ + meta.maintainers = with lib.maintainers; [ leonm1 ]; + + options.services.matter-server = with types; { + enable = mkEnableOption (lib.mdDoc "Matter-server"); + + package = mkPackageOptionMD pkgs "python-matter-server" { }; + + port = mkOption { + type = types.port; + default = 5580; + description = "Port to expose the matter-server service on."; + }; + + logLevel = mkOption { + type = types.enum [ "critical" "error" "warning" "info" "debug" ]; + default = "info"; + description = "Verbosity of logs from the matter-server"; + }; + + extraArgs = mkOption { + type = listOf str; + default = []; + description = '' + Extra arguments to pass to the matter-server executable. + See https://github.com/home-assistant-libs/python-matter-server?tab=readme-ov-file#running-the-development-server for options. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.matter-server = { + after = [ "network-online.target" ]; + before = [ "home-assistant.service" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + description = "Matter Server"; + environment.HOME = storagePath; + serviceConfig = { + ExecStart = (concatStringsSep " " [ + "${cfg.package}/bin/matter-server" + "--port" (toString cfg.port) + "--vendorid" vendorId + "--storage-path" storagePath + "--log-level" "${cfg.logLevel}" + "${escapeShellArgs cfg.extraArgs}" + ]); + # Start with a clean root filesystem, and allowlist what the container + # is permitted to access. + TemporaryFileSystem = "/"; + # Allowlist /nix/store (to allow the binary to find its dependencies) + # and dbus. + ReadOnlyPaths = "/nix/store /run/dbus"; + # Let systemd manage `/var/lib/matter-server` for us inside the + # ephemeral TemporaryFileSystem. + StateDirectory = storageDir; + # `python-matter-server` writes to /data even when a storage-path is + # specified. This bind-mount points /data at the systemd-managed + # /var/lib/matter-server, so all files get dropped into the state + # directory. + BindPaths = "${storagePath}:/data"; + + # Hardening bits + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + DynamicUser = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallFilter = concatStringsSep " " [ + "~" # Blocklist + "@clock" + "@cpu-emulation" + "@debug" + "@module" + "@mount" + "@obsolete" + "@privileged" + "@raw-io" + "@reboot" + "@resources" + "@swap" + ]; + UMask = "0077"; + }; + }; + }; +} + diff --git a/nixos/modules/services/mail/stalwart-mail.nix b/nixos/modules/services/mail/stalwart-mail.nix index f576a426b318f..8ab3497f7a175 100644 --- a/nixos/modules/services/mail/stalwart-mail.nix +++ b/nixos/modules/services/mail/stalwart-mail.nix @@ -32,10 +32,15 @@ in { global.tracing.level = mkDefault "info"; queue.path = mkDefault "${dataDir}/queue"; report.path = mkDefault "${dataDir}/reports"; + store.db.type = mkDefault "sqlite"; store.db.path = mkDefault "${dataDir}/data/index.sqlite3"; - store.blob.type = mkDefault "local"; - store.blob.local.path = mkDefault "${dataDir}/data/blobs"; + store.blob.type = mkDefault "fs"; + store.blob.path = mkDefault "${dataDir}/data/blobs"; + storage.data = mkDefault "db"; + storage.fts = mkDefault "db"; + storage.blob = mkDefault "blob"; resolver.type = mkDefault "system"; + resolver.public-suffix = mkDefault ["https://publicsuffix.org/list/public_suffix_list.dat"]; }; systemd.services.stalwart-mail = { @@ -57,8 +62,8 @@ in { KillSignal = "SIGINT"; Restart = "on-failure"; RestartSec = 5; - StandardOutput = "syslog"; - StandardError = "syslog"; + StandardOutput = "journal"; + StandardError = "journal"; SyslogIdentifier = "stalwart-mail"; DynamicUser = true; diff --git a/nixos/modules/services/matrix/synapse.md b/nixos/modules/services/matrix/synapse.md index f270be8c8d781..9c9c025fc5f54 100644 --- a/nixos/modules/services/matrix/synapse.md +++ b/nixos/modules/services/matrix/synapse.md @@ -126,8 +126,9 @@ then enable `services.matrix-synapse.settings.enable_registration = true;`. Otherwise, or you can generate a registration secret with {command}`pwgen -s 64 1` and set it with [](#opt-services.matrix-synapse.settings.registration_shared_secret). -To create a new user or admin, run the following after you have set the secret -and have rebuilt NixOS: +To create a new user or admin from the terminal your client listener +must be configured to use TCP sockets. Then you can run the following +after you have set the secret and have rebuilt NixOS: ```ShellSession $ nix-shell -p matrix-synapse $ register_new_matrix_user -k your-registration-shared-secret http://localhost:8008 diff --git a/nixos/modules/services/matrix/synapse.nix b/nixos/modules/services/matrix/synapse.nix index 4c1c396eac056..e3f9c7742cc7d 100644 --- a/nixos/modules/services/matrix/synapse.nix +++ b/nixos/modules/services/matrix/synapse.nix @@ -6,8 +6,16 @@ let cfg = config.services.matrix-synapse; format = pkgs.formats.yaml { }; + filterRecursiveNull = o: + if isAttrs o then + mapAttrs (_: v: filterRecursiveNull v) (filterAttrs (_: v: v != null) o) + else if isList o then + map filterRecursiveNull (filter (v: v != null) o) + else + o; + # remove null values from the final configuration - finalSettings = lib.filterAttrsRecursive (_: v: v != null) cfg.settings; + finalSettings = filterRecursiveNull cfg.settings; configFile = format.generate "homeserver.yaml" finalSettings; usePostgresql = cfg.settings.database.name == "psycopg2"; @@ -105,6 +113,19 @@ let SYSLOG_IDENTIFIER = logName; }; }); + + toIntBase8 = str: + lib.pipe str [ + lib.stringToCharacters + (map lib.toInt) + (lib.foldl (acc: digit: acc * 8 + digit) 0) + ]; + + toDecimalFilePermission = value: + if value == null then + null + else + toIntBase8 value; in { imports = [ @@ -192,10 +213,11 @@ in { ]; options = let - listenerType = workerContext: types.submodule { + listenerType = workerContext: types.submodule ({ config, ... }: { options = { port = mkOption { - type = types.port; + type = types.nullOr types.port; + default = null; example = 8448; description = lib.mdDoc '' The port to listen for HTTP(S) requests on. @@ -203,11 +225,20 @@ in { }; bind_addresses = mkOption { - type = types.listOf types.str; - default = [ + type = types.nullOr (types.listOf types.str); + default = if config.path != null then null else [ "::1" "127.0.0.1" ]; + defaultText = literalExpression '' + if path != null then + null + else + [ + "::1" + "127.0.0.1" + ] + ''; example = literalExpression '' [ "::" @@ -219,6 +250,35 @@ in { ''; }; + path = mkOption { + type = types.nullOr types.path; + default = null; + description = '' + Unix domain socket path to bind this listener to. + + ::: {.note} + This option is incompatible with {option}`bind_addresses`, {option}`port`, {option}`tls` + and also does not support the `metrics` and `manhole` listener {option}`type`. + ::: + ''; + }; + + mode = mkOption { + type = types.nullOr (types.strMatching "^[0,2-7]{3,4}$"); + default = if config.path != null then "660" else null; + defaultText = literalExpression '' + if path != null then + "660" + else + null + ''; + example = "660"; + description = '' + File permissions on the UNIX domain socket. + ''; + apply = toDecimalFilePermission; + }; + type = mkOption { type = types.enum [ "http" @@ -234,17 +294,30 @@ in { }; tls = mkOption { - type = types.bool; - default = !workerContext; + type = types.nullOr types.bool; + default = if config.path != null then + null + else + !workerContext; + defaultText = '' + Enabled for the main instance listener, unless it is configured with a UNIX domain socket path. + ''; example = false; description = lib.mdDoc '' Whether to enable TLS on the listener socket. + + ::: {.note} + This option will be ignored for UNIX domain sockets. + ::: ''; }; x_forwarded = mkOption { type = types.bool; - default = false; + default = config.path != null; + defaultText = '' + Enabled if the listener is configured with a UNIX domain socket path + ''; example = true; description = lib.mdDoc '' Use the X-Forwarded-For (XFF) header as the client IP and not the @@ -291,11 +364,28 @@ in { ''; }; }; - }; + }); in { services.matrix-synapse = { enable = mkEnableOption (lib.mdDoc "matrix.org synapse"); + enableRegistrationScript = mkOption { + type = types.bool; + default = clientListener.bind_addresses != []; + example = false; + defaultText = '' + Enabled if the client listener uses TCP sockets + ''; + description = '' + Whether to install the `register_new_matrix_user` script, that + allows account creation on the terminal. + + ::: {.note} + This script does not work when the client listener uses UNIX domain sockets + ::: + ''; + }; + serviceUnit = lib.mkOption { type = lib.types.str; readOnly = true; @@ -616,11 +706,8 @@ in { compress = false; }]; }] ++ lib.optional hasWorkers { - port = 9093; - bind_addresses = [ "127.0.0.1" ]; + path = "/run/matrix-synapse/main_replication.sock"; type = "http"; - tls = false; - x_forwarded = false; resources = [{ names = [ "replication" ]; compress = false; @@ -630,7 +717,7 @@ in { List of ports that Synapse should listen on, their purpose and their configuration. By default, synapse will be configured for client and federation traffic on port 8008, and - for worker replication traffic on port 9093. See [`services.matrix-synapse.workers`](#opt-services.matrix-synapse.workers) + use a UNIX domain socket for worker replication. See [`services.matrix-synapse.workers`](#opt-services.matrix-synapse.workers) for more details. ''; }; @@ -1006,9 +1093,15 @@ in { listener = lib.findFirst ( listener: - listener.port == main.port + ( + lib.hasAttr "port" main && listener.port or null == main.port + || lib.hasAttr "path" main && listener.path or null == main.path + ) && listenerSupportsResource "replication" listener - && (lib.any (bind: bind == main.host || bind == "0.0.0.0" || bind == "::") listener.bind_addresses) + && ( + lib.hasAttr "host" main && lib.any (bind: bind == main.host || bind == "0.0.0.0" || bind == "::") listener.bind_addresses + || lib.hasAttr "path" main + ) ) null cfg.settings.listeners; @@ -1022,15 +1115,44 @@ in { This is done by default unless you manually configure either of those settings. ''; } - ]; + { + assertion = cfg.enableRegistrationScript -> clientListener.path == null; + message = '' + The client listener on matrix-synapse is configured to use UNIX domain sockets. + This configuration is incompatible with the `register_new_matrix_user` script. + + Disable `services.mastrix-synapse.enableRegistrationScript` to continue. + ''; + } + ] + ++ (map (listener: { + assertion = (listener.path == null) != (listener.bind_addresses == null); + message = '' + Listeners require either a UNIX domain socket `path` or `bind_addresses` for a TCP socket. + ''; + }) cfg.settings.listeners) + ++ (map (listener: { + assertion = listener.path != null -> (listener.bind_addresses == null && listener.port == null && listener.tls == null); + message = let + formatKeyValue = key: value: lib.optionalString (value != null) " - ${key}=${toString value}\n"; + in '' + Listener configured with UNIX domain socket (${toString listener.path}) ignores the following options: + ${formatKeyValue "bind_addresses" listener.bind_addresses}${formatKeyValue "port" listener.port}${formatKeyValue "tls" listener.tls} + ''; + }) cfg.settings.listeners) + ++ (map (listener: { + assertion = listener.path == null || listener.type == "http"; + message = '' + Listener configured with UNIX domain socket (${toString listener.path}) only supports the "http" listener type. + ''; + }) cfg.settings.listeners); services.matrix-synapse.settings.redis = lib.mkIf cfg.configureRedisLocally { enabled = true; path = config.services.redis.servers.matrix-synapse.unixSocket; }; services.matrix-synapse.settings.instance_map.main = lib.mkIf hasWorkers (lib.mkDefault { - host = "127.0.0.1"; - port = 9093; + path = "/run/matrix-synapse/main_replication.sock"; }); services.matrix-synapse.serviceUnit = if hasWorkers then "matrix-synapse.target" else "matrix-synapse.service"; @@ -1086,6 +1208,8 @@ in { User = "matrix-synapse"; Group = "matrix-synapse"; WorkingDirectory = cfg.dataDir; + RuntimeDirectory = "matrix-synapse"; + RuntimeDirectoryPreserve = true; ExecReload = "${pkgs.util-linux}/bin/kill -HUP $MAINPID"; Restart = "on-failure"; UMask = "0077"; @@ -1178,7 +1302,9 @@ in { user = "matrix-synapse"; }; - environment.systemPackages = [ registerNewMatrixUser ]; + environment.systemPackages = lib.optionals cfg.enableRegistrationScript [ + registerNewMatrixUser + ]; }; meta = { diff --git a/nixos/modules/services/misc/atuin.nix b/nixos/modules/services/misc/atuin.nix index 2d6ffc510ce55..7e89929884d6f 100644 --- a/nixos/modules/services/misc/atuin.nix +++ b/nixos/modules/services/misc/atuin.nix @@ -8,6 +8,8 @@ in services.atuin = { enable = lib.mkEnableOption (mdDoc "Atuin server for shell history sync"); + package = lib.mkPackageOption pkgs "atuin" { }; + openRegistration = mkOption { type = types.bool; default = false; @@ -52,10 +54,13 @@ in }; uri = mkOption { - type = types.str; + type = types.nullOr types.str; default = "postgresql:///atuin?host=/run/postgresql"; example = "postgresql://atuin@localhost:5432/atuin"; - description = mdDoc "URI to the database"; + description = mdDoc '' + URI to the database. + Can be set to null in which case ATUIN_DB_URI should be set through an EnvironmentFile + ''; }; }; }; @@ -85,7 +90,7 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { - ExecStart = "${pkgs.atuin}/bin/atuin server start"; + ExecStart = "${lib.getExe cfg.package} server start"; RuntimeDirectory = "atuin"; RuntimeDirectoryMode = "0700"; DynamicUser = true; @@ -132,9 +137,10 @@ in ATUIN_PORT = toString cfg.port; ATUIN_MAX_HISTORY_LENGTH = toString cfg.maxHistoryLength; ATUIN_OPEN_REGISTRATION = lib.boolToString cfg.openRegistration; - ATUIN_DB_URI = cfg.database.uri; ATUIN_PATH = cfg.path; ATUIN_CONFIG_DIR = "/run/atuin"; # required to start, but not used as configuration is via environment variables + } // lib.optionalAttrs (cfg.database.uri != null) { + ATUIN_DB_URI = cfg.database.uri; }; }; diff --git a/nixos/modules/services/misc/docker-registry.nix b/nixos/modules/services/misc/docker-registry.nix index e8fbc05423d31..78d1d6339ed65 100644 --- a/nixos/modules/services/misc/docker-registry.nix +++ b/nixos/modules/services/misc/docker-registry.nix @@ -63,6 +63,12 @@ in { type = types.port; }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Opens the port used by the firewall."; + }; + storagePath = mkOption { type = types.nullOr types.path; default = "/var/lib/docker-registry"; @@ -154,5 +160,9 @@ in { isSystemUser = true; }; users.groups.docker-registry = {}; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.port ]; + }; }; } diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix index d0135b2ba7acd..08feea853e470 100644 --- a/nixos/modules/services/misc/gitea.nix +++ b/nixos/modules/services/misc/gitea.nix @@ -681,6 +681,11 @@ in optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++ optional (cfg.extraConfig != null) '' services.gitea.`extraConfig` is deprecated, please use services.gitea.`settings`. + '' ++ + optional (lib.getName cfg.package == "forgejo") '' + Running forgejo via services.gitea.package is no longer supported. + Please use services.forgejo instead. + See https://nixos.org/manual/nixos/unstable/#module-forgejo for migration instructions. ''; # Create database passwordFile default when password is configured. diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index 6756d59cf367c..ec347a75f063e 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -1386,10 +1386,8 @@ in { systemd.services.gitlab-db-config = { after = [ "gitlab-config.service" "gitlab-postgresql.service" "postgresql.service" ]; - bindsTo = [ - "gitlab-config.service" - ] ++ optional (cfg.databaseHost == "") "postgresql.service" - ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service"; + wants = optional (cfg.databaseHost == "") "postgresql.service" ++ optional databaseActuallyCreateLocally "gitlab-postgresql.service"; + bindsTo = [ "gitlab-config.service" ]; wantedBy = [ "gitlab.target" ]; partOf = [ "gitlab.target" ]; serviceConfig = { @@ -1422,10 +1420,10 @@ in { "gitlab-db-config.service" ]; bindsTo = [ - "redis-gitlab.service" "gitlab-config.service" "gitlab-db-config.service" - ] ++ optional (cfg.databaseHost == "") "postgresql.service"; + ]; + wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service"; wantedBy = [ "gitlab.target" ]; partOf = [ "gitlab.target" ]; environment = gitlabEnv // (optionalAttrs cfg.sidekiq.memoryKiller.enable { @@ -1612,10 +1610,10 @@ in { "gitlab-db-config.service" ]; bindsTo = [ - "redis-gitlab.service" "gitlab-config.service" "gitlab-db-config.service" - ] ++ optional (cfg.databaseHost == "") "postgresql.service"; + ]; + wants = [ "redis-gitlab.service" ] ++ optional (cfg.databaseHost == "") "postgresql.service"; requiredBy = [ "gitlab.target" ]; partOf = [ "gitlab.target" ]; environment = gitlabEnv; diff --git a/nixos/modules/services/misc/jellyfin.nix b/nixos/modules/services/misc/jellyfin.nix index 7042b491ffa4e..a1d3910bd93b0 100644 --- a/nixos/modules/services/misc/jellyfin.nix +++ b/nixos/modules/services/misc/jellyfin.nix @@ -1,109 +1,149 @@ { config, pkgs, lib, ... }: -with lib; - let + inherit (lib) mkIf getExe maintainers mkEnableOption mkOption mkPackageOption; + inherit (lib.types) str path bool; cfg = config.services.jellyfin; in { options = { services.jellyfin = { - enable = mkEnableOption (lib.mdDoc "Jellyfin Media Server"); + enable = mkEnableOption "Jellyfin Media Server"; + + package = mkPackageOption pkgs "jellyfin" { }; user = mkOption { - type = types.str; + type = str; default = "jellyfin"; - description = lib.mdDoc "User account under which Jellyfin runs."; + description = "User account under which Jellyfin runs."; }; - package = mkPackageOption pkgs "jellyfin" { }; - group = mkOption { - type = types.str; + type = str; default = "jellyfin"; - description = lib.mdDoc "Group under which jellyfin runs."; + description = "Group under which jellyfin runs."; + }; + + dataDir = mkOption { + type = path; + default = "/var/lib/jellyfin"; + description = '' + Base data directory, + passed with `--datadir` see [#data-directory](https://jellyfin.org/docs/general/administration/configuration/#data-directory) + ''; + }; + + configDir = mkOption { + type = path; + default = "${cfg.dataDir}/config"; + defaultText = "\${cfg.dataDir}/config"; + description = '' + Directory containing the server configuration files, + passed with `--configdir` see [configuration-directory](https://jellyfin.org/docs/general/administration/configuration/#configuration-directory) + ''; + }; + + cacheDir = mkOption { + type = path; + default = "/var/cache/jellyfin"; + description = '' + Directory containing the jellyfin server cache, + passed with `--cachedir` see [#cache-directory](https://jellyfin.org/docs/general/administration/configuration/#cache-directory) + ''; + }; + + logDir = mkOption { + type = path; + default = "${cfg.dataDir}/log"; + defaultText = "\${cfg.dataDir}/log"; + description = '' + Directory where the Jellyfin logs will be stored, + passed with `--logdir` see [#log-directory](https://jellyfin.org/docs/general/administration/configuration/#log-directory) + ''; }; openFirewall = mkOption { - type = types.bool; + type = bool; default = false; - description = lib.mdDoc '' + description = '' Open the default ports in the firewall for the media server. The HTTP/HTTPS ports can be changed in the Web UI, so this option should - only be used if they are unchanged. + only be used if they are unchanged, see [Port Bindings](https://jellyfin.org/docs/general/networking/#port-bindings). ''; }; }; }; config = mkIf cfg.enable { - systemd.services.jellyfin = { - description = "Jellyfin Media Server"; - after = [ "network-online.target" ]; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - - # This is mostly follows: https://github.com/jellyfin/jellyfin/blob/master/fedora/jellyfin.service - # Upstream also disable some hardenings when running in LXC, we do the same with the isContainer option - serviceConfig = rec { - Type = "simple"; - User = cfg.user; - Group = cfg.group; - StateDirectory = "jellyfin"; - StateDirectoryMode = "0700"; - CacheDirectory = "jellyfin"; - CacheDirectoryMode = "0700"; - UMask = "0077"; - WorkingDirectory = "/var/lib/jellyfin"; - ExecStart = "${cfg.package}/bin/jellyfin --datadir '/var/lib/${StateDirectory}' --cachedir '/var/cache/${CacheDirectory}'"; - Restart = "on-failure"; - TimeoutSec = 15; - SuccessExitStatus = ["0" "143"]; - - # Security options: - NoNewPrivileges = true; - SystemCallArchitectures = "native"; - # AF_NETLINK needed because Jellyfin monitors the network connection - RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ]; - RestrictNamespaces = !config.boot.isContainer; - RestrictRealtime = true; - RestrictSUIDSGID = true; - ProtectControlGroups = !config.boot.isContainer; - ProtectHostname = true; - ProtectKernelLogs = !config.boot.isContainer; - ProtectKernelModules = !config.boot.isContainer; - ProtectKernelTunables = !config.boot.isContainer; - LockPersonality = true; - PrivateTmp = !config.boot.isContainer; - # needed for hardware acceleration - PrivateDevices = false; - PrivateUsers = true; - RemoveIPC = true; - - SystemCallFilter = [ - "~@clock" - "~@aio" - "~@chown" - "~@cpu-emulation" - "~@debug" - "~@keyring" - "~@memlock" - "~@module" - "~@mount" - "~@obsolete" - "~@privileged" - "~@raw-io" - "~@reboot" - "~@setuid" - "~@swap" - ]; - SystemCallErrorNumber = "EPERM"; + systemd = { + tmpfiles.settings.jellyfinDirs = { + "${cfg.dataDir}"."d" = { + mode = "700"; + inherit (cfg) user group; + }; + "${cfg.configDir}"."d" = { + mode = "700"; + inherit (cfg) user group; + }; + "${cfg.logDir}"."d" = { + mode = "700"; + inherit (cfg) user group; + }; + "${cfg.cacheDir}"."d" = { + mode = "700"; + inherit (cfg) user group; + }; + }; + services.jellyfin = { + description = "Jellyfin Media Server"; + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + # This is mostly follows: https://github.com/jellyfin/jellyfin/blob/master/fedora/jellyfin.service + # Upstream also disable some hardenings when running in LXC, we do the same with the isContainer option + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + UMask = "0077"; + WorkingDirectory = cfg.dataDir; + ExecStart = "${getExe cfg.package} --datadir '${cfg.dataDir}' --configdir '${cfg.configDir}' --cachedir '${cfg.cacheDir}' --logdir '${cfg.logDir}'"; + Restart = "on-failure"; + TimeoutSec = 15; + SuccessExitStatus = ["0" "143"]; + + # Security options: + NoNewPrivileges = true; + SystemCallArchitectures = "native"; + # AF_NETLINK needed because Jellyfin monitors the network connection + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" "AF_NETLINK" ]; + RestrictNamespaces = !config.boot.isContainer; + RestrictRealtime = true; + RestrictSUIDSGID = true; + ProtectControlGroups = !config.boot.isContainer; + ProtectHostname = true; + ProtectKernelLogs = !config.boot.isContainer; + ProtectKernelModules = !config.boot.isContainer; + ProtectKernelTunables = !config.boot.isContainer; + LockPersonality = true; + PrivateTmp = !config.boot.isContainer; + # needed for hardware acceleration + PrivateDevices = false; + PrivateUsers = true; + RemoveIPC = true; + + SystemCallFilter = [ + "~@clock" "~@aio" "~@chown" "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@module" "~@mount" "~@obsolete" "~@privileged" "~@raw-io" "~@reboot" "~@setuid" "~@swap" + ]; + SystemCallErrorNumber = "EPERM"; + }; }; }; users.users = mkIf (cfg.user == "jellyfin") { jellyfin = { - group = cfg.group; + inherit (cfg) group; isSystemUser = true; }; }; @@ -120,5 +160,5 @@ in }; - meta.maintainers = with lib.maintainers; [ minijackson ]; + meta.maintainers = with maintainers; [ minijackson nu-nu-ko ]; } diff --git a/nixos/modules/services/misc/nix-gc.nix b/nixos/modules/services/misc/nix-gc.nix index de6bd76c7eb9d..656cbad813734 100644 --- a/nixos/modules/services/misc/nix-gc.nix +++ b/nixos/modules/services/misc/nix-gc.nix @@ -64,8 +64,7 @@ in example = "--max-freed $((64 * 1024**3))"; type = lib.types.singleLineStr; description = lib.mdDoc '' - Options given to {file}`nix-collect-garbage` when the - garbage collector is run automatically. + Options given to [`nix-collect-garbage`](https://nixos.org/manual/nix/stable/command-ref/nix-collect-garbage) when the garbage collector is run automatically. ''; }; diff --git a/nixos/modules/services/misc/ollama.nix b/nixos/modules/services/misc/ollama.nix index d9359d2b5cd44..3ac3beb4de078 100644 --- a/nixos/modules/services/misc/ollama.nix +++ b/nixos/modules/services/misc/ollama.nix @@ -1,27 +1,44 @@ -{ config, lib, pkgs, ... }: let +{ config, lib, pkgs, ... }: +let + inherit (lib) types; cfg = config.services.ollama; - -in { - + ollamaPackage = cfg.package.override { + inherit (cfg) acceleration; + linuxPackages = config.boot.kernelPackages // { + nvidia_x11 = config.hardware.nvidia.package; + }; + }; +in +{ options = { services.ollama = { enable = lib.mkEnableOption ( lib.mdDoc "Server for local large language models" ); listenAddress = lib.mkOption { - type = lib.types.str; + type = types.str; default = "127.0.0.1:11434"; description = lib.mdDoc '' Specifies the bind address on which the ollama server HTTP interface listens. ''; }; + acceleration = lib.mkOption { + type = types.nullOr (types.enum [ "rocm" "cuda" ]); + default = null; + example = "rocm"; + description = lib.mdDoc '' + Specifies the interface to use for hardware acceleration. + + - `rocm`: supported by modern AMD GPUs + - `cuda`: supported by modern NVIDIA GPUs + ''; + }; package = lib.mkPackageOption pkgs "ollama" { }; }; }; config = lib.mkIf cfg.enable { - systemd = { services.ollama = { wantedBy = [ "multi-user.target" ]; @@ -33,7 +50,7 @@ in { OLLAMA_HOST = cfg.listenAddress; }; serviceConfig = { - ExecStart = "${lib.getExe cfg.package} serve"; + ExecStart = "${lib.getExe ollamaPackage} serve"; WorkingDirectory = "/var/lib/ollama"; StateDirectory = [ "ollama" ]; DynamicUser = true; @@ -41,10 +58,8 @@ in { }; }; - environment.systemPackages = [ cfg.package ]; - + environment.systemPackages = [ ollamaPackage ]; }; - meta.maintainers = with lib.maintainers; [ onny ]; - + meta.maintainers = with lib.maintainers; [ abysssol onny ]; } diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix index 1256d8315c8b7..ab042e4b6ee2a 100644 --- a/nixos/modules/services/misc/paperless.nix +++ b/nixos/modules/services/misc/paperless.nix @@ -307,6 +307,9 @@ in Restart = "on-failure"; }; environment = env; + # Allow the consumer to access the private /tmp directory of the server. + # This is required to support consuming files via a local folder. + unitConfig.JoinsNamespaceOf = "paperless-task-queue.service"; }; systemd.services.paperless-web = { diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix index aa803d3bb6939..557d6d7e71683 100644 --- a/nixos/modules/services/misc/sourcehut/default.nix +++ b/nixos/modules/services/misc/sourcehut/default.nix @@ -790,13 +790,21 @@ in ''; }; systemd.tmpfiles.settings."10-sourcehut-gitsrht" = mkIf cfg.git.enable ( - builtins.listToAttrs (map (name: { - name = "/var/log/sourcehut/gitsrht-${name}"; - value.f = { - inherit (cfg.git) user group; - mode = "0644"; - }; - }) [ "keys" "shell" "update-hook" ]) + mkMerge [ + (builtins.listToAttrs (map (name: { + name = "/var/log/sourcehut/gitsrht-${name}"; + value.f = { + inherit (cfg.git) user group; + mode = "0644"; + }; + }) [ "keys" "shell" "update-hook" ])) + { + ${cfg.settings."git.sr.ht".repos}.d = { + inherit (cfg.git) user group; + mode = "0644"; + }; + } + ] ); systemd.services.sshd = { preStart = mkIf cfg.hg.enable '' @@ -1370,5 +1378,5 @@ in ]; meta.doc = ./default.md; - meta.maintainers = with maintainers; [ tomberek nessdoor ]; + meta.maintainers = with maintainers; [ tomberek nessdoor christoph-heiss ]; } diff --git a/nixos/modules/services/misc/tandoor-recipes.nix b/nixos/modules/services/misc/tandoor-recipes.nix index 6c51a9bb85550..a8300ecd52337 100644 --- a/nixos/modules/services/misc/tandoor-recipes.nix +++ b/nixos/modules/services/misc/tandoor-recipes.nix @@ -17,14 +17,11 @@ let lib.mapAttrs (_: toString) cfg.extraConfig ); - manage = - let - setupEnv = lib.concatStringsSep "\n" (mapAttrsToList (name: val: "export ${name}=\"${val}\"") env); - in - pkgs.writeShellScript "manage" '' - ${setupEnv} - exec ${pkg}/bin/tandoor-recipes "$@" - ''; + manage = pkgs.writeShellScript "manage" '' + set -o allexport # Export the following env vars + ${lib.toShellVars env} + exec ${pkg}/bin/tandoor-recipes "$@" + ''; in { meta.maintainers = with maintainers; [ ambroisie ]; diff --git a/nixos/modules/services/misc/transfer-sh.nix b/nixos/modules/services/misc/transfer-sh.nix new file mode 100644 index 0000000000000..899d9dfc3c108 --- /dev/null +++ b/nixos/modules/services/misc/transfer-sh.nix @@ -0,0 +1,102 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.transfer-sh; + inherit (lib) + mkDefault mkEnableOption mkPackageOption mkIf mkOption + types mapAttrs isBool getExe boolToString mdDoc optionalAttrs; +in +{ + options.services.transfer-sh = { + enable = mkEnableOption (mdDoc "Easy and fast file sharing from the command-line"); + + package = mkPackageOption pkgs "transfer-sh" { }; + + settings = mkOption { + type = types.submodule { freeformType = with types; attrsOf (oneOf [ bool int str ]); }; + default = { }; + example = { + LISTENER = ":8080"; + BASEDIR = "/var/lib/transfer.sh"; + TLS_LISTENER_ONLY = false; + }; + description = mdDoc '' + Additional configuration for transfer-sh, see + <https://github.com/dutchcoders/transfer.sh#usage-1> + for supported values. + + For secrets use secretFile option instead. + ''; + }; + + provider = mkOption { + type = types.enum [ "local" "s3" "storj" "gdrive" ]; + default = "local"; + description = mdDoc "Storage providers to use"; + }; + + secretFile = mkOption { + type = types.nullOr types.path; + default = null; + example = "/run/secrets/transfer-sh.env"; + description = mdDoc '' + Path to file containing environment variables. + Useful for passing down secrets. + Some variables that can be considered secrets are: + - AWS_ACCESS_KEY + - AWS_ACCESS_KEY + - TLS_PRIVATE_KEY + - HTTP_AUTH_HTPASSWD + ''; + }; + }; + + config = + let + localProvider = (cfg.provider == "local"); + stateDirectory = "/var/lib/transfer.sh"; + in + mkIf cfg.enable + { + services.transfer-sh.settings = { + LISTENER = mkDefault ":8080"; + } // optionalAttrs localProvider { + BASEDIR = mkDefault stateDirectory; + }; + + systemd.services.transfer-sh = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = mapAttrs (_: v: if isBool v then boolToString v else toString v) cfg.settings; + serviceConfig = { + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + DevicePolicy = "closed"; + DynamicUser = true; + ExecStart = "${getExe cfg.package} --provider ${cfg.provider}"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = [ "native" ]; + SystemCallFilter = [ "@system-service" ]; + StateDirectory = baseNameOf stateDirectory; + } // optionalAttrs (cfg.secretFile != null) { + EnvironmentFile = cfg.secretFile; + } // optionalAttrs localProvider { + ReadWritePaths = cfg.settings.BASEDIR; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ ocfox ]; +} diff --git a/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix b/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix index 36409caccf2e3..2a8b7fc0818d5 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/fastly.nix @@ -1,41 +1,54 @@ -{ config, lib, pkgs, options }: +{ config +, lib +, pkgs +, options +}: -with lib; +let + inherit (lib) + escapeShellArgs + mkOption + optionals + types + ; -let cfg = config.services.prometheus.exporters.fastly; + cfg = config.services.prometheus.exporters.fastly; in { port = 9118; - extraOpts = { - debug = mkEnableOption (lib.mdDoc "Debug logging mode for fastly-exporter"); - + extraOpts = with types; { configFile = mkOption { - type = types.nullOr types.path; + type = nullOr path; default = null; - description = lib.mdDoc '' + example = "./fastly-exporter-config.txt"; + description = '' Path to a fastly-exporter configuration file. Example one can be generated with `fastly-exporter --config-file-example`. ''; - example = "./fastly-exporter-config.txt"; }; tokenPath = mkOption { - type = types.nullOr types.path; - apply = final: if final == null then null else toString final; - description = lib.mdDoc '' + type = path; + description = '' A run-time path to the token file, which is supposed to be provisioned outside of Nix store. ''; }; }; serviceOpts = { - script = '' - ${optionalString (cfg.tokenPath != null) - "export FASTLY_API_TOKEN=$(cat ${toString cfg.tokenPath})"} - ${pkgs.prometheus-fastly-exporter}/bin/fastly-exporter \ - -listen http://${cfg.listenAddress}:${toString cfg.port} - ${optionalString cfg.debug "-debug true"} \ - ${optionalString (cfg.configFile != null) "-config-file ${cfg.configFile}"} + serviceConfig = { + LoadCredential = "fastly-api-token:${cfg.tokenPath}"; + }; + script = let + call = escapeShellArgs ([ + "${pkgs.prometheus-fastly-exporter}/bin/fastly-exporter" + "-listen" "${cfg.listenAddress}:${toString cfg.port}" + ] ++ optionals (cfg.configFile != null) [ + "--config-file" cfg.configFile + ] ++ cfg.extraFlags); + in '' + export FASTLY_API_TOKEN="$(cat $CREDENTIALS_DIRECTORY/fastly-api-token)" + ${call} ''; }; } diff --git a/nixos/modules/services/monitoring/prometheus/exporters/nut.nix b/nixos/modules/services/monitoring/prometheus/exporters/nut.nix index 1c86b48b4509b..e58a394456a38 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/nut.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/nut.nix @@ -36,6 +36,17 @@ in provisioned outside of Nix store. ''; }; + nutVariables = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + List of NUT variable names to monitor. + + If no variables are set, all numeric variables will be exported automatically. + See the [upstream docs](https://github.com/DRuggeri/nut_exporter?tab=readme-ov-file#variables-and-information) + for more information. + ''; + }; }; serviceOpts = { script = '' @@ -44,7 +55,9 @@ in ${pkgs.prometheus-nut-exporter}/bin/nut_exporter \ --nut.server=${cfg.nutServer} \ --web.listen-address="${cfg.listenAddress}:${toString cfg.port}" \ - ${optionalString (cfg.nutUser != "") "--nut.username=${cfg.nutUser}"} + ${optionalString (cfg.nutUser != "") "--nut.username=${cfg.nutUser}"} \ + ${optionalString (cfg.nutVariables != []) "--nut.vars_enable=${concatStringsSep "," cfg.nutVariables}"} \ + ${concatStringsSep " " cfg.extraFlags} ''; }; } diff --git a/nixos/modules/services/monitoring/rustdesk-server.nix b/nixos/modules/services/monitoring/rustdesk-server.nix index 0a6a8e71672fd..fcfd57167dd8f 100644 --- a/nixos/modules/services/monitoring/rustdesk-server.nix +++ b/nixos/modules/services/monitoring/rustdesk-server.nix @@ -24,6 +24,24 @@ in { The public facing IP of the RustDesk relay. ''; }; + + extraSignalArgs = mkOption { + type = listOf str; + default = []; + example = [ "-k" "_" ]; + description = '' + A list of extra command line arguments to pass to the `hbbs` process. + ''; + }; + + extraRelayArgs = mkOption { + type = listOf str; + default = []; + example = [ "-k" "_" ]; + description = '' + A list of extra command line arguments to pass to the `hbbr` process. + ''; + }; }; config = let @@ -83,11 +101,11 @@ in { }; systemd.services.rustdesk-signal = lib.mkMerge [ serviceDefaults { - serviceConfig.ExecStart = "${cfg.package}/bin/hbbs -r ${cfg.relayIP}"; + serviceConfig.ExecStart = "${cfg.package}/bin/hbbs -r ${cfg.relayIP} ${lib.escapeShellArgs cfg.extraSignalArgs}"; } ]; systemd.services.rustdesk-relay = lib.mkMerge [ serviceDefaults { - serviceConfig.ExecStart = "${cfg.package}/bin/hbbr"; + serviceConfig.ExecStart = "${cfg.package}/bin/hbbr ${lib.escapeShellArgs cfg.extraRelayArgs}"; } ]; }; diff --git a/nixos/modules/services/monitoring/scrutiny.nix b/nixos/modules/services/monitoring/scrutiny.nix new file mode 100644 index 0000000000000..454668a9a128d --- /dev/null +++ b/nixos/modules/services/monitoring/scrutiny.nix @@ -0,0 +1,221 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.scrutiny; + # Define the settings format used for this program + settingsFormat = pkgs.formats.yaml { }; +in +{ + options = { + services.scrutiny = { + enable = lib.mkEnableOption "Enables the scrutiny web application."; + + package = lib.mkPackageOptionMD pkgs "scrutiny" { }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Open the default ports in the firewall for Scrutiny."; + }; + + influxdb.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = lib.mdDoc '' + Enables InfluxDB on the host system using the `services.influxdb2` NixOS module + with default options. + + If you already have InfluxDB configured, or wish to connect to an external InfluxDB + instance, disable this option. + ''; + }; + + settings = lib.mkOption { + description = lib.mdDoc '' + Scrutiny settings to be rendered into the configuration file. + + See https://github.com/AnalogJ/scrutiny/blob/master/example.scrutiny.yaml. + ''; + default = { }; + type = lib.types.submodule { + freeformType = settingsFormat.type; + + options.web.listen.port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = lib.mdDoc "Port for web application to listen on."; + }; + + options.web.listen.host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = lib.mdDoc "Interface address for web application to bind to."; + }; + + options.web.listen.basepath = lib.mkOption { + type = lib.types.str; + default = ""; + example = "/scrutiny"; + description = lib.mdDoc '' + If Scrutiny will be behind a path prefixed reverse proxy, you can override this + value to serve Scrutiny on a subpath. + ''; + }; + + options.log.level = lib.mkOption { + type = lib.types.enum [ "INFO" "DEBUG" ]; + default = "INFO"; + description = lib.mdDoc "Log level for Scrutiny."; + }; + + options.web.influxdb.scheme = lib.mkOption { + type = lib.types.str; + default = "http"; + description = lib.mdDoc "URL scheme to use when connecting to InfluxDB."; + }; + + options.web.influxdb.host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = lib.mdDoc "IP or hostname of the InfluxDB instance."; + }; + + options.web.influxdb.port = lib.mkOption { + type = lib.types.port; + default = 8086; + description = lib.mdDoc "The port of the InfluxDB instance."; + }; + + options.web.influxdb.tls.insecure_skip_verify = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc "Skip TLS verification when connecting to InfluxDB."; + }; + + options.web.influxdb.token = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc "Authentication token for connecting to InfluxDB."; + }; + + options.web.influxdb.org = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc "InfluxDB organisation under which to store data."; + }; + + options.web.influxdb.bucket = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc "InfluxDB bucket in which to store data."; + }; + }; + }; + + collector = { + enable = lib.mkEnableOption "Enables the scrutiny metrics collector."; + + package = lib.mkPackageOptionMD pkgs "scrutiny-collector" { }; + + schedule = lib.mkOption { + type = lib.types.str; + default = "*:0/15"; + description = lib.mdDoc '' + How often to run the collector in systemd calendar format. + ''; + }; + + settings = lib.mkOption { + description = lib.mdDoc '' + Collector settings to be rendered into the collector configuration file. + + See https://github.com/AnalogJ/scrutiny/blob/master/example.collector.yaml. + ''; + default = { }; + type = lib.types.submodule { + freeformType = settingsFormat.type; + + options.host.id = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc "Host ID for identifying/labelling groups of disks"; + }; + + options.api.endpoint = lib.mkOption { + type = lib.types.str; + default = "http://localhost:8080"; + description = lib.mdDoc "Scrutiny app API endpoint for sending metrics to."; + }; + + options.log.level = lib.mkOption { + type = lib.types.enum [ "INFO" "DEBUG" ]; + default = "INFO"; + description = lib.mdDoc "Log level for Scrutiny collector."; + }; + }; + }; + }; + }; + }; + + config = lib.mkIf (cfg.enable || cfg.collector.enable) { + services.influxdb2.enable = cfg.influxdb.enable; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.settings.web.listen.port ]; + }; + + services.smartd = lib.mkIf cfg.collector.enable { + enable = true; + extraOptions = [ + "-A /var/log/smartd/" + "--interval=600" + ]; + }; + + systemd = { + services = { + scrutiny = lib.mkIf cfg.enable { + description = "Hard Drive S.M.A.R.T Monitoring, Historical Trends & Real World Failure Thresholds"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = { + SCRUTINY_VERSION = "1"; + SCRUTINY_WEB_DATABASE_LOCATION = "/var/lib/scrutiny/scrutiny.db"; + SCRUTINY_WEB_SRC_FRONTEND_PATH = "${cfg.package}/share/scrutiny"; + }; + serviceConfig = { + DynamicUser = true; + ExecStart = "${lib.getExe cfg.package} start --config ${settingsFormat.generate "scrutiny.yaml" cfg.settings}"; + Restart = "always"; + StateDirectory = "scrutiny"; + StateDirectoryMode = "0750"; + }; + }; + + scrutiny-collector = lib.mkIf cfg.collector.enable { + description = "Scrutiny Collector Service"; + environment = { + COLLECTOR_VERSION = "1"; + COLLECTOR_API_ENDPOINT = cfg.collector.settings.api.endpoint; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${lib.getExe cfg.collector.package} run --config ${settingsFormat.generate "scrutiny-collector.yaml" cfg.collector.settings}"; + }; + }; + }; + + timers = lib.mkIf cfg.collector.enable { + scrutiny-collector = { + timerConfig = { + OnCalendar = cfg.collector.schedule; + Persistent = true; + Unit = "scrutiny-collector.service"; + }; + }; + }; + }; + }; + + meta.maintainers = [ lib.maintainers.jnsgruk ]; +} diff --git a/nixos/modules/services/networking/bee-clef.nix b/nixos/modules/services/networking/bee-clef.nix deleted file mode 100644 index 75e76f019a71f..0000000000000 --- a/nixos/modules/services/networking/bee-clef.nix +++ /dev/null @@ -1,107 +0,0 @@ -{ config, lib, pkgs, ... }: - -# NOTE for now nothing is installed into /etc/bee-clef/. the config files are used as read-only from the nix store. - -with lib; -let - cfg = config.services.bee-clef; -in { - meta = { - maintainers = with maintainers; [ attila-lendvai ]; - }; - - ### interface - - options = { - services.bee-clef = { - enable = mkEnableOption (lib.mdDoc "clef external signer instance for Ethereum Swarm Bee"); - - dataDir = mkOption { - type = types.nullOr types.str; - default = "/var/lib/bee-clef"; - description = lib.mdDoc '' - Data dir for bee-clef. Beware that some helper scripts may not work when changed! - The service itself should work fine, though. - ''; - }; - - passwordFile = mkOption { - type = types.nullOr types.str; - default = "/var/lib/bee-clef/password"; - description = lib.mdDoc "Password file for bee-clef."; - }; - - user = mkOption { - type = types.str; - default = "bee-clef"; - description = lib.mdDoc '' - User the bee-clef daemon should execute under. - ''; - }; - - group = mkOption { - type = types.str; - default = "bee-clef"; - description = lib.mdDoc '' - Group the bee-clef daemon should execute under. - ''; - }; - }; - }; - - ### implementation - - config = mkIf cfg.enable { - # if we ever want to have rules.js under /etc/bee-clef/ - # environment.etc."bee-clef/rules.js".source = ${pkgs.bee-clef}/rules.js - - systemd.packages = [ pkgs.bee-clef ]; # include the upstream bee-clef.service file - - systemd.tmpfiles.rules = [ - "d '${cfg.dataDir}/' 0750 ${cfg.user} ${cfg.group}" - "d '${cfg.dataDir}/keystore' 0700 ${cfg.user} ${cfg.group}" - ]; - - systemd.services.bee-clef = { - path = [ - # these are needed for the ensure-clef-account script - pkgs.coreutils - pkgs.gnused - pkgs.gawk - ]; - - wantedBy = [ "bee.service" "multi-user.target" ]; - - serviceConfig = { - User = cfg.user; - Group = cfg.group; - ExecStartPre = ''${pkgs.bee-clef}/share/bee-clef/ensure-clef-account "${cfg.dataDir}" "${pkgs.bee-clef}/share/bee-clef/"''; - ExecStart = [ - "" # this hides/overrides what's in the original entry - "${pkgs.bee-clef}/share/bee-clef/bee-clef-service start" - ]; - ExecStop = [ - "" # this hides/overrides what's in the original entry - "${pkgs.bee-clef}/share/bee-clef/bee-clef-service stop" - ]; - Environment = [ - "CONFIGDIR=${cfg.dataDir}" - "PASSWORD_FILE=${cfg.passwordFile}" - ]; - }; - }; - - users.users = optionalAttrs (cfg.user == "bee-clef") { - bee-clef = { - group = cfg.group; - home = cfg.dataDir; - isSystemUser = true; - description = "Daemon user for the bee-clef service"; - }; - }; - - users.groups = optionalAttrs (cfg.group == "bee-clef") { - bee-clef = {}; - }; - }; -} diff --git a/nixos/modules/services/networking/bee.nix b/nixos/modules/services/networking/bee.nix index 962cfd30c3fe9..a4d20494bf6b9 100644 --- a/nixos/modules/services/networking/bee.nix +++ b/nixos/modules/services/networking/bee.nix @@ -8,7 +8,7 @@ let in { meta = { # doc = ./bee.xml; - maintainers = with maintainers; [ attila-lendvai ]; + maintainers = with maintainers; [ ]; }; ### interface @@ -73,13 +73,10 @@ in { } ]; - warnings = optional (! config.services.bee-clef.enable) "The bee service requires an external signer. Consider setting `config.services.bee-clef.enable` = true"; - services.bee.settings = { data-dir = lib.mkDefault "/var/lib/bee"; password-file = lib.mkDefault "/var/lib/bee/password"; clef-signer-enable = lib.mkDefault true; - clef-signer-endpoint = lib.mkDefault "/var/lib/bee-clef/clef.ipc"; swap-endpoint = lib.mkDefault "https://rpc.slock.it/goerli"; }; @@ -90,9 +87,6 @@ in { ]; systemd.services.bee = { - requires = optional config.services.bee-clef.enable - "bee-clef.service"; - wantedBy = [ "multi-user.target" ]; serviceConfig = { @@ -120,7 +114,6 @@ Bee has SWAP enabled by default and it needs ethereum endpoint to operate. It is recommended to use external signer with bee. Check documentation for more info: - SWAP https://docs.ethswarm.org/docs/installation/manual#swap-bandwidth-incentives -- External signer https://docs.ethswarm.org/docs/installation/bee-clef After you finish configuration run 'sudo bee-get-addr'." fi @@ -133,8 +126,6 @@ After you finish configuration run 'sudo bee-get-addr'." home = cfg.settings.data-dir; isSystemUser = true; description = "Daemon user for Ethereum Swarm Bee"; - extraGroups = optional config.services.bee-clef.enable - config.services.bee-clef.group; }; }; diff --git a/nixos/modules/services/networking/cloudflared.nix b/nixos/modules/services/networking/cloudflared.nix index 80c60fdb80137..b9556bfa60d06 100644 --- a/nixos/modules/services/networking/cloudflared.nix +++ b/nixos/modules/services/networking/cloudflared.nix @@ -276,9 +276,11 @@ in ingressesSet = filterIngressSet tunnel.ingress; ingressesStr = filterIngressStr tunnel.ingress; - fullConfig = { + fullConfig = filterConfig { tunnel = name; "credentials-file" = tunnel.credentialsFile; + warp-routing = filterConfig tunnel.warp-routing; + originRequest = filterConfig tunnel.originRequest; ingress = (map (key: { @@ -294,6 +296,7 @@ in (attrNames ingressesStr)) ++ [{ service = tunnel.default; }]; }; + mkConfigFile = pkgs.writeText "cloudflared.yml" (builtins.toJSON fullConfig); in nameValuePair "cloudflared-tunnel-${name}" ({ @@ -322,5 +325,5 @@ in }; }; - meta.maintainers = with maintainers; [ bbigras ]; + meta.maintainers = with maintainers; [ bbigras anpin ]; } diff --git a/nixos/modules/services/networking/dhcpcd.nix b/nixos/modules/services/networking/dhcpcd.nix index 2b59352ac616b..8d5ac02ba88be 100644 --- a/nixos/modules/services/networking/dhcpcd.nix +++ b/nixos/modules/services/networking/dhcpcd.nix @@ -13,6 +13,8 @@ let enableDHCP = config.networking.dhcpcd.enable && (config.networking.useDHCP || any (i: i.useDHCP == true) interfaces); + enableNTPService = (config.services.ntp.enable || config.services.ntpd-rs.enable || config.services.openntpd.enable || config.services.chrony.enable); + # Don't start dhcpcd on explicitly configured interfaces or on # interfaces that are part of a bridge, bond or sit device. ignoredInterfaces = @@ -89,20 +91,22 @@ let ${cfg.extraConfig} ''; - exitHook = pkgs.writeText "dhcpcd.exit-hook" - '' + exitHook = pkgs.writeText "dhcpcd.exit-hook" '' + ${optionalString enableNTPService '' if [ "$reason" = BOUND -o "$reason" = REBOOT ]; then - # Restart ntpd. We need to restart it to make sure that it - # will actually do something: if ntpd cannot resolve the - # server hostnames in its config file, then it will never do - # anything ever again ("couldn't resolve ..., giving up on - # it"), so we silently lose time synchronisation. This also - # applies to openntpd. - /run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd.service openntpd.service chronyd.service ntpd-rs.service || true + # Restart ntpd. We need to restart it to make sure that it will actually do something: + # if ntpd cannot resolve the server hostnames in its config file, then it will never do + # anything ever again ("couldn't resolve ..., giving up on it"), so we silently lose + # time synchronisation. This also applies to openntpd. + ${optionalString config.services.ntp.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd.service || true"} + ${optionalString config.services.ntpd-rs.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart ntpd-rs.service || true"} + ${optionalString config.services.openntpd.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart openntpd.service || true"} + ${optionalString config.services.chrony.enable "/run/current-system/systemd/bin/systemctl try-reload-or-restart chronyd.service || true"} fi + ''} - ${cfg.runHook} - ''; + ${cfg.runHook} + ''; in @@ -219,6 +223,8 @@ in ''; } ]; + environment.etc."dhcpcd.conf".source = dhcpcdConf; + systemd.services.dhcpcd = let cfgN = config.networking; hasDefaultGatewaySet = (cfgN.defaultGateway != null && cfgN.defaultGateway.address != "") @@ -230,7 +236,7 @@ in wants = [ "network.target" ]; before = [ "network-online.target" ]; - restartTriggers = [ exitHook ]; + restartTriggers = optional (enableNTPService || cfg.runHook != "") [ exitHook ]; # Stopping dhcpcd during a reconfiguration is undesirable # because it brings down the network interfaces configured by @@ -259,7 +265,9 @@ in environment.systemPackages = [ dhcpcd ]; - environment.etc."dhcpcd.exit-hook".source = exitHook; + environment.etc."dhcpcd.exit-hook" = mkIf (enableNTPService || cfg.runHook != "") { + source = exitHook; + }; powerManagement.resumeCommands = mkIf config.systemd.services.dhcpcd.enable '' diff --git a/nixos/modules/services/networking/go-camo.nix b/nixos/modules/services/networking/go-camo.nix new file mode 100644 index 0000000000000..cb3b6eade4644 --- /dev/null +++ b/nixos/modules/services/networking/go-camo.nix @@ -0,0 +1,73 @@ +{ lib, pkgs, config, ... }: + +let + cfg = config.services.go-camo; + inherit (lib) mkOption mkEnableOption mkIf mkMerge types optionalString; +in +{ + options.services.go-camo = { + enable = mkEnableOption "go-camo service"; + listen = mkOption { + type = types.nullOr types.str; + default = null; + description = "Address:Port to bind to for HTTP (default: 0.0.0.0:8080)."; + apply = v: optionalString (v != null) "--listen=${v}"; + }; + sslListen = mkOption { + type = types.nullOr types.str; + default = null; + description = "Address:Port to bind to for HTTPS."; + apply = v: optionalString (v != null) "--ssl-listen=${v}"; + }; + sslKey = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to TLS private key."; + apply = v: optionalString (v != null) "--ssl-key=${v}"; + }; + sslCert = mkOption { + type = types.nullOr types.path; + default = null; + description = "Path to TLS certificate."; + apply = v: optionalString (v != null) "--ssl-cert=${v}"; + }; + keyFile = mkOption { + type = types.path; + default = null; + description = '' + A file containing the HMAC key to use for signing URLs. + The file can contain any string. Can be generated using "openssl rand -base64 18 > the_file". + ''; + }; + extraOptions = mkOption { + type = with types; listOf str; + default = []; + description = "Extra options passed to the go-camo command."; + }; + }; + + config = mkIf cfg.enable { + systemd.services.go-camo = { + description = "go-camo service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + environment = { + GOCAMO_HMAC_FILE = "%d/hmac"; + }; + script = '' + export GOCAMO_HMAC=$(cat "$GOCAMO_HMAC_FILE") + exec ${lib.escapeShellArgs(lib.lists.remove "" ([ "${pkgs.go-camo}/bin/go-camo" cfg.listen cfg.sslListen cfg.sslKey cfg.sslCert ] ++ cfg.extraOptions))} + ''; + serviceConfig = { + NoNewPrivileges = true; + ProtectSystem = "strict"; + DynamicUser = true; + User = "gocamo"; + Group = "gocamo"; + LoadCredential = [ + "hmac:${cfg.keyFile}" + ]; + }; + }; + }; +} diff --git a/nixos/modules/services/networking/hostapd.nix b/nixos/modules/services/networking/hostapd.nix index 00482e59acf37..40542155ed63b 100644 --- a/nixos/modules/services/networking/hostapd.nix +++ b/nixos/modules/services/networking/hostapd.nix @@ -909,7 +909,7 @@ in { in { settings = { ssid = bssCfg.ssid; - utf8_ssid = bssCfg.ssid; + utf8_ssid = bssCfg.utf8Ssid; logger_syslog = mkDefault (-1); logger_syslog_level = bssCfg.logLevel; diff --git a/nixos/modules/services/networking/jibri/default.nix b/nixos/modules/services/networking/jibri/default.nix index 73d11bdbee5a2..dfba38896a914 100644 --- a/nixos/modules/services/networking/jibri/default.nix +++ b/nixos/modules/services/networking/jibri/default.nix @@ -5,12 +5,7 @@ with lib; let cfg = config.services.jibri; - # Copied from the jitsi-videobridge.nix file. - toHOCON = x: - if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}") - else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}" - else if isList x then "[${ concatMapStringsSep "," toHOCON x }]" - else builtins.toJSON x; + format = pkgs.formats.hocon { }; # We're passing passwords in environment variables that have names generated # from an attribute name, which may not be a valid bash identifier. @@ -38,13 +33,13 @@ let control-login = { domain = env.control.login.domain; username = env.control.login.username; - password.__hocon_envvar = toVarName "${name}_control"; + password = format.lib.mkSubstitution (toVarName "${name}_control"); }; call-login = { domain = env.call.login.domain; username = env.call.login.username; - password.__hocon_envvar = toVarName "${name}_call"; + password = format.lib.mkSubstitution (toVarName "${name}_call"); }; strip-from-room-domain = env.stripFromRoomDomain; @@ -85,13 +80,13 @@ let }; # Allow overriding leaves of the default config despite types.attrs not doing any merging. jibriConfig = recursiveUpdate defaultJibriConfig cfg.config; - configFile = pkgs.writeText "jibri.conf" (toHOCON { jibri = jibriConfig; }); + configFile = format.generate "jibri.conf" { jibri = jibriConfig; }; in { options.services.jibri = with types; { enable = mkEnableOption (lib.mdDoc "Jitsi BRoadcasting Infrastructure. Currently Jibri must be run on a host that is also running {option}`services.jitsi-meet.enable`, so for most use cases it will be simpler to run {option}`services.jitsi-meet.jibri.enable`"); config = mkOption { - type = attrs; + type = format.type; default = { }; description = lib.mdDoc '' Jibri configuration. diff --git a/nixos/modules/services/networking/jicofo.nix b/nixos/modules/services/networking/jicofo.nix index 0886bbe004c46..380344c8eaa15 100644 --- a/nixos/modules/services/networking/jicofo.nix +++ b/nixos/modules/services/networking/jicofo.nix @@ -5,14 +5,9 @@ with lib; let cfg = config.services.jicofo; - # HOCON is a JSON superset that some jitsi-meet components use for configuration - toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}") - else if isAttrs x && x ? __hocon_unquoted_string then x.__hocon_unquoted_string - else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}" - else if isList x then "[${ concatMapStringsSep "," toHOCON x }]" - else builtins.toJSON x; - - configFile = pkgs.writeText "jicofo.conf" (toHOCON cfg.config); + format = pkgs.formats.hocon { }; + + configFile = format.generate "jicofo.conf" cfg.config; in { options.services.jicofo = with types; { @@ -77,7 +72,7 @@ in }; config = mkOption { - type = (pkgs.formats.json {}).type; + type = format.type; default = { }; example = literalExpression '' { @@ -99,7 +94,7 @@ in hostname = cfg.xmppHost; username = cfg.userName; domain = cfg.userDomain; - password = { __hocon_envvar = "JICOFO_AUTH_PASS"; }; + password = format.lib.mkSubstitution "JICOFO_AUTH_PASS"; xmpp-domain = if cfg.xmppDomain == null then cfg.xmppHost else cfg.xmppDomain; }; service = client; diff --git a/nixos/modules/services/networking/jitsi-videobridge.nix b/nixos/modules/services/networking/jitsi-videobridge.nix index 37b0b1e5bf500..00ea5b9da5461 100644 --- a/nixos/modules/services/networking/jitsi-videobridge.nix +++ b/nixos/modules/services/networking/jitsi-videobridge.nix @@ -6,16 +6,7 @@ let cfg = config.services.jitsi-videobridge; attrsToArgs = a: concatStringsSep " " (mapAttrsToList (k: v: "${k}=${toString v}") a); - # HOCON is a JSON superset that videobridge2 uses for configuration. - # It can substitute environment variables which we use for passwords here. - # https://github.com/lightbend/config/blob/master/README.md - # - # Substitution for environment variable FOO is represented as attribute set - # { __hocon_envvar = "FOO"; } - toHOCON = x: if isAttrs x && x ? __hocon_envvar then ("\${" + x.__hocon_envvar + "}") - else if isAttrs x then "{${ concatStringsSep "," (mapAttrsToList (k: v: ''"${k}":${toHOCON v}'') x) }}" - else if isList x then "[${ concatMapStringsSep "," toHOCON x }]" - else builtins.toJSON x; + format = pkgs.formats.hocon { }; # We're passing passwords in environment variables that have names generated # from an attribute name, which may not be a valid bash identifier. @@ -38,7 +29,7 @@ let hostname = xmppConfig.hostName; domain = xmppConfig.domain; username = xmppConfig.userName; - password = { __hocon_envvar = toVarName name; }; + password = format.lib.mkSubstitution (toVarName name); muc_jids = xmppConfig.mucJids; muc_nickname = xmppConfig.mucNickname; disable_certificate_verification = xmppConfig.disableCertificateVerification; @@ -221,7 +212,7 @@ in "-Dnet.java.sip.communicator.SC_HOME_DIR_LOCATION" = "/etc/jitsi"; "-Dnet.java.sip.communicator.SC_HOME_DIR_NAME" = "videobridge"; "-Djava.util.logging.config.file" = "/etc/jitsi/videobridge/logging.properties"; - "-Dconfig.file" = pkgs.writeText "jvb.conf" (toHOCON jvbConfig); + "-Dconfig.file" = format.generate "jvb.conf" jvbConfig; # Mitigate CVE-2021-44228 "-Dlog4j2.formatMsgNoLookups" = true; } // (mapAttrs' (k: v: nameValuePair "-D${k}" v) cfg.extraProperties); diff --git a/nixos/modules/services/networking/knot.nix b/nixos/modules/services/networking/knot.nix index 94c32586736a5..6488a159b3b73 100644 --- a/nixos/modules/services/networking/knot.nix +++ b/nixos/modules/services/networking/knot.nix @@ -1,8 +1,36 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, utils, ... }: -with lib; let + inherit (lib) + attrNames + concatMapStrings + concatMapStringsSep + concatStrings + concatStringsSep + elem + filter + flip + hasAttr + hasPrefix + isAttrs + isBool + isDerivation + isList + mapAttrsToList + mkChangedOptionModule + mkEnableOption + mkIf + mkOption + mkPackageOption + optionals + types + ; + + inherit (utils) + escapeSystemdExecArgs + ; + cfg = config.services.knot; yamlConfig = let @@ -113,8 +141,7 @@ let mkConfigFile = configString: pkgs.writeTextFile { name = "knot.conf"; text = (concatMapStringsSep "\n" (file: "include: ${file}") cfg.keyFiles) + "\n" + configString; - # TODO: maybe we could do some checks even when private keys complicate this? - checkPhase = lib.optionalString (cfg.keyFiles == []) '' + checkPhase = lib.optionalString cfg.checkConfig '' ${cfg.package}/bin/knotc --config=$out conf-check ''; }; @@ -142,12 +169,45 @@ let in { options = { services.knot = { - enable = mkEnableOption (lib.mdDoc "Knot authoritative-only DNS server"); + enable = mkEnableOption "Knot authoritative-only DNS server"; + + enableXDP = mkOption { + type = types.bool; + default = lib.hasAttrByPath [ "xdp" "listen" ] cfg.settings; + defaultText = '' + Enabled when the `xdp.listen` setting is configured through `settings`. + ''; + example = true; + description = '' + Extends the systemd unit with permissions to allow for the use of + the eXpress Data Path (XDP). + + ::: {.note} + Make sure to read up on functional [limitations](https://www.knot-dns.cz/docs/latest/singlehtml/index.html#mode-xdp-limitations) + when running in XDP mode. + ::: + ''; + }; + + checkConfig = mkOption { + type = types.bool; + # TODO: maybe we could do some checks even when private keys complicate this? + # conf-check fails hard on missing IPs/devices with XDP + default = cfg.keyFiles == [] && !cfg.enableXDP; + defaultText = '' + Disabled when the config uses `keyFiles` or `enableXDP`. + ''; + example = false; + description = '' + Toggles the configuration test at build time. It runs in a + sandbox, and therefore cannot be used in all scenarios. + ''; + }; extraArgs = mkOption { type = types.listOf types.str; default = []; - description = lib.mdDoc '' + description = '' List of additional command line parameters for knotd ''; }; @@ -155,7 +215,7 @@ in { keyFiles = mkOption { type = types.listOf types.path; default = []; - description = lib.mdDoc '' + description = '' A list of files containing additional configuration to be included using the include directive. This option allows to include configuration like TSIG keys without @@ -168,7 +228,7 @@ in { settings = mkOption { type = types.attrs; default = {}; - description = lib.mdDoc '' + description = '' Extra configuration as nix values. ''; }; @@ -176,7 +236,7 @@ in { settingsFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc '' + description = '' As alternative to ``settings``, you can provide whole configuration directly in the almost-YAML format of Knot DNS. You might want to utilize ``pkgs.writeText "knot.conf" "longConfigString"`` for this. @@ -210,19 +270,35 @@ in { wants = [ "network.target" ]; after = ["network.target" ]; - serviceConfig = { + serviceConfig = let + # https://www.knot-dns.cz/docs/3.3/singlehtml/index.html#pre-requisites + xdpCapabilities = lib.optionals (cfg.enableXDP) [ + "CAP_NET_ADMIN" + "CAP_NET_RAW" + "CAP_SYS_ADMIN" + "CAP_IPC_LOCK" + ] ++ lib.optionals (lib.versionOlder config.boot.kernelPackages.kernel.version "5.11") [ + "CAP_SYS_RESOURCE" + ]; + in { Type = "notify"; - ExecStart = "${cfg.package}/bin/knotd --config=${configFile} --socket=${socketFile} ${concatStringsSep " " cfg.extraArgs}"; - ExecReload = "${knot-cli-wrappers}/bin/knotc reload"; + ExecStart = escapeSystemdExecArgs ([ + (lib.getExe cfg.package) + "--config=${configFile}" + "--socket=${socketFile}" + ] ++ cfg.extraArgs); + ExecReload = escapeSystemdExecArgs [ + "${knot-cli-wrappers}/bin/knotc" "reload" + ]; User = "knot"; Group = "knot"; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" - ]; + ] ++ xdpCapabilities; CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" - ]; + ] ++ xdpCapabilities; DeviceAllow = ""; DevicePolicy = "closed"; LockPersonality = true; @@ -247,6 +323,9 @@ in { "AF_INET" "AF_INET6" "AF_UNIX" + ] ++ optionals (cfg.enableXDP) [ + "AF_NETLINK" + "AF_XDP" ]; RestrictNamespaces = true; RestrictRealtime =true; @@ -258,6 +337,8 @@ in { SystemCallFilter = [ "@system-service" "~@privileged" + ] ++ optionals (cfg.enableXDP) [ + "bpf" ]; UMask = "0077"; }; diff --git a/nixos/modules/services/networking/libreswan.nix b/nixos/modules/services/networking/libreswan.nix index db4d2f7f0ba00..a44cac93d5f61 100644 --- a/nixos/modules/services/networking/libreswan.nix +++ b/nixos/modules/services/networking/libreswan.nix @@ -133,9 +133,6 @@ in "ipsec.d/01-nixos.conf".source = configFile; } // policyFiles; - # Create NSS database directory - systemd.tmpfiles.rules = [ "d /var/lib/ipsec/nss 755 root root -" ]; - systemd.services.ipsec = { description = "Internet Key Exchange (IKE) Protocol Daemon for IPsec"; wantedBy = [ "multi-user.target" ]; @@ -153,6 +150,10 @@ in echo 0 | tee /proc/sys/net/ipv4/conf/*/send_redirects echo 0 | tee /proc/sys/net/ipv{4,6}/conf/*/accept_redirects ''; + serviceConfig = { + StateDirectory = "ipsec/nss"; + StateDirectoryMode = 0700; + }; }; }; diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix index ad9eefb422525..4a08f5ed23709 100644 --- a/nixos/modules/services/networking/mosquitto.nix +++ b/nixos/modules/services/networking/mosquitto.nix @@ -177,17 +177,6 @@ let '' ++ hashedLines)); - makeACLFile = idx: users: supplement: - pkgs.writeText "mosquitto-acl-${toString idx}.conf" - (concatStringsSep - "\n" - (flatten [ - supplement - (mapAttrsToList - (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl) - users) - ])); - authPluginOptions = with types; submodule { options = { plugin = mkOption { @@ -342,7 +331,7 @@ let formatListener = idx: listener: [ "listener ${toString listener.port} ${toString listener.address}" - "acl_file ${makeACLFile idx listener.users listener.acl}" + "acl_file /etc/mosquitto/acl-${toString idx}.conf" ] ++ optional (! listener.omitPasswordAuth) "password_file ${cfg.dataDir}/passwd-${toString idx}" ++ formatFreeform {} listener.settings @@ -698,6 +687,27 @@ in cfg.listeners); }; + environment.etc = listToAttrs ( + imap0 + (idx: listener: { + name = "mosquitto/acl-${toString idx}.conf"; + value = { + user = config.users.users.mosquitto.name; + group = config.users.users.mosquitto.group; + mode = "0400"; + text = (concatStringsSep + "\n" + (flatten [ + listener.acl + (mapAttrsToList + (n: u: [ "user ${n}" ] ++ map (t: "topic ${t}") u.acl) + listener.users) + ])); + }; + }) + cfg.listeners + ); + users.users.mosquitto = { description = "Mosquitto MQTT Broker Daemon owner"; group = "mosquitto"; diff --git a/nixos/modules/services/networking/murmur.nix b/nixos/modules/services/networking/murmur.nix index 0cd80e134ace4..5805f332a66fe 100644 --- a/nixos/modules/services/networking/murmur.nix +++ b/nixos/modules/services/networking/murmur.nix @@ -326,6 +326,29 @@ in RuntimeDirectoryMode = "0700"; User = "murmur"; Group = "murmur"; + + # service hardening + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + CapabilityBoundingSet = "CAP_NET_BIND_SERVICE"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectSystem = "full"; + RestrictAddressFamilies = "~AF_PACKET AF_NETLINK"; + RestrictNamespaces = true; + RestrictSUIDSGID = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; }; }; diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix index 424d005dc0b5e..2351ebf4b7074 100644 --- a/nixos/modules/services/networking/nftables.nix +++ b/nixos/modules/services/networking/nftables.nix @@ -185,6 +185,19 @@ in can be loaded using "nft -f". The ruleset is updated atomically. ''; }; + + networking.nftables.flattenRulesetFile = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Use `builtins.readFile` rather than `include` to handle {option}`networking.nftables.rulesetFile`. It is useful when you want to apply {option}`networking.nftables.preCheckRuleset` to {option}`networking.nftables.rulesetFile`. + + ::: {.note} + It is expected that {option}`networking.nftables.rulesetFile` can be accessed from the build sandbox. + ::: + ''; + }; + networking.nftables.tables = mkOption { type = types.attrsOf (types.submodule tableSubmodule); @@ -252,8 +265,10 @@ in networking.nftables.flushRuleset = mkDefault (versionOlder config.system.stateVersion "23.11" || (cfg.rulesetFile != null || cfg.ruleset != "")); systemd.services.nftables = { description = "nftables firewall"; - before = [ "network-pre.target" ]; - wants = [ "network-pre.target" ]; + after = [ "sysinit.target" ]; + before = [ "network-pre.target" "shutdown.target" ]; + conflicts = [ "shutdown.target" ]; + wants = [ "network-pre.target" "sysinit.target" ]; wantedBy = [ "multi-user.target" ]; reloadIfChanged = true; serviceConfig = let @@ -293,9 +308,13 @@ in } '') enabledTables)} ${cfg.ruleset} - ${lib.optionalString (cfg.rulesetFile != null) '' - include "${cfg.rulesetFile}" - ''} + ${if cfg.rulesetFile != null then + if cfg.flattenRulesetFile then + builtins.readFile cfg.rulesetFile + else '' + include "${cfg.rulesetFile}" + '' + else ""} ''; checkPhase = lib.optionalString cfg.checkRuleset '' cp $out ruleset.conf @@ -315,6 +334,7 @@ in ExecStop = [ deletionsScriptVar cleanupDeletionsScript ]; StateDirectory = "nftables"; }; + unitConfig.DefaultDependencies = false; }; }; } diff --git a/nixos/modules/services/networking/pyload.nix b/nixos/modules/services/networking/pyload.nix index f2b85499d4ddb..93f8dd7d731a9 100644 --- a/nixos/modules/services/networking/pyload.nix +++ b/nixos/modules/services/networking/pyload.nix @@ -34,6 +34,18 @@ in description = "Directory to store downloads."; }; + user = mkOption { + type = types.str; + default = "pyload"; + description = "User under which pyLoad runs, and which owns the download directory."; + }; + + group = mkOption { + type = types.str; + default = "pyload"; + description = "Group under which pyLoad runs, and which owns the download directory."; + }; + credentialsFile = mkOption { type = with types; nullOr path; default = null; @@ -52,7 +64,7 @@ in config = lib.mkIf cfg.enable { systemd.tmpfiles.settings.pyload = { - ${cfg.downloadDirectory}.d = { }; + ${cfg.downloadDirectory}.d = { inherit (cfg) user group; }; }; systemd.services.pyload = { @@ -80,9 +92,8 @@ in cfg.downloadDirectory ]; - User = "pyload"; - Group = "pyload"; - DynamicUser = true; + User = cfg.user; + Group = cfg.group; EnvironmentFile = lib.optional (cfg.credentialsFile != null) cfg.credentialsFile; @@ -143,5 +154,13 @@ in ]; }; }; + + users.users.pyload = lib.mkIf (cfg.user == "pyload") { + isSystemUser = true; + group = cfg.group; + home = stateDir; + }; + + users.groups.pyload = lib.mkIf (cfg.group == "pyload") { }; }; } diff --git a/nixos/modules/services/networking/sabnzbd.nix b/nixos/modules/services/networking/sabnzbd.nix index cff2622b38e90..2f0d17ad3d177 100644 --- a/nixos/modules/services/networking/sabnzbd.nix +++ b/nixos/modules/services/networking/sabnzbd.nix @@ -36,6 +36,14 @@ in default = "sabnzbd"; description = lib.mdDoc "Group to run the service as"; }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Open ports in the firewall for the sabnzbd web interface + ''; + }; }; }; @@ -43,17 +51,16 @@ in ###### implementation config = mkIf cfg.enable { - - users.users.sabnzbd = { - uid = config.ids.uids.sabnzbd; - group = "sabnzbd"; - description = "sabnzbd user"; - home = "/var/lib/sabnzbd/"; - createHome = true; + users.users = mkIf (cfg.user == "sabnzbd") { + sabnzbd = { + uid = config.ids.uids.sabnzbd; + group = cfg.group; + description = "sabnzbd user"; + }; }; - users.groups.sabnzbd = { - gid = config.ids.gids.sabnzbd; + users.groups = mkIf (cfg.group == "sabnzbd") { + sabnzbd.gid = config.ids.gids.sabnzbd; }; systemd.services.sabnzbd = { @@ -63,10 +70,15 @@ in serviceConfig = { Type = "forking"; GuessMainPID = "no"; - User = "${cfg.user}"; - Group = "${cfg.group}"; + User = cfg.user; + Group = cfg.group; + StateDirectory = "sabnzbd"; ExecStart = "${lib.getBin cfg.package}/bin/sabnzbd -d -f ${cfg.configFile}"; }; }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ 8080 ]; + }; }; } diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix index 938d585e31793..5bbf875f0d57b 100644 --- a/nixos/modules/services/networking/searx.nix +++ b/nixos/modules/services/networking/searx.nix @@ -213,7 +213,7 @@ in serviceConfig = { User = "searx"; Group = "searx"; - ExecStart = "${cfg.package}/bin/searx-run"; + ExecStart = lib.getExe cfg.package; } // optionalAttrs (cfg.environmentFile != null) { EnvironmentFile = builtins.toPath cfg.environmentFile; }; environment = { diff --git a/nixos/modules/services/networking/tailscale.nix b/nixos/modules/services/networking/tailscale.nix index f11fe57d6ce5e..972299a4697a0 100644 --- a/nixos/modules/services/networking/tailscale.nix +++ b/nixos/modules/services/networking/tailscale.nix @@ -66,6 +66,13 @@ in { default = []; example = ["--ssh"]; }; + + extraDaemonFlags = mkOption { + description = lib.mdDoc "Extra flags to pass to {command}`tailscaled`."; + type = types.listOf types.str; + default = []; + example = ["--no-logs-no-support"]; + }; }; config = mkIf cfg.enable { @@ -80,7 +87,7 @@ in { ] ++ lib.optional config.networking.resolvconf.enable config.networking.resolvconf.package; serviceConfig.Environment = [ "PORT=${toString cfg.port}" - ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName}"'' + ''"FLAGS=--tun ${lib.escapeShellArg cfg.interfaceName} ${lib.concatStringsSep " " cfg.extraDaemonFlags}"'' ] ++ (lib.optionals (cfg.permitCertUid != null) [ "TS_PERMIT_CERT_UID=${cfg.permitCertUid}" ]); diff --git a/nixos/modules/services/networking/unbound.nix b/nixos/modules/services/networking/unbound.nix index 616b32f117979..8438e472e11ee 100644 --- a/nixos/modules/services/networking/unbound.nix +++ b/nixos/modules/services/networking/unbound.nix @@ -24,12 +24,24 @@ let confNoServer = concatStringsSep "\n" ((mapAttrsToList (toConf "") (builtins.removeAttrs cfg.settings [ "server" ])) ++ [""]); confServer = concatStringsSep "\n" (mapAttrsToList (toConf " ") (builtins.removeAttrs cfg.settings.server [ "define-tag" ])); - confFile = pkgs.writeText "unbound.conf" '' + confFileUnchecked = pkgs.writeText "unbound.conf" '' server: ${optionalString (cfg.settings.server.define-tag != "") (toOption " " "define-tag" cfg.settings.server.define-tag)} ${confServer} ${confNoServer} ''; + confFile = if cfg.checkconf then pkgs.runCommandLocal "unbound-checkconf" { } '' + cp ${confFileUnchecked} unbound.conf + + # fake stateDir which is not accesible in the sandbox + mkdir -p $PWD/state + sed -i unbound.conf \ + -e '/auto-trust-anchor-file/d' \ + -e "s|${cfg.stateDir}|$PWD/state|" + ${cfg.package}/bin/unbound-checkconf unbound.conf + + cp ${confFileUnchecked} $out + '' else confFileUnchecked; rootTrustAnchorFile = "${cfg.stateDir}/root.key"; @@ -62,6 +74,17 @@ in { description = lib.mdDoc "Directory holding all state for unbound to run."; }; + checkconf = mkOption { + type = types.bool; + default = !cfg.settings ? include; + defaultText = "!config.services.unbound.settings ? include"; + description = lib.mdDoc '' + Wether to check the resulting config file with unbound checkconf for syntax errors. + + If settings.include is used, then this options is disabled, as the import can likely not be resolved at build time. + ''; + }; + resolveLocalQueries = mkOption { type = types.bool; default = true; diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix index c659d93b40872..9d074c3027d02 100644 --- a/nixos/modules/services/security/kanidm.nix +++ b/nixos/modules/services/security/kanidm.nix @@ -132,6 +132,28 @@ in default = "WriteReplica"; type = lib.types.enum [ "WriteReplica" "WriteReplicaNoUI" "ReadOnlyReplica" ]; }; + online_backup = { + path = lib.mkOption { + description = lib.mdDoc "Path to the output directory for backups."; + type = lib.types.path; + default = "/var/lib/kanidm/backups"; + }; + schedule = lib.mkOption { + description = lib.mdDoc "The schedule for backups in cron format."; + type = lib.types.str; + default = "00 22 * * *"; + }; + versions = lib.mkOption { + description = lib.mdDoc '' + Number of backups to keep. + + The default is set to `0`, in order to disable backups by default. + ''; + type = lib.types.ints.unsigned; + default = 0; + example = 7; + }; + }; }; }; default = { }; @@ -233,6 +255,14 @@ in environment.systemPackages = lib.mkIf cfg.enableClient [ cfg.package ]; + systemd.tmpfiles.settings."10-kanidm" = { + ${cfg.serverSettings.online_backup.path}.d = { + mode = "0700"; + user = "kanidm"; + group = "kanidm"; + }; + }; + systemd.services.kanidm = lib.mkIf cfg.enableServer { description = "kanidm identity management daemon"; wantedBy = [ "multi-user.target" ]; @@ -253,6 +283,8 @@ in BindPaths = [ # To create the socket "/run/kanidmd:/run/kanidmd" + # To store backups + cfg.serverSettings.online_backup.path ]; AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; diff --git a/nixos/modules/services/security/opensnitch.nix b/nixos/modules/services/security/opensnitch.nix index 97ac3a72804c2..42cf8159f3ea5 100644 --- a/nixos/modules/services/security/opensnitch.nix +++ b/nixos/modules/services/security/opensnitch.nix @@ -36,7 +36,8 @@ in { description = mdDoc '' Declarative configuration of firewall rules. - All rules will be stored in `/var/lib/opensnitch/rules`. + All rules will be stored in `/var/lib/opensnitch/rules` by default. + Rules path can be configured with `settings.Rules.Path`. See [upstream documentation](https://github.com/evilsocket/opensnitch/wiki/Rules) for available options. ''; @@ -79,15 +80,6 @@ in { ''; }; - DefaultDuration = mkOption { - type = types.enum [ - "once" "always" "until restart" "30s" "5m" "15m" "30m" "1h" - ]; - description = mdDoc '' - Default duration of firewall rule. - ''; - }; - InterceptUnknown = mkOption { type = types.bool; description = mdDoc '' @@ -134,6 +126,30 @@ in { }; }; + + Ebpf.ModulesPath = mkOption { + type = types.path; + default = if cfg.settings.ProcMonitorMethod == "ebpf" then "${config.boot.kernelPackages.opensnitch-ebpf}/etc/opensnitchd" else null; + defaultText = literalExpression '' + if cfg.settings.ProcMonitorMethod == "ebpf" then + "\\$\\{config.boot.kernelPackages.opensnitch-ebpf\\}/etc/opensnitchd" + else null; + ''; + description = mdDoc '' + Configure eBPF modules path. Used when + `settings.ProcMonitorMethod` is set to `ebpf`. + ''; + }; + + Rules.Path = mkOption { + type = types.path; + default = "/var/lib/opensnitch/rules"; + description = mdDoc '' + Path to the directory where firewall rules can be found and will + get stored by the NixOS module. + ''; + }; + }; }; description = mdDoc '' @@ -151,40 +167,42 @@ in { systemd = { packages = [ pkgs.opensnitch ]; - services.opensnitchd.wantedBy = [ "multi-user.target" ]; + services.opensnitchd = { + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = [ + "" + "${pkgs.opensnitch}/bin/opensnitchd --config-file ${format.generate "default-config.json" cfg.settings}" + ]; + }; + preStart = mkIf (cfg.rules != {}) (let + rules = flip mapAttrsToList predefinedRules (file: content: { + inherit (content) file; + local = "${cfg.settings.Rules.Path}/${file}.json"; + }); + in '' + # Remove all firewall rules from rules path (configured with + # cfg.settings.Rules.Path) that are symlinks to a store-path, but aren't + # declared in `cfg.rules` (i.e. all networks that were "removed" from + # `cfg.rules`). + find ${cfg.settings.Rules.Path} -type l -lname '${builtins.storeDir}/*' ${optionalString (rules != {}) '' + -not \( ${concatMapStringsSep " -o " ({ local, ... }: + "-name '${baseNameOf local}*'") + rules} \) \ + ''} -delete + ${concatMapStrings ({ file, local }: '' + ln -sf '${file}' "${local}" + '') rules} + ''); + }; + tmpfiles.rules = [ + "d ${cfg.settings.Rules.Path} 0750 root root - -" + "L+ /etc/opensnitchd/system-fw.json - - - - ${pkgs.opensnitch}/etc/opensnitchd/system-fw.json" + ]; }; - systemd.services.opensnitchd.preStart = mkIf (cfg.rules != {}) (let - rules = flip mapAttrsToList predefinedRules (file: content: { - inherit (content) file; - local = "/var/lib/opensnitch/rules/${file}.json"; - }); - in '' - # Remove all firewall rules from `/var/lib/opensnitch/rules` that are symlinks to a store-path, - # but aren't declared in `cfg.rules` (i.e. all networks that were "removed" from - # `cfg.rules`). - find /var/lib/opensnitch/rules -type l -lname '${builtins.storeDir}/*' ${optionalString (rules != {}) '' - -not \( ${concatMapStringsSep " -o " ({ local, ... }: - "-name '${baseNameOf local}*'") - rules} \) \ - ''} -delete - ${concatMapStrings ({ file, local }: '' - ln -sf '${file}' "${local}" - '') rules} - - if [ ! -f /etc/opensnitchd/system-fw.json ]; then - cp "${pkgs.opensnitch}/etc/opensnitchd/system-fw.json" "/etc/opensnitchd/system-fw.json" - fi - ''); - - environment.etc = mkMerge [ ({ - "opensnitchd/default-config.json".source = format.generate "default-config.json" cfg.settings; - }) (mkIf (cfg.settings.ProcMonitorMethod == "ebpf") { - "opensnitchd/opensnitch.o".source = "${config.boot.kernelPackages.opensnitch-ebpf}/etc/opensnitchd/opensnitch.o"; - "opensnitchd/opensnitch-dns.o".source = "${config.boot.kernelPackages.opensnitch-ebpf}/etc/opensnitchd/opensnitch-dns.o"; - "opensnitchd/opensnitch-procs.o".source = "${config.boot.kernelPackages.opensnitch-ebpf}/etc/opensnitchd/opensnitch-procs.o"; - })]; - }; + + meta.maintainers = with lib.maintainers; [ onny ]; } diff --git a/nixos/modules/services/system/automatic-timezoned.nix b/nixos/modules/services/system/automatic-timezoned.nix index 8934ed3a7ef28..7d3cd004a7ba1 100644 --- a/nixos/modules/services/system/automatic-timezoned.nix +++ b/nixos/modules/services/system/automatic-timezoned.nix @@ -50,7 +50,7 @@ in serviceConfig = { Type = "exec"; User = "automatic-timezoned"; - ExecStart = "${cfg.package}/bin/automatic-timezoned --zoneinfo-path=${pkgs.tzdata}/share/zoneinfo/zone1970.tab"; + ExecStart = "${cfg.package}/bin/automatic-timezoned"; }; wantedBy = [ "default.target" ]; }; diff --git a/nixos/modules/services/torrent/transmission.nix b/nixos/modules/services/torrent/transmission.nix index 5dd02eb331633..a9fb123b981ec 100644 --- a/nixos/modules/services/torrent/transmission.nix +++ b/nixos/modules/services/torrent/transmission.nix @@ -74,7 +74,7 @@ in description = lib.mdDoc ""; }; options.message-level = mkOption { - type = types.ints.between 0 3; + type = types.ints.between 0 6; default = 2; description = lib.mdDoc "Set verbosity of transmission messages."; }; diff --git a/nixos/modules/services/web-apps/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix index 8d09d1b978283..7fc710c6fcec1 100644 --- a/nixos/modules/services/web-apps/mastodon.nix +++ b/nixos/modules/services/web-apps/mastodon.nix @@ -4,7 +4,8 @@ let cfg = config.services.mastodon; opt = options.services.mastodon; - # We only want to create a database if we're actually going to connect to it. + # We only want to create a Redis and PostgreSQL databases if we're actually going to connect to it local. + redisActuallyCreateLocally = cfg.redis.createLocally && (cfg.redis.host == "127.0.0.1" || cfg.redis.enableUnixSocket); databaseActuallyCreateLocally = cfg.database.createLocally && cfg.database.host == "/run/postgresql"; env = { @@ -33,6 +34,7 @@ let TRUSTED_PROXY_IP = cfg.trustedProxy; } + // lib.optionalAttrs (cfg.redis.createLocally && cfg.redis.enableUnixSocket) { REDIS_URL = "unix://${config.services.redis.servers.mastodon.unixSocket}"; } // lib.optionalAttrs (cfg.database.host != "/run/postgresql" && cfg.database.port != null) { DB_PORT = toString cfg.database.port; } // lib.optionalAttrs cfg.smtp.authenticate { SMTP_LOGIN = cfg.smtp.user; } // lib.optionalAttrs (cfg.elasticsearch.host != null) { ES_HOST = cfg.elasticsearch.host; } @@ -116,9 +118,11 @@ let threads = toString (if processCfg.threads == null then cfg.sidekiqThreads else processCfg.threads); in { after = [ "network.target" "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; requires = [ "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; description = "Mastodon sidekiq${jobClassLabel}"; @@ -146,9 +150,11 @@ let name = "mastodon-streaming-${toString i}"; value = { after = [ "network.target" "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; requires = [ "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; wantedBy = [ "mastodon.target" "mastodon-streaming.target" ]; @@ -404,6 +410,19 @@ in { type = lib.types.port; default = 31637; }; + + passwordFile = lib.mkOption { + description = lib.mdDoc "A file containing the password for Redis database."; + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/mastodon-redis-password"; + }; + + enableUnixSocket = lib.mkOption { + description = lib.mdDoc "Use Unix socket"; + type = lib.types.bool; + default = true; + }; }; database = { @@ -613,6 +632,13 @@ in { config = lib.mkIf cfg.enable (lib.mkMerge [{ assertions = [ { + assertion = redisActuallyCreateLocally -> (!cfg.redis.enableUnixSocket || cfg.redis.passwordFile == null); + message = '' + <option>services.mastodon.redis.enableUnixSocket</option> needs to be disabled if + <option>services.mastodon.redis.passwordFile</option> is used. + ''; + } + { assertion = databaseActuallyCreateLocally -> (cfg.user == cfg.database.user && cfg.database.user == cfg.database.name); message = '' For local automatic database provisioning (services.mastodon.database.createLocally == true) with peer @@ -689,6 +715,8 @@ in { OTP_SECRET="$(cat ${cfg.otpSecretFile})" VAPID_PRIVATE_KEY="$(cat ${cfg.vapidPrivateKeyFile})" VAPID_PUBLIC_KEY="$(cat ${cfg.vapidPublicKeyFile})" + '' + lib.optionalString (cfg.redis.passwordFile != null)'' + REDIS_PASSWORD="$(cat ${cfg.redis.passwordFile})" '' + lib.optionalString (cfg.database.passwordFile != null) '' DB_PASS="$(cat ${cfg.database.passwordFile})" '' + lib.optionalString cfg.smtp.authenticate '' @@ -751,9 +779,11 @@ in { systemd.services.mastodon-web = { after = [ "network.target" "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; requires = [ "mastodon-init-dirs.service" ] + ++ lib.optional redisActuallyCreateLocally "redis-mastodon.service" ++ lib.optional databaseActuallyCreateLocally "postgresql.service" ++ lib.optional cfg.automaticMigrations "mastodon-init-db.service"; wantedBy = [ "mastodon.target" ]; @@ -834,11 +864,14 @@ in { enable = true; hostname = lib.mkDefault "${cfg.localDomain}"; }; - services.redis.servers.mastodon = lib.mkIf (cfg.redis.createLocally && cfg.redis.host == "127.0.0.1") { - enable = true; - port = cfg.redis.port; - bind = "127.0.0.1"; - }; + services.redis.servers.mastodon = lib.mkIf redisActuallyCreateLocally (lib.mkMerge [ + { + enable = true; + } + (lib.mkIf (!cfg.redis.enableUnixSocket) { + port = cfg.redis.port; + }) + ]); services.postgresql = lib.mkIf databaseActuallyCreateLocally { enable = true; ensureUsers = [ @@ -859,6 +892,7 @@ in { }; }) (lib.attrsets.setAttrByPath [ cfg.user "packages" ] [ cfg.package pkgs.imagemagick ]) + (lib.mkIf (cfg.redis.createLocally && cfg.redis.enableUnixSocket) {${config.services.mastodon.user}.extraGroups = [ "redis-mastodon" ];}) ]; users.groups.${cfg.group}.members = lib.optional cfg.configureNginx config.services.nginx.user; diff --git a/nixos/modules/services/web-apps/mealie.nix b/nixos/modules/services/web-apps/mealie.nix new file mode 100644 index 0000000000000..8bb7542c6b56d --- /dev/null +++ b/nixos/modules/services/web-apps/mealie.nix @@ -0,0 +1,79 @@ +{ config, lib, pkgs, ...}: +let + cfg = config.services.mealie; + pkg = cfg.package; +in +{ + options.services.mealie = { + enable = lib.mkEnableOption "Mealie, a recipe manager and meal planner"; + + package = lib.mkPackageOption pkgs "mealie" { }; + + listenAddress = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "Address on which the service should listen."; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 9000; + description = "Port on which to serve the Mealie service."; + }; + + settings = lib.mkOption { + type = with lib.types; attrsOf anything; + default = {}; + description = lib.mdDoc '' + Configuration of the Mealie service. + + See [the mealie documentation](https://nightly.mealie.io/documentation/getting-started/installation/backend-config/) for available options and default values. + + In addition to the official documentation, you can set {env}`MEALIE_LOG_FILE`. + ''; + example = { + ALLOW_SIGNUP = "false"; + }; + }; + + credentialsFile = lib.mkOption { + type = with lib.types; nullOr path; + default = null; + example = "/run/secrets/mealie-credentials.env"; + description = '' + File containing credentials used in mealie such as {env}`POSTGRES_PASSWORD` + or sensitive LDAP options. + + Expects the format of an `EnvironmentFile=`, as described by {manpage}`systemd.exec(5)`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.mealie = { + description = "Mealie, a self hosted recipe manager and meal planner"; + + after = [ "network-online.target" ]; + wants = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + + environment = { + PRODUCTION = "true"; + ALEMBIC_CONFIG_FILE="${pkg}/config/alembic.ini"; + API_PORT = toString cfg.port; + DATA_DIR = "/var/lib/mealie"; + CRF_MODEL_PATH = "/var/lib/mealie/model.crfmodel"; + } // (builtins.mapAttrs (_: val: toString val) cfg.settings); + + serviceConfig = { + DynamicUser = true; + User = "mealie"; + ExecStartPre = "${pkg}/libexec/init_db"; + ExecStart = "${lib.getExe pkg} -b ${cfg.listenAddress}:${builtins.toString cfg.port}"; + EnvironmentFile = lib.mkIf (cfg.credentialsFile != null) cfg.credentialsFile; + StateDirectory = "mealie"; + StandardOutput="journal"; + }; + }; + }; +} diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index 8669f84b1cbb5..08f90dcf59d80 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -873,9 +873,11 @@ in { { systemd.timers.nextcloud-cron = { wantedBy = [ "timers.target" ]; after = [ "nextcloud-setup.service" ]; - timerConfig.OnBootSec = "5m"; - timerConfig.OnUnitActiveSec = "5m"; - timerConfig.Unit = "nextcloud-cron.service"; + timerConfig = { + OnBootSec = "5m"; + OnUnitActiveSec = "5m"; + Unit = "nextcloud-cron.service"; + }; }; systemd.tmpfiles.rules = map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [ @@ -992,15 +994,21 @@ in { nextcloud-cron = { after = [ "nextcloud-setup.service" ]; environment.NEXTCLOUD_CONFIG_DIR = "${datadir}/config"; - serviceConfig.Type = "oneshot"; - serviceConfig.User = "nextcloud"; - serviceConfig.ExecStart = "${phpPackage}/bin/php -f ${webroot}/cron.php"; + serviceConfig = { + Type = "oneshot"; + User = "nextcloud"; + ExecCondition = "${lib.getExe phpPackage} -f ${webroot}/occ status -e"; + ExecStart = "${lib.getExe phpPackage} -f ${webroot}/cron.php"; + KillMode = "process"; + }; }; nextcloud-update-plugins = mkIf cfg.autoUpdateApps.enable { after = [ "nextcloud-setup.service" ]; - serviceConfig.Type = "oneshot"; - serviceConfig.ExecStart = "${occ}/bin/nextcloud-occ app:update --all"; - serviceConfig.User = "nextcloud"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${occ}/bin/nextcloud-occ app:update --all"; + User = "nextcloud"; + }; startAt = cfg.autoUpdateApps.startAt; }; }; diff --git a/nixos/modules/services/web-apps/photoprism.nix b/nixos/modules/services/web-apps/photoprism.nix index d3773cc9cf788..ccf995fccf3e5 100644 --- a/nixos/modules/services/web-apps/photoprism.nix +++ b/nixos/modules/services/web-apps/photoprism.nix @@ -12,17 +12,14 @@ let lib.mapAttrs (_: toString) cfg.settings ); - manage = - let - setupEnv = lib.concatStringsSep "\n" (lib.mapAttrsToList (name: val: "export ${name}=${lib.escapeShellArg val}") env); - in - pkgs.writeShellScript "manage" '' - ${setupEnv} - eval "$(${config.systemd.package}/bin/systemctl show -pUID,MainPID photoprism.service | ${pkgs.gnused}/bin/sed "s/UID/ServiceUID/")" - exec ${pkgs.util-linux}/bin/nsenter \ - -t $MainPID -m -S $ServiceUID -G $ServiceUID --wdns=${cfg.storagePath} \ - ${cfg.package}/bin/photoprism "$@" - ''; + manage = pkgs.writeShellScript "manage" '' + set -o allexport # Export the following env vars + ${lib.toShellVars env} + eval "$(${config.systemd.package}/bin/systemctl show -pUID,MainPID photoprism.service | ${pkgs.gnused}/bin/sed "s/UID/ServiceUID/")" + exec ${pkgs.util-linux}/bin/nsenter \ + -t $MainPID -m -S $ServiceUID -G $ServiceUID --wdns=${cfg.storagePath} \ + ${cfg.package}/bin/photoprism "$@" + ''; in { meta.maintainers = with lib.maintainers; [ stunkymonkey ]; diff --git a/nixos/modules/services/web-apps/restya-board.nix b/nixos/modules/services/web-apps/restya-board.nix deleted file mode 100644 index 959bcbc5c9f11..0000000000000 --- a/nixos/modules/services/web-apps/restya-board.nix +++ /dev/null @@ -1,380 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -# TODO: are these php-packages needed? -#imagick -#php-geoip -> php.ini: extension = geoip.so -#expat - -let - cfg = config.services.restya-board; - fpm = config.services.phpfpm.pools.${poolName}; - - runDir = "/run/restya-board"; - - poolName = "restya-board"; - -in - -{ - - ###### interface - - options = { - - services.restya-board = { - - enable = mkEnableOption (lib.mdDoc "restya-board"); - - dataDir = mkOption { - type = types.path; - default = "/var/lib/restya-board"; - description = lib.mdDoc '' - Data of the application. - ''; - }; - - user = mkOption { - type = types.str; - default = "restya-board"; - description = lib.mdDoc '' - User account under which the web-application runs. - ''; - }; - - group = mkOption { - type = types.str; - default = "nginx"; - description = lib.mdDoc '' - Group account under which the web-application runs. - ''; - }; - - virtualHost = { - serverName = mkOption { - type = types.str; - default = "restya.board"; - description = lib.mdDoc '' - Name of the nginx virtualhost to use. - ''; - }; - - listenHost = mkOption { - type = types.str; - default = "localhost"; - description = lib.mdDoc '' - Listen address for the virtualhost to use. - ''; - }; - - listenPort = mkOption { - type = types.port; - default = 3000; - description = lib.mdDoc '' - Listen port for the virtualhost to use. - ''; - }; - }; - - database = { - host = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc '' - Host of the database. Leave 'null' to use a local PostgreSQL database. - A local PostgreSQL database is initialized automatically. - ''; - }; - - port = mkOption { - type = types.nullOr types.int; - default = 5432; - description = lib.mdDoc '' - The database's port. - ''; - }; - - name = mkOption { - type = types.str; - default = "restya_board"; - description = lib.mdDoc '' - Name of the database. The database must exist. - ''; - }; - - user = mkOption { - type = types.str; - default = "restya_board"; - description = lib.mdDoc '' - The database user. The user must exist and have access to - the specified database. - ''; - }; - - passwordFile = mkOption { - type = types.nullOr types.path; - default = null; - description = lib.mdDoc '' - The database user's password. 'null' if no password is set. - ''; - }; - }; - - email = { - server = mkOption { - type = types.nullOr types.str; - default = null; - example = "localhost"; - description = lib.mdDoc '' - Hostname to send outgoing mail. Null to use the system MTA. - ''; - }; - - port = mkOption { - type = types.port; - default = 25; - description = lib.mdDoc '' - Port used to connect to SMTP server. - ''; - }; - - login = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - SMTP authentication login used when sending outgoing mail. - ''; - }; - - password = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - SMTP authentication password used when sending outgoing mail. - - ATTENTION: The password is stored world-readable in the nix-store! - ''; - }; - }; - - timezone = mkOption { - type = types.lines; - default = "GMT"; - description = lib.mdDoc '' - Timezone the web-app runs in. - ''; - }; - - }; - - }; - - - ###### implementation - - config = mkIf cfg.enable { - - services.phpfpm.pools = { - ${poolName} = { - inherit (cfg) user group; - - phpOptions = '' - date.timezone = "CET" - - ${optionalString (cfg.email.server != null) '' - SMTP = ${cfg.email.server} - smtp_port = ${toString cfg.email.port} - auth_username = ${cfg.email.login} - auth_password = ${cfg.email.password} - ''} - ''; - settings = mapAttrs (name: mkDefault) { - "listen.owner" = "nginx"; - "listen.group" = "nginx"; - "listen.mode" = "0600"; - "pm" = "dynamic"; - "pm.max_children" = 75; - "pm.start_servers" = 10; - "pm.min_spare_servers" = 5; - "pm.max_spare_servers" = 20; - "pm.max_requests" = 500; - "catch_workers_output" = 1; - }; - }; - }; - - services.nginx.enable = true; - services.nginx.virtualHosts.${cfg.virtualHost.serverName} = { - listen = [ { addr = cfg.virtualHost.listenHost; port = cfg.virtualHost.listenPort; } ]; - serverName = cfg.virtualHost.serverName; - root = runDir; - extraConfig = '' - index index.html index.php; - - gzip on; - - gzip_comp_level 6; - gzip_min_length 1100; - gzip_buffers 16 8k; - gzip_proxied any; - gzip_types text/plain application/xml text/css text/js text/xml application/x-javascript text/javascript application/json application/xml+rss; - - client_max_body_size 300M; - - rewrite ^/oauth/authorize$ /server/php/authorize.php last; - rewrite ^/oauth_callback/([a-zA-Z0-9_\.]*)/([a-zA-Z0-9_\.]*)$ /server/php/oauth_callback.php?plugin=$1&code=$2 last; - rewrite ^/download/([0-9]*)/([a-zA-Z0-9_\.]*)$ /server/php/download.php?id=$1&hash=$2 last; - rewrite ^/ical/([0-9]*)/([0-9]*)/([a-z0-9]*).ics$ /server/php/ical.php?board_id=$1&user_id=$2&hash=$3 last; - rewrite ^/api/(.*)$ /server/php/R/r.php?_url=$1&$args last; - rewrite ^/api_explorer/api-docs/$ /client/api_explorer/api-docs/index.php last; - ''; - - locations."/".root = "${runDir}/client"; - - locations."~ \\.php$" = { - tryFiles = "$uri =404"; - extraConfig = '' - include ${config.services.nginx.package}/conf/fastcgi_params; - fastcgi_pass unix:${fpm.socket}; - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; - fastcgi_param PHP_VALUE "upload_max_filesize=9G \n post_max_size=9G \n max_execution_time=200 \n max_input_time=200 \n memory_limit=256M"; - ''; - }; - - locations."~* \\.(css|js|less|html|ttf|woff|jpg|jpeg|gif|png|bmp|ico)" = { - root = "${runDir}/client"; - extraConfig = '' - if (-f $request_filename) { - break; - } - rewrite ^/img/([a-zA-Z_]*)/([a-zA-Z_]*)/([a-zA-Z0-9_\.]*)$ /server/php/image.php?size=$1&model=$2&filename=$3 last; - add_header Cache-Control public; - add_header Cache-Control must-revalidate; - expires 7d; - ''; - }; - }; - - systemd.services.restya-board-init = { - description = "Restya board initialization"; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - - wantedBy = [ "multi-user.target" ]; - requires = lib.optional (cfg.database.host != null) "postgresql.service"; - after = [ "network.target" ] ++ (lib.optional (cfg.database.host != null) "postgresql.service"); - - script = '' - rm -rf "${runDir}" - mkdir -m 750 -p "${runDir}" - cp -r "${pkgs.restya-board}/"* "${runDir}" - sed -i "s/@restya.com/@${cfg.virtualHost.serverName}/g" "${runDir}/sql/restyaboard_with_empty_data.sql" - rm -rf "${runDir}/media" - rm -rf "${runDir}/client/img" - chmod -R 0750 "${runDir}" - - sed -i "s@^php@${config.services.phpfpm.phpPackage}/bin/php@" "${runDir}/server/php/shell/"*.sh - - ${if (cfg.database.host == null) then '' - sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', 'localhost');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', 'restya');/g" "${runDir}/server/php/config.inc.php" - '' else '' - sed -i "s/^.*'R_DB_HOST'.*$/define('R_DB_HOST', '${cfg.database.host}');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_PASSWORD'.*$/define('R_DB_PASSWORD', ${if cfg.database.passwordFile == null then "''" else "'$(cat ${cfg.database.passwordFile})');/g"}" "${runDir}/server/php/config.inc.php" - ''} - sed -i "s/^.*'R_DB_PORT'.*$/define('R_DB_PORT', '${toString cfg.database.port}');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_NAME'.*$/define('R_DB_NAME', '${cfg.database.name}');/g" "${runDir}/server/php/config.inc.php" - sed -i "s/^.*'R_DB_USER'.*$/define('R_DB_USER', '${cfg.database.user}');/g" "${runDir}/server/php/config.inc.php" - - chmod 0400 "${runDir}/server/php/config.inc.php" - - ln -sf "${cfg.dataDir}/media" "${runDir}/media" - ln -sf "${cfg.dataDir}/client/img" "${runDir}/client/img" - - chmod g+w "${runDir}/tmp/cache" - chown -R "${cfg.user}":"${cfg.group}" "${runDir}" - - - mkdir -m 0750 -p "${cfg.dataDir}" - mkdir -m 0750 -p "${cfg.dataDir}/media" - mkdir -m 0750 -p "${cfg.dataDir}/client/img" - cp -r "${pkgs.restya-board}/media/"* "${cfg.dataDir}/media" - cp -r "${pkgs.restya-board}/client/img/"* "${cfg.dataDir}/client/img" - chown "${cfg.user}":"${cfg.group}" "${cfg.dataDir}" - chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/media" - chown -R "${cfg.user}":"${cfg.group}" "${cfg.dataDir}/client/img" - - ${optionalString (cfg.database.host == null) '' - if ! [ -e "${cfg.dataDir}/.db-initialized" ]; then - ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ - ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \ - -c "CREATE USER ${cfg.database.user} WITH ENCRYPTED PASSWORD 'restya'" - - ${pkgs.sudo}/bin/sudo -u ${config.services.postgresql.superUser} \ - ${config.services.postgresql.package}/bin/psql -U ${config.services.postgresql.superUser} \ - -c "CREATE DATABASE ${cfg.database.name} OWNER ${cfg.database.user} ENCODING 'UTF8' TEMPLATE template0" - - ${pkgs.sudo}/bin/sudo -u ${cfg.user} \ - ${config.services.postgresql.package}/bin/psql -U ${cfg.database.user} \ - -d ${cfg.database.name} -f "${runDir}/sql/restyaboard_with_empty_data.sql" - - touch "${cfg.dataDir}/.db-initialized" - fi - ''} - ''; - }; - - systemd.timers.restya-board = { - description = "restya-board scripts for e.g. email notification"; - wantedBy = [ "timers.target" ]; - after = [ "restya-board-init.service" ]; - requires = [ "restya-board-init.service" ]; - timerConfig = { - OnUnitInactiveSec = "60s"; - Unit = "restya-board-timers.service"; - }; - }; - - systemd.services.restya-board-timers = { - description = "restya-board scripts for e.g. email notification"; - serviceConfig.Type = "oneshot"; - serviceConfig.User = cfg.user; - - after = [ "restya-board-init.service" ]; - requires = [ "restya-board-init.service" ]; - - script = '' - /bin/sh ${runDir}/server/php/shell/instant_email_notification.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/periodic_email_notification.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/imap.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/webhook.sh 2> /dev/null || true - /bin/sh ${runDir}/server/php/shell/card_due_notification.sh 2> /dev/null || true - ''; - }; - - users.users.restya-board = { - isSystemUser = true; - createHome = false; - home = runDir; - group = "restya-board"; - }; - users.groups.restya-board = {}; - - services.postgresql.enable = mkIf (cfg.database.host == null) true; - - services.postgresql.identMap = optionalString (cfg.database.host == null) - '' - restya-board-users restya-board restya_board - ''; - - services.postgresql.authentication = optionalString (cfg.database.host == null) - '' - local restya_board all ident map=restya-board-users - ''; - - }; - -} - diff --git a/nixos/modules/services/web-apps/suwayomi-server.nix b/nixos/modules/services/web-apps/suwayomi-server.nix index c4c1540edbee5..94dbe6f99356e 100644 --- a/nixos/modules/services/web-apps/suwayomi-server.nix +++ b/nixos/modules/services/web-apps/suwayomi-server.nix @@ -3,6 +3,8 @@ let cfg = config.services.suwayomi-server; inherit (lib) mkOption mdDoc mkEnableOption mkIf types; + + format = pkgs.formats.hocon { }; in { options = { @@ -48,19 +50,7 @@ in settings = mkOption { type = types.submodule { - freeformType = - let - recursiveAttrsType = with types; attrsOf (nullOr (oneOf [ - str - path - int - float - bool - (listOf str) - (recursiveAttrsType // { description = "instances of this type recursively"; }) - ])); - in - recursiveAttrsType; + freeformType = format.type; options = { server = { ip = mkOption { @@ -180,38 +170,7 @@ in systemd.services.suwayomi-server = let - flattenConfig = prefix: config: - lib.foldl' - lib.mergeAttrs - { } - (lib.attrValues - (lib.mapAttrs - (k: v: - if !(lib.isAttrs v) - then { "${prefix}${k}" = v; } - else flattenConfig "${prefix}${k}." v - ) - config - ) - ); - - # HOCON is a JSON superset that suwayomi-server use for configuration - toHOCON = attr: - let - attrType = builtins.typeOf attr; - in - if builtins.elem attrType [ "string" "path" "int" "float" ] - then ''"${toString attr}"'' - else if attrType == "bool" - then lib.boolToString attr - else if attrType == "list" - then "[\n${lib.concatMapStringsSep ",\n" toHOCON attr}\n]" - else # attrs, lambda, null - throw '' - [suwayomi-server]: invalid config value type '${attrType}'. - ''; - - configFile = pkgs.writeText "server.conf" (lib.pipe cfg.settings [ + configFile = format.generate "server.conf" (lib.pipe cfg.settings [ (settings: lib.recursiveUpdate settings { server.basicAuthPasswordFile = null; server.basicAuthPassword = @@ -219,12 +178,8 @@ in then "$TACHIDESK_SERVER_BASIC_AUTH_PASSWORD" else null; }) - (flattenConfig "") - (lib.filterAttrs (_: x: x != null)) - (lib.mapAttrsToList (name: value: ''${name} = ${toHOCON value}'')) - lib.concatLines + (lib.filterAttrsRecursive (_: x: x != null)) ]); - in { description = "A free and open source manga reader server that runs extensions built for Tachiyomi."; diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix index 6c9b4bd1b8a7d..84342165c9c04 100644 --- a/nixos/modules/services/web-apps/tt-rss.nix +++ b/nixos/modules/services/web-apps/tt-rss.nix @@ -4,6 +4,8 @@ with lib; let cfg = config.services.tt-rss; + inherit (cfg) phpPackage; + configVersion = 26; dbPort = if cfg.database.port == null @@ -26,7 +28,7 @@ let ; in pkgs.writeText "config.php" '' <?php - putenv('TTRSS_PHP_EXECUTABLE=${pkgs.php}/bin/php'); + putenv('TTRSS_PHP_EXECUTABLE=${phpPackage}/bin/php'); putenv('TTRSS_LOCK_DIRECTORY=${cfg.root}/lock'); putenv('TTRSS_CACHE_DIR=${cfg.root}/cache'); @@ -456,6 +458,15 @@ let ''; }; + phpPackage = lib.mkOption { + type = lib.types.package; + default = pkgs.php; + defaultText = "pkgs.php"; + description = lib.mdDoc '' + php package to use for php fpm and update daemon. + ''; + }; + plugins = mkOption { type = types.listOf types.str; default = ["auth_internal" "note"]; @@ -543,7 +554,7 @@ let services.phpfpm.pools = mkIf (cfg.pool == "${poolName}") { ${poolName} = { inherit (cfg) user; - phpPackage = pkgs.php81; + inherit phpPackage; settings = mapAttrs (name: mkDefault) { "listen.owner" = "nginx"; "listen.group" = "nginx"; @@ -605,13 +616,13 @@ let description = "Tiny Tiny RSS feeds update daemon"; preStart = '' - ${pkgs.php81}/bin/php ${cfg.root}/www/update.php --update-schema --force-yes + ${phpPackage}/bin/php ${cfg.root}/www/update.php --update-schema --force-yes ''; serviceConfig = { User = "${cfg.user}"; Group = "tt_rss"; - ExecStart = "${pkgs.php}/bin/php ${cfg.root}/www/update.php --daemon --quiet"; + ExecStart = "${phpPackage}/bin/php ${cfg.root}/www/update.php --daemon --quiet"; Restart = "on-failure"; RestartSec = "60"; SyslogIdentifier = "tt-rss"; diff --git a/nixos/modules/services/web-servers/garage.nix b/nixos/modules/services/web-servers/garage.nix index 47b4c6ab416e8..48dd5b34757c1 100644 --- a/nixos/modules/services/web-servers/garage.nix +++ b/nixos/modules/services/web-servers/garage.nix @@ -30,7 +30,7 @@ in }; logLevel = mkOption { - type = types.enum ([ "info" "debug" "trace" ]); + type = types.enum ([ "error" "warn" "info" "debug" "trace" ]); default = "info"; example = "debug"; description = lib.mdDoc "Garage log level, see <https://garagehq.deuxfleurs.fr/documentation/quick-start/#launching-the-garage-server> for examples."; diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix index e545869ca4320..14361df2bb663 100644 --- a/nixos/modules/services/web-servers/ttyd.nix +++ b/nixos/modules/services/web-servers/ttyd.nix @@ -1,11 +1,17 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.services.ttyd; + inherit (lib) + optionals + types + concatLists + mapAttrsToList + mkOption + ; + # Command line arguments for the ttyd daemon args = [ "--port" (toString cfg.port) ] ++ optionals (cfg.socket != null) [ "--interface" cfg.socket ] @@ -14,6 +20,7 @@ let ++ (concatLists (mapAttrsToList (_k: _v: [ "--client-option" "${_k}=${_v}" ]) cfg.clientOptions)) ++ [ "--terminal-type" cfg.terminalType ] ++ optionals cfg.checkOrigin [ "--check-origin" ] + ++ optionals cfg.writeable [ "--writable" ] # the typo is correct ++ [ "--max-clients" (toString cfg.maxClients) ] ++ optionals (cfg.indexFile != null) [ "--index" cfg.indexFile ] ++ optionals cfg.enableIPv6 [ "--ipv6" ] @@ -30,40 +37,40 @@ in options = { services.ttyd = { - enable = mkEnableOption (lib.mdDoc "ttyd daemon"); + enable = lib.mkEnableOption ("ttyd daemon"); port = mkOption { type = types.port; default = 7681; - description = lib.mdDoc "Port to listen on (use 0 for random port)"; + description = "Port to listen on (use 0 for random port)"; }; socket = mkOption { type = types.nullOr types.path; default = null; example = "/var/run/ttyd.sock"; - description = lib.mdDoc "UNIX domain socket path to bind."; + description = "UNIX domain socket path to bind."; }; interface = mkOption { type = types.nullOr types.str; default = null; example = "eth0"; - description = lib.mdDoc "Network interface to bind."; + description = "Network interface to bind."; }; username = mkOption { type = types.nullOr types.str; default = null; - description = lib.mdDoc "Username for basic authentication."; + description = "Username for basic http authentication."; }; passwordFile = mkOption { type = types.nullOr types.path; default = null; apply = value: if value == null then null else toString value; - description = lib.mdDoc '' - File containing the password to use for basic authentication. + description = '' + File containing the password to use for basic http authentication. For insecurely putting the password in the globally readable store use `pkgs.writeText "ttydpw" "MyPassword"`. ''; @@ -72,19 +79,46 @@ in signal = mkOption { type = types.ints.u8; default = 1; - description = lib.mdDoc "Signal to send to the command on session close."; + description = "Signal to send to the command on session close."; + }; + + entrypoint = mkOption { + type = types.listOf types.str; + default = [ "${pkgs.shadow}/bin/login" ]; + defaultText = lib.literalExpression '' + [ "''${pkgs.shadow}/bin/login" ] + ''; + example = lib.literalExpression '' + [ (lib.getExe pkgs.htop) ] + ''; + description = "Which command ttyd runs."; + apply = lib.escapeShellArgs; + }; + + user = mkOption { + type = types.str; + # `login` needs to be run as root + default = "root"; + description = "Which unix user ttyd should run as."; + }; + + writeable = mkOption { + type = types.nullOr types.bool; + default = null; # null causes an eval error, forcing the user to consider attack surface + example = true; + description = "Allow clients to write to the TTY."; }; clientOptions = mkOption { type = types.attrsOf types.str; default = {}; - example = literalExpression '' + example = lib.literalExpression '' { fontSize = "16"; fontFamily = "Fira Code"; } ''; - description = lib.mdDoc '' + description = '' Attribute set of client options for xtermjs. <https://xtermjs.org/docs/api/terminal/interfaces/iterminaloptions/> ''; @@ -93,50 +127,50 @@ in terminalType = mkOption { type = types.str; default = "xterm-256color"; - description = lib.mdDoc "Terminal type to report."; + description = "Terminal type to report."; }; checkOrigin = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether to allow a websocket connection from a different origin."; + description = "Whether to allow a websocket connection from a different origin."; }; maxClients = mkOption { type = types.int; default = 0; - description = lib.mdDoc "Maximum clients to support (0, no limit)"; + description = "Maximum clients to support (0, no limit)"; }; indexFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc "Custom index.html path"; + description = "Custom index.html path"; }; enableIPv6 = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether or not to enable IPv6 support."; + description = "Whether or not to enable IPv6 support."; }; enableSSL = mkOption { type = types.bool; default = false; - description = lib.mdDoc "Whether or not to enable SSL (https) support."; + description = "Whether or not to enable SSL (https) support."; }; certFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc "SSL certificate file path."; + description = "SSL certificate file path."; }; keyFile = mkOption { type = types.nullOr types.path; default = null; apply = value: if value == null then null else toString value; - description = lib.mdDoc '' + description = '' SSL key file path. For insecurely putting the keyFile in the globally readable store use `pkgs.writeText "ttydKeyFile" "SSLKEY"`. @@ -146,25 +180,27 @@ in caFile = mkOption { type = types.nullOr types.path; default = null; - description = lib.mdDoc "SSL CA file path for client certificate verification."; + description = "SSL CA file path for client certificate verification."; }; logLevel = mkOption { type = types.int; default = 7; - description = lib.mdDoc "Set log level."; + description = "Set log level."; }; }; }; ###### implementation - config = mkIf cfg.enable { + config = lib.mkIf cfg.enable { assertions = [ { assertion = cfg.enableSSL -> cfg.certFile != null && cfg.keyFile != null && cfg.caFile != null; message = "SSL is enabled for ttyd, but no certFile, keyFile or caFile has been specified."; } + { assertion = cfg.writeable != null; + message = "services.ttyd.writeable must be set"; } { assertion = ! (cfg.interface != null && cfg.socket != null); message = "Cannot set both interface and socket for ttyd."; } { assertion = (cfg.username != null) == (cfg.passwordFile != null); @@ -177,21 +213,19 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { - # Runs login which needs to be run as root - # login: Cannot possibly work without effective root - User = "root"; + User = cfg.user; LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}"; }; script = if cfg.passwordFile != null then '' PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE") ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ - --credential ${escapeShellArg cfg.username}:"$PASSWORD" \ - ${pkgs.shadow}/bin/login + --credential ${lib.escapeShellArg cfg.username}:"$PASSWORD" \ + ${cfg.entrypoint} '' else '' ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \ - ${pkgs.shadow}/bin/login + ${cfg.entrypoint} ''; }; }; diff --git a/nixos/modules/services/web-servers/zope2.nix b/nixos/modules/services/web-servers/zope2.nix deleted file mode 100644 index 29731b29eea4f..0000000000000 --- a/nixos/modules/services/web-servers/zope2.nix +++ /dev/null @@ -1,262 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - - cfg = config.services.zope2; - - zope2Opts = { name, ... }: { - options = { - - name = mkOption { - default = "${name}"; - type = types.str; - description = lib.mdDoc "The name of the zope2 instance. If undefined, the name of the attribute set will be used."; - }; - - threads = mkOption { - default = 2; - type = types.int; - description = lib.mdDoc "Specify the number of threads that Zope's ZServer web server will use to service requests. "; - }; - - http_address = mkOption { - default = "localhost:8080"; - type = types.str; - description = lib.mdDoc "Give a port and address for the HTTP server."; - }; - - user = mkOption { - default = "zope2"; - type = types.str; - description = lib.mdDoc "The name of the effective user for the Zope process."; - }; - - clientHome = mkOption { - default = "/var/lib/zope2/${name}"; - type = types.path; - description = lib.mdDoc "Home directory of zope2 instance."; - }; - extra = mkOption { - default = - '' - <zodb_db main> - mount-point / - cache-size 30000 - <blobstorage> - blob-dir /var/lib/zope2/${name}/blobstorage - <filestorage> - path /var/lib/zope2/${name}/filestorage/Data.fs - </filestorage> - </blobstorage> - </zodb_db> - ''; - type = types.lines; - description = lib.mdDoc "Extra zope.conf"; - }; - - packages = mkOption { - type = types.listOf types.package; - description = lib.mdDoc "The list of packages you want to make available to the zope2 instance."; - }; - - }; - }; - -in - -{ - - ###### interface - - options = { - - services.zope2.instances = mkOption { - default = {}; - type = with types; attrsOf (submodule zope2Opts); - example = literalExpression '' - { - plone01 = { - http_address = "127.0.0.1:8080"; - extra = - ''' - <zodb_db main> - mount-point / - cache-size 30000 - <blobstorage> - blob-dir /var/lib/zope2/plone01/blobstorage - <filestorage> - path /var/lib/zope2/plone01/filestorage/Data.fs - </filestorage> - </blobstorage> - </zodb_db> - '''; - }; - } - ''; - description = lib.mdDoc "zope2 instances to be created automatically by the system."; - }; - }; - - ###### implementation - - config = mkIf (cfg.instances != {}) { - - users.users.zope2 = { - isSystemUser = true; - group = "zope2"; - }; - users.groups.zope2 = {}; - - systemd.services = - let - - createZope2Instance = opts: name: - let - interpreter = pkgs.writeScript "interpreter" - '' - import sys - - _interactive = True - if len(sys.argv) > 1: - _options, _args = __import__("getopt").getopt(sys.argv[1:], 'ic:m:') - _interactive = False - for (_opt, _val) in _options: - if _opt == '-i': - _interactive = True - elif _opt == '-c': - exec _val - elif _opt == '-m': - sys.argv[1:] = _args - _args = [] - __import__("runpy").run_module( - _val, {}, "__main__", alter_sys=True) - - if _args: - sys.argv[:] = _args - __file__ = _args[0] - del _options, _args - execfile(__file__) - - if _interactive: - del _interactive - __import__("code").interact(banner="", local=globals()) - ''; - env = pkgs.buildEnv { - name = "zope2-${name}-env"; - paths = [ - pkgs.python27 - pkgs.python27Packages.recursive-pth-loader - pkgs.python27Packages."plone.recipe.zope2instance" - ] ++ attrValues pkgs.python27.modules - ++ opts.packages; - postBuild = - '' - echo "#!$out/bin/python" > $out/bin/interpreter - cat ${interpreter} >> $out/bin/interpreter - ''; - }; - conf = pkgs.writeText "zope2-${name}-conf" - '' - %define INSTANCEHOME ${env} - instancehome $INSTANCEHOME - %define CLIENTHOME ${opts.clientHome}/${opts.name} - clienthome $CLIENTHOME - - debug-mode off - security-policy-implementation C - verbose-security off - default-zpublisher-encoding utf-8 - zserver-threads ${toString opts.threads} - effective-user ${opts.user} - - pid-filename ${opts.clientHome}/${opts.name}/pid - lock-filename ${opts.clientHome}/${opts.name}/lock - python-check-interval 1000 - enable-product-installation off - - <environment> - zope_i18n_compile_mo_files false - </environment> - - <eventlog> - level INFO - <logfile> - path /var/log/zope2/${name}.log - level INFO - </logfile> - </eventlog> - - <logger access> - level WARN - <logfile> - path /var/log/zope2/${name}-Z2.log - format %(message)s - </logfile> - </logger> - - <http-server> - address ${opts.http_address} - </http-server> - - <zodb_db temporary> - <temporarystorage> - name temporary storage for sessioning - </temporarystorage> - mount-point /temp_folder - container-class Products.TemporaryFolder.TemporaryContainer - </zodb_db> - - ${opts.extra} - ''; - ctlScript = pkgs.writeScript "zope2-${name}-ctl-script" - '' - #!${env}/bin/python - - import sys - import plone.recipe.zope2instance.ctl - - if __name__ == '__main__': - sys.exit(plone.recipe.zope2instance.ctl.main( - ["-C", "${conf}"] - + sys.argv[1:])) - ''; - - ctl = pkgs.writeScript "zope2-${name}-ctl" - '' - #!${pkgs.bash}/bin/bash -e - export PYTHONHOME=${env} - exec ${ctlScript} "$@" - ''; - in { - #description = "${name} instance"; - after = [ "network.target" ]; # with RelStorage also add "postgresql.service" - wantedBy = [ "multi-user.target" ]; - path = opts.packages; - preStart = - '' - mkdir -p /var/log/zope2/ - touch /var/log/zope2/${name}.log - touch /var/log/zope2/${name}-Z2.log - chown ${opts.user} /var/log/zope2/${name}.log - chown ${opts.user} /var/log/zope2/${name}-Z2.log - - mkdir -p ${opts.clientHome}/filestorage ${opts.clientHome}/blobstorage - mkdir -p ${opts.clientHome}/${opts.name} - chown ${opts.user} ${opts.clientHome} -R - - ${ctl} adduser admin admin - ''; - - serviceConfig.Type = "forking"; - serviceConfig.ExecStart = "${ctl} start"; - serviceConfig.ExecStop = "${ctl} stop"; - serviceConfig.ExecReload = "${ctl} restart"; - }; - - in listToAttrs (map (name: { name = "zope2-${name}"; value = createZope2Instance (builtins.getAttr name cfg.instances) name; }) (builtins.attrNames cfg.instances)); - - }; - -} diff --git a/nixos/modules/services/x11/desktop-managers/budgie.nix b/nixos/modules/services/x11/desktop-managers/budgie.nix index 463c45675cee4..fe39097a22e8f 100644 --- a/nixos/modules/services/x11/desktop-managers/budgie.nix +++ b/nixos/modules/services/x11/desktop-managers/budgie.nix @@ -39,6 +39,10 @@ let ''; destination = "/share/gnome-background-properties/nixos.xml"; }; + + budgie-control-center = pkgs.budgie.budgie-control-center.override { + enableSshSocket = config.services.openssh.startWhenNeeded; + }; in { options = { services.xserver.desktopManager.budgie = { @@ -114,7 +118,7 @@ in { [ # Budgie Desktop. budgie.budgie-backgrounds - budgie.budgie-control-center + budgie-control-center (budgie.budgie-desktop-with-plugins.override { plugins = cfg.extraPlugins; }) budgie.budgie-desktop-view budgie.budgie-screensaver @@ -233,8 +237,8 @@ in { services.gvfs.enable = mkDefault true; # Register packages for DBus. - services.dbus.packages = with pkgs; [ - budgie.budgie-control-center + services.dbus.packages = [ + budgie-control-center ]; # Register packages for udev. diff --git a/nixos/modules/services/x11/desktop-managers/deepin.nix b/nixos/modules/services/x11/desktop-managers/deepin.nix index 7d3acada60739..0824d6e30a8a9 100644 --- a/nixos/modules/services/x11/desktop-managers/deepin.nix +++ b/nixos/modules/services/x11/desktop-managers/deepin.nix @@ -173,19 +173,20 @@ in ]; optionalPackages = [ onboard # dde-dock plugin - deepin-camera deepin-calculator deepin-compressor deepin-editor deepin-picker deepin-draw - deepin-album - deepin-image-viewer deepin-music deepin-movie-reborn deepin-system-monitor - deepin-screen-recorder deepin-shortcut-viewer + # freeimage has knownVulnerabilties, don't install packages using freeiamge by default + # deepin-album + # deepin-camera + # deepin-image-viewer + # deepin-screen-recorder ]; in requiredPackages diff --git a/nixos/modules/services/x11/desktop-managers/default.nix b/nixos/modules/services/x11/desktop-managers/default.nix index 66cb4ee29c0a9..ecb8d1e91bde2 100644 --- a/nixos/modules/services/x11/desktop-managers/default.nix +++ b/nixos/modules/services/x11/desktop-managers/default.nix @@ -18,7 +18,7 @@ in # determines the default: later modules (if enabled) are preferred. # E.g., if Plasma 5 is enabled, it supersedes xterm. imports = [ - ./none.nix ./xterm.nix ./phosh.nix ./xfce.nix ./plasma5.nix ./lumina.nix + ./none.nix ./xterm.nix ./phosh.nix ./xfce.nix ./plasma5.nix ./plasma6.nix ./lumina.nix ./lxqt.nix ./enlightenment.nix ./gnome.nix ./retroarch.nix ./kodi.nix ./mate.nix ./pantheon.nix ./surf-display.nix ./cde.nix ./cinnamon.nix ./budgie.nix ./deepin.nix diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix index 677465f55c47f..7645b3070369c 100644 --- a/nixos/modules/services/x11/desktop-managers/plasma5.nix +++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix @@ -362,7 +362,7 @@ in security.pam.services.kde = { allowNullPassword = true; }; - security.pam.services.login.enableKwallet = true; + security.pam.services.login.kwallet.enable = true; systemd.user.services = { plasma-early-setup = mkIf cfg.runUsingSystemd { @@ -384,6 +384,7 @@ in system.userActivationScripts.plasmaSetup = activationScript; programs.firefox.nativeMessagingHosts.packages = [ pkgs.plasma5Packages.plasma-browser-integration ]; + programs.chromium.enablePlasmaBrowserIntegration = true; }) (mkIf (cfg.kwinrc != {}) { diff --git a/nixos/modules/services/x11/desktop-managers/plasma6.nix b/nixos/modules/services/x11/desktop-managers/plasma6.nix new file mode 100644 index 0000000000000..8d84c5a7431b0 --- /dev/null +++ b/nixos/modules/services/x11/desktop-managers/plasma6.nix @@ -0,0 +1,277 @@ +{ + config, + lib, + pkgs, + utils, + ... +}: let + xcfg = config.services.xserver; + cfg = xcfg.desktopManager.plasma6; + + inherit (pkgs) kdePackages; + inherit (lib) literalExpression mkDefault mkIf mkOption mkPackageOptionMD types; +in { + options = { + services.xserver.desktopManager.plasma6 = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Enable the Plasma 6 (KDE 6) desktop environment."; + }; + + enableQt5Integration = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Enable Qt 5 integration (theming, etc). Disable for a pure Qt 6 system."; + }; + + notoPackage = mkPackageOptionMD pkgs "Noto fonts - used for UI by default" { + default = ["noto-fonts"]; + example = "noto-fonts-lgc-plus"; + }; + }; + + environment.plasma6.excludePackages = mkOption { + description = lib.mdDoc "List of default packages to exclude from the configuration"; + type = types.listOf types.package; + default = []; + example = literalExpression "[ pkgs.kdePackages.elisa ]"; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.enable -> !config.services.xserver.desktopManager.plasma5.enable; + message = "Cannot enable plasma5 and plasma6 at the same time!"; + } + ]; + + qt.enable = true; + environment.systemPackages = with kdePackages; let + requiredPackages = [ + # Hack? To make everything run on Wayland + qtwayland + # Needed to render SVG icons + qtsvg + + # Frameworks with globally loadable bits + frameworkintegration # provides Qt plugin + kauth # provides helper service + kcoreaddons # provides extra mime type info + kded # provides helper service + kfilemetadata # provides Qt plugins + kguiaddons # provides geo URL handlers + kiconthemes # provides Qt plugins + kimageformats # provides Qt plugins + kio # provides helper service + a bunch of other stuff + kpackage # provides kpackagetool tool + kservice # provides kbuildsycoca6 tool + kwallet # provides helper service + kwallet-pam # provides helper service + kwalletmanager # provides KCMs and stuff + plasma-activities # provides plasma-activities-cli tool + solid # provides solid-hardware6 tool + phonon-vlc # provides Phonon plugin + + # Core Plasma parts + kwin + pkgs.xwayland + + kscreen + libkscreen + + kscreenlocker + + kactivitymanagerd + kde-cli-tools + kglobalacceld + kwrited # wall message proxy, not to be confused with kwrite + + milou + polkit-kde-agent-1 + + plasma-desktop + plasma-workspace + + # Crash handler + drkonqi + + # Application integration + libplasma # provides Kirigami platform theme + plasma-integration # provides Qt platform theme + kde-gtk-config + + # Artwork + themes + breeze + breeze-icons + breeze-gtk + ocean-sound-theme + plasma-workspace-wallpapers + pkgs.hicolor-icon-theme # fallback icons + qqc2-breeze-style + qqc2-desktop-style + + # misc Plasma extras + kdeplasma-addons + + pkgs.xdg-user-dirs # recommended upstream + + # Plasma utilities + kmenuedit + + kinfocenter + plasma-systemmonitor + ksystemstats + libksysguard + + spectacle + systemsettings + kcmutils + + # Gear + baloo + dolphin + dolphin-plugins + ffmpegthumbs + kdegraphics-thumbnailers + kde-inotify-survey + kio-admin + kio-extras + kio-fuse + ]; + optionalPackages = [ + plasma-browser-integration + konsole + (lib.getBin qttools) # Expose qdbus in PATH + + ark + elisa + gwenview + okular + kate + khelpcenter + print-manager + ]; + in + requiredPackages + ++ utils.removePackagesByName optionalPackages config.environment.plasma6.excludePackages + ++ lib.optionals config.services.xserver.desktopManager.plasma6.enableQt5Integration [ + breeze.qt5 + plasma-integration.qt5 + pkgs.plasma5Packages.kwayland-integration + kio-extras-kf5 + ] + # Optional hardware support features + ++ lib.optionals config.hardware.bluetooth.enable [bluedevil bluez-qt pkgs.openobex pkgs.obexftp] + ++ lib.optional config.networking.networkmanager.enable plasma-nm + ++ lib.optional config.hardware.pulseaudio.enable plasma-pa + ++ lib.optional config.services.pipewire.pulse.enable plasma-pa + ++ lib.optional config.powerManagement.enable powerdevil + ++ lib.optional config.services.colord.enable colord-kde + ++ lib.optional config.services.hardware.bolt.enable plasma-thunderbolt + ++ lib.optionals config.services.samba.enable [kdenetwork-filesharing pkgs.samba] + ++ lib.optional config.services.xserver.wacom.enable wacomtablet + ++ lib.optional config.services.flatpak.enable flatpak-kcm; + + environment.pathsToLink = [ + # FIXME: modules should link subdirs of `/share` rather than relying on this + "/share" + "/libexec" # for drkonqi + ]; + + environment.etc."X11/xkb".source = xcfg.xkb.dir; + + # Add ~/.config/kdedefaults to XDG_CONFIG_DIRS for shells, since Plasma sets that. + # FIXME: maybe we should append to XDG_CONFIG_DIRS in /etc/set-environment instead? + environment.sessionVariables.XDG_CONFIG_DIRS = ["$HOME/.config/kdedefaults"]; + + # Needed for things that depend on other store.kde.org packages to install correctly, + # notably Plasma look-and-feel packages (a.k.a. Global Themes) + # + # FIXME: this is annoyingly impure and should really be fixed at source level somehow, + # but kpackage is a library so we can't just wrap the one thing invoking it and be done. + # This also means things won't work for people not on Plasma, but at least this way it + # works for SOME people. + environment.sessionVariables.KPACKAGE_DEP_RESOLVERS_PATH = "${kdePackages.frameworkintegration.out}/libexec/kf6/kpackagehandlers"; + + # Enable GTK applications to load SVG icons + services.xserver.gdk-pixbuf.modulePackages = [pkgs.librsvg]; + + fonts.packages = [cfg.notoPackage pkgs.hack-font]; + fonts.fontconfig.defaultFonts = { + monospace = ["Hack" "Noto Sans Mono"]; + sansSerif = ["Noto Sans"]; + serif = ["Noto Serif"]; + }; + + programs.ssh.askPassword = mkDefault "${kdePackages.ksshaskpass.out}/bin/ksshaskpass"; + + # Enable helpful DBus services. + services.accounts-daemon.enable = true; + # when changing an account picture the accounts-daemon reads a temporary file containing the image which systemsettings5 may place under /tmp + systemd.services.accounts-daemon.serviceConfig.PrivateTmp = false; + + services.power-profiles-daemon.enable = mkDefault true; + services.system-config-printer.enable = mkIf config.services.printing.enable (mkDefault true); + services.udisks2.enable = true; + services.upower.enable = config.powerManagement.enable; + services.xserver.libinput.enable = mkDefault true; + + # Extra UDEV rules used by Solid + services.udev.packages = [ + # libmtp has "bin", "dev", "out" outputs. UDEV rules file is in "out". + pkgs.libmtp.out + pkgs.media-player-info + ]; + + # Set up Dr. Konqi as crash handler + systemd.packages = [kdePackages.drkonqi]; + systemd.services."drkonqi-coredump-processor@".wantedBy = ["systemd-coredump@.service"]; + + xdg.portal.enable = true; + xdg.portal.extraPortals = [kdePackages.xdg-desktop-portal-kde]; + xdg.portal.configPackages = mkDefault [kdePackages.xdg-desktop-portal-kde]; + services.pipewire.enable = mkDefault true; + + services.xserver.displayManager = { + sessionPackages = [kdePackages.plasma-workspace]; + defaultSession = mkDefault "plasma"; + }; + services.xserver.displayManager.sddm = { + package = kdePackages.sddm; + theme = mkDefault "breeze"; + extraPackages = with kdePackages; [ + breeze-icons + kirigami + plasma5support + qtsvg + qtvirtualkeyboard + ]; + }; + + security.pam.services = { + login.kwallet = { + enable = true; + package = kdePackages.kwallet-pam; + }; + kde.kwallet = { + enable = true; + package = kdePackages.kwallet-pam; + }; + kde-fingerprint = lib.mkIf config.services.fprintd.enable { fprintAuth = true; }; + kde-smartcard = lib.mkIf config.security.pam.p11.enable { p11Auth = true; }; + }; + + programs.dconf.enable = true; + + programs.firefox.nativeMessagingHosts.packages = [kdePackages.plasma-browser-integration]; + + programs.chromium = { + enablePlasmaBrowserIntegration = true; + plasmaBrowserIntegrationPackage = pkgs.kdePackages.plasma-browser-integration; + }; + + programs.kdeconnect.package = kdePackages.kdeconnect-kde; + }; +} diff --git a/nixos/modules/services/x11/display-managers/sddm.nix b/nixos/modules/services/x11/display-managers/sddm.nix index 0576619cc8d28..5b7f4bc58d80c 100644 --- a/nixos/modules/services/x11/display-managers/sddm.nix +++ b/nixos/modules/services/x11/display-managers/sddm.nix @@ -7,7 +7,10 @@ let cfg = dmcfg.sddm; xEnv = config.systemd.services.display-manager.environment; - sddm = cfg.package; + sddm = cfg.package.override(old: { + withWayland = cfg.wayland.enable; + extraPackages = old.extraPackages or [] ++ cfg.extraPackages; + }); iniFmt = pkgs.formats.ini { }; @@ -140,6 +143,15 @@ in ''; }; + extraPackages = mkOption { + type = types.listOf types.package; + default = []; + defaultText = "[]"; + description = lib.mdDoc '' + Extra Qt plugins / QML libraries to add to the environment. + ''; + }; + autoNumlock = mkOption { type = types.bool; default = false; @@ -211,7 +223,7 @@ in keymap_variant = xcfg.xkb.variant; keymap_options = xcfg.xkb.options; }; - }; in "${pkgs.weston}/bin/weston --shell=fullscreen-shell.so -c ${westonIni}"; + }; in "${pkgs.weston}/bin/weston --shell=kiosk -c ${westonIni}"; description = lib.mdDoc "Command used to start the selected compositor"; }; }; @@ -235,15 +247,7 @@ in } ]; - services.xserver.displayManager.job = { - environment = { - # Load themes from system environment - QT_PLUGIN_PATH = "/run/current-system/sw/" + pkgs.qt5.qtbase.qtPluginPrefix; - QML2_IMPORT_PATH = "/run/current-system/sw/" + pkgs.qt5.qtbase.qtQmlPrefix; - }; - - execCmd = "exec /run/current-system/sw/bin/sddm"; - }; + services.xserver.displayManager.job.execCmd = "exec /run/current-system/sw/bin/sddm"; security.pam.services = { sddm.text = '' diff --git a/nixos/modules/services/x11/window-managers/icewm.nix b/nixos/modules/services/x11/window-managers/icewm.nix index 48741aa41d853..e3cb5cc3be2ba 100644 --- a/nixos/modules/services/x11/window-managers/icewm.nix +++ b/nixos/modules/services/x11/window-managers/icewm.nix @@ -17,7 +17,7 @@ in { name = "icewm"; start = '' - ${pkgs.icewm}/bin/icewm & + ${pkgs.icewm}/bin/icewm-session & waitPID=$! ''; }; diff --git a/nixos/modules/system/boot/kernel.nix b/nixos/modules/system/boot/kernel.nix index a46331ccd431d..b0ac857feb4b8 100644 --- a/nixos/modules/system/boot/kernel.nix +++ b/nixos/modules/system/boot/kernel.nix @@ -81,6 +81,13 @@ in extraStructuredConfig.FOO = lib.kernel.yes; features.foo = true; } + { + name = "foo-ml-mbox"; + patch = (fetchurl { + url = "https://lore.kernel.org/lkml/19700205182810.58382-1-email@domain/t.mbox.gz"; + hash = "sha256-..."; + }); + } ] ''; description = lib.mdDoc '' 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 a9978d7adf803..258cf622a894a 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py @@ -17,6 +17,9 @@ from dataclasses import dataclass # These values will be replaced with actual values during the package build EFI_SYS_MOUNT_POINT = "@efiSysMountPoint@" +BOOT_MOUNT_POINT = "@bootMountPoint@" +LOADER_CONF = f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf" # Always stored on the ESP +NIXOS_DIR = "@nixosDir@" TIMEOUT = "@timeout@" EDITOR = "@editor@" == "1" CONSOLE_MODE = "@consoleMode@" @@ -28,6 +31,7 @@ CONFIGURATION_LIMIT = int("@configurationLimit@") CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@" GRACEFUL = "@graceful@" COPY_EXTRA_FILES = "@copyExtraFiles@" +CHECK_MOUNTPOINTS = "@checkMountpoints@" @dataclass class BootSpec: @@ -87,7 +91,7 @@ def generation_conf_filename(profile: str | None, generation: int, specialisatio def write_loader_conf(profile: str | None, generation: int, specialisation: str | None) -> None: - with open(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", 'w') as f: + with open(f"{LOADER_CONF}.tmp", 'w') as f: if TIMEOUT != "": f.write(f"timeout {TIMEOUT}\n") f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation)) @@ -96,7 +100,7 @@ def write_loader_conf(profile: str | None, generation: int, specialisation: str f.write(f"console-mode {CONSOLE_MODE}\n") f.flush() os.fsync(f.fileno()) - os.rename(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf") + os.rename(f"{LOADER_CONF}.tmp", LOADER_CONF) def get_bootspec(profile: str | None, generation: int) -> BootSpec: @@ -126,9 +130,9 @@ def copy_from_file(file: str, dry_run: bool = False) -> str: store_file_path = os.path.realpath(file) suffix = os.path.basename(store_file_path) store_dir = os.path.basename(os.path.dirname(store_file_path)) - efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix) + efi_file_path = f"{NIXOS_DIR}/{store_dir}-{suffix}.efi" if not dry_run: - copy_if_not_exists(store_file_path, f"{EFI_SYS_MOUNT_POINT}%s" % (efi_file_path)) + copy_if_not_exists(store_file_path, f"{BOOT_MOUNT_POINT}{efi_file_path}") return efi_file_path def write_entry(profile: str | None, generation: int, specialisation: str | None, @@ -145,7 +149,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None try: if bootspec.initrdSecrets is not None: - subprocess.check_call([bootspec.initrdSecrets, f"{EFI_SYS_MOUNT_POINT}%s" % (initrd)]) + subprocess.check_call([bootspec.initrdSecrets, f"{BOOT_MOUNT_POINT}%s" % (initrd)]) except subprocess.CalledProcessError: if current: print("failed to create initrd secrets!", file=sys.stderr) @@ -155,7 +159,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr) print("note: this is normal after having removed " "or renamed a file in `boot.initrd.secrets`", file=sys.stderr) - entry_file = f"{EFI_SYS_MOUNT_POINT}/loader/entries/%s" % ( + entry_file = f"{BOOT_MOUNT_POINT}/loader/entries/%s" % ( generation_conf_filename(profile, generation, specialisation)) tmp_path = "%s.tmp" % (entry_file) kernel_params = "init=%s " % bootspec.init @@ -202,14 +206,14 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]: def remove_old_entries(gens: list[SystemIdentifier]) -> None: - rex_profile = re.compile(r"^" + re.escape(EFI_SYS_MOUNT_POINT) + "/loader/entries/nixos-(.*)-generation-.*\.conf$") - rex_generation = re.compile(r"^" + re.escape(EFI_SYS_MOUNT_POINT) + "/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$") + rex_profile = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + "/loader/entries/nixos-(.*)-generation-.*\.conf$") + rex_generation = re.compile(r"^" + re.escape(BOOT_MOUNT_POINT) + "/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$") known_paths = [] for gen in gens: bootspec = get_bootspec(gen.profile, gen.generation) known_paths.append(copy_from_file(bootspec.kernel, True)) known_paths.append(copy_from_file(bootspec.initrd, True)) - for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/loader/entries/nixos*-generation-[1-9]*.conf"): + for path in glob.iglob(f"{BOOT_MOUNT_POINT}/loader/entries/nixos*-generation-[1-9]*.conf"): if rex_profile.match(path): prof = rex_profile.sub(r"\1", path) else: @@ -220,11 +224,18 @@ def remove_old_entries(gens: list[SystemIdentifier]) -> None: continue if not (prof, gen_number, None) in gens: os.unlink(path) - for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/*"): + for path in glob.iglob(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/*"): if not path in known_paths and not os.path.isdir(path): os.unlink(path) +def cleanup_esp() -> None: + for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/loader/entries/nixos*"): + os.unlink(path) + if os.path.isdir(f"{EFI_SYS_MOUNT_POINT}/{NIXOS_DIR}"): + shutil.rmtree(f"{EFI_SYS_MOUNT_POINT}/{NIXOS_DIR}") + + def get_profiles() -> list[str]: if os.path.isdir("/nix/var/nix/profiles/system-profiles/"): return [x @@ -255,6 +266,9 @@ def install_bootloader(args: argparse.Namespace) -> None: # flags to pass to bootctl install/update bootctl_flags = [] + if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT: + bootctl_flags.append(f"--boot-path={BOOT_MOUNT_POINT}") + if CAN_TOUCH_EFI_VARIABLES != "1": bootctl_flags.append("--no-variables") @@ -263,8 +277,8 @@ def install_bootloader(args: argparse.Namespace) -> None: if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1": # bootctl uses fopen() with modes "wxe" and fails if the file exists. - if os.path.exists(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf"): - os.unlink(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf") + if os.path.exists(LOADER_CONF): + os.unlink(LOADER_CONF) subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["install"]) else: @@ -291,13 +305,15 @@ def install_bootloader(args: argparse.Namespace) -> None: print("updating systemd-boot from %s to %s" % (installed_version, available_version)) subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["update"]) - os.makedirs(f"{EFI_SYS_MOUNT_POINT}/efi/nixos", exist_ok=True) - os.makedirs(f"{EFI_SYS_MOUNT_POINT}/loader/entries", exist_ok=True) + os.makedirs(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}", exist_ok=True) + os.makedirs(f"{BOOT_MOUNT_POINT}/loader/entries", exist_ok=True) gens = get_generations() for profile in get_profiles(): gens += get_generations(profile) + remove_old_entries(gens) + for gen in gens: try: bootspec = get_bootspec(gen.profile, gen.generation) @@ -315,9 +331,15 @@ def install_bootloader(args: argparse.Namespace) -> None: else: raise e - for root, _, files in os.walk(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files", topdown=False): - relative_root = root.removeprefix(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files").removeprefix("/") - actual_root = os.path.join(f"{EFI_SYS_MOUNT_POINT}", relative_root) + if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT: + # Cleanup any entries in ESP if xbootldrMountPoint is set. + # If the user later unsets xbootldrMountPoint, entries in XBOOTLDR will not be cleaned up + # automatically, as we don't have information about the mount point anymore. + cleanup_esp() + + for root, _, files in os.walk(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files", topdown=False): + relative_root = root.removeprefix(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files").removeprefix("/") + actual_root = os.path.join(f"{BOOT_MOUNT_POINT}", relative_root) for file in files: actual_file = os.path.join(actual_root, file) @@ -330,7 +352,7 @@ def install_bootloader(args: argparse.Namespace) -> None: os.rmdir(actual_root) os.rmdir(root) - os.makedirs(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files", exist_ok=True) + os.makedirs(f"{BOOT_MOUNT_POINT}/{NIXOS_DIR}/.extra-files", exist_ok=True) subprocess.check_call(COPY_EXTRA_FILES) @@ -340,6 +362,8 @@ def main() -> None: parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help=f"The default {DISTRO_NAME} config to boot") args = parser.parse_args() + subprocess.check_call(CHECK_MOUNTPOINTS) + try: install_bootloader(args) finally: @@ -347,9 +371,14 @@ def main() -> None: # it can leave the system in an unbootable state, when a crash/outage # happens shortly after an update. To decrease the likelihood of this # event sync the efi filesystem after each update. - rc = libc.syncfs(os.open(f"{EFI_SYS_MOUNT_POINT}", os.O_RDONLY)) + rc = libc.syncfs(os.open(f"{BOOT_MOUNT_POINT}", os.O_RDONLY)) if rc != 0: - print(f"could not sync {EFI_SYS_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr) + print(f"could not sync {BOOT_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr) + + if BOOT_MOUNT_POINT != EFI_SYS_MOUNT_POINT: + rc = libc.syncfs(os.open(EFI_SYS_MOUNT_POINT, os.O_RDONLY)) + if rc != 0: + print(f"could not sync {EFI_SYS_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr) if __name__ == '__main__': 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 ea4553b8208f6..645b764760dad 100644 --- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix +++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix @@ -7,7 +7,7 @@ let efi = config.boot.loader.efi; - systemdBootBuilder = pkgs.substituteAll { + systemdBootBuilder = pkgs.substituteAll rec { src = ./systemd-boot-builder.py; isExecutable = true; @@ -28,33 +28,48 @@ let inherit (efi) efiSysMountPoint canTouchEfiVariables; + bootMountPoint = if cfg.xbootldrMountPoint != null + then cfg.xbootldrMountPoint + else efi.efiSysMountPoint; + + nixosDir = "/EFI/nixos"; + inherit (config.system.nixos) distroName; memtest86 = optionalString cfg.memtest86.enable pkgs.memtest86plus; netbootxyz = optionalString cfg.netbootxyz.enable pkgs.netbootxyz-efi; + checkMountpoints = pkgs.writeShellScript "check-mountpoints" '' + fail() { + echo "$1 = '$2' is not a mounted partition. Is the path configured correctly?" >&2 + exit 1 + } + ${pkgs.util-linuxMinimal}/bin/findmnt ${efiSysMountPoint} > /dev/null || fail efiSysMountPoint ${efiSysMountPoint} + ${lib.optionalString + (cfg.xbootldrMountPoint != null) + "${pkgs.util-linuxMinimal}/bin/findmnt ${cfg.xbootldrMountPoint} > /dev/null || fail xbootldrMountPoint ${cfg.xbootldrMountPoint}"} + ''; + copyExtraFiles = pkgs.writeShellScript "copy-extra-files" '' empty_file=$(${pkgs.coreutils}/bin/mktemp) ${concatStrings (mapAttrsToList (n: v: '' - ${pkgs.coreutils}/bin/install -Dp "${v}" "${efi.efiSysMountPoint}/"${escapeShellArg n} - ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -Dp "${v}" "${bootMountPoint}/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/"${escapeShellArg n} '') cfg.extraFiles)} ${concatStrings (mapAttrsToList (n: v: '' - ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${efi.efiSysMountPoint}/loader/entries/"${escapeShellArg n} - ${pkgs.coreutils}/bin/install -D $empty_file "${efi.efiSysMountPoint}/efi/nixos/.extra-files/loader/entries/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -Dp "${pkgs.writeText n v}" "${bootMountPoint}/loader/entries/"${escapeShellArg n} + ${pkgs.coreutils}/bin/install -D $empty_file "${bootMountPoint}/${nixosDir}/.extra-files/loader/entries/"${escapeShellArg n} '') cfg.extraEntries)} ''; }; - checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" { - nativeBuildInputs = [ pkgs.mypy ]; - } '' + checkedSystemdBootBuilder = pkgs.runCommand "systemd-boot" { } '' mkdir -p $out/bin install -m755 ${systemdBootBuilder} $out/bin/systemd-boot-builder - mypy \ + ${lib.getExe pkgs.buildPackages.mypy} \ --no-implicit-optional \ --disallow-untyped-calls \ --disallow-untyped-defs \ @@ -101,6 +116,18 @@ in { ''; }; + xbootldrMountPoint = mkOption { + default = null; + type = types.nullOr types.str; + description = lib.mdDoc '' + Where the XBOOTLDR partition is mounted. + + If set, this partition will be used as $BOOT to store boot loader entries and extra files + instead of the EFI partition. As per the bootloader specification, it is recommended that + the EFI and XBOOTLDR partitions be mounted at `/efi` and `/boot`, respectively. + ''; + }; + configurationLimit = mkOption { default = null; example = 120; @@ -110,7 +137,7 @@ in { Useful to prevent boot partition running out of disk space. `null` means no limit i.e. all generations - that were not garbage collected yet. + that have not been garbage collected yet. ''; }; @@ -202,7 +229,7 @@ in { ''; description = lib.mdDoc '' Any additional entries you want added to the `systemd-boot` menu. - These entries will be copied to {file}`/boot/loader/entries`. + These entries will be copied to {file}`$BOOT/loader/entries`. Each attribute name denotes the destination file name, and the corresponding attribute value is the contents of the entry. @@ -219,9 +246,9 @@ in { { "efi/memtest86/memtest.efi" = "''${pkgs.memtest86plus}/memtest.efi"; } ''; description = lib.mdDoc '' - A set of files to be copied to {file}`/boot`. + A set of files to be copied to {file}`$BOOT`. Each attribute name denotes the destination file name in - {file}`/boot`, while the corresponding + {file}`$BOOT`, while the corresponding attribute value specifies the source file. ''; }; @@ -246,6 +273,18 @@ in { config = mkIf cfg.enable { assertions = [ { + assertion = (hasPrefix "/" efi.efiSysMountPoint); + message = "The ESP mount point '${efi.efiSysMountPoint}' must be an absolute path"; + } + { + assertion = cfg.xbootldrMountPoint == null || (hasPrefix "/" cfg.xbootldrMountPoint); + message = "The XBOOTLDR mount point '${cfg.xbootldrMountPoint}' must be an absolute path"; + } + { + assertion = cfg.xbootldrMountPoint != efi.efiSysMountPoint; + message = "The XBOOTLDR mount point '${cfg.xbootldrMountPoint}' cannot be the same as the ESP mount point '${efi.efiSysMountPoint}'"; + } + { assertion = (config.boot.kernelPackages.kernel.features or { efiBootStub = true; }) ? efiBootStub; message = "This kernel does not support the EFI boot stub"; } diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index a7399bd55e770..88d6a2ded873c 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -647,9 +647,9 @@ let "BatmanAdvanced" ]) # Note: For DHCP the values both, none, v4, v6 are deprecated - (assertValueOneOf "DHCP" ["yes" "no" "ipv4" "ipv6"]) + (assertValueOneOf "DHCP" (boolValues ++ ["ipv4" "ipv6"])) (assertValueOneOf "DHCPServer" boolValues) - (assertValueOneOf "LinkLocalAddressing" ["yes" "no" "ipv4" "ipv6" "fallback" "ipv4-fallback"]) + (assertValueOneOf "LinkLocalAddressing" (boolValues ++ ["ipv4" "ipv6" "fallback" "ipv4-fallback"])) (assertValueOneOf "IPv6LinkLocalAddressGenerationMode" ["eui64" "none" "stable-privacy" "random"]) (assertValueOneOf "IPv4LLRoute" boolValues) (assertValueOneOf "DefaultRouteOnDevice" boolValues) diff --git a/nixos/modules/system/boot/plymouth.nix b/nixos/modules/system/boot/plymouth.nix index b041b8951fa37..16bca40993aeb 100644 --- a/nixos/modules/system/boot/plymouth.nix +++ b/nixos/modules/system/boot/plymouth.nix @@ -186,6 +186,8 @@ in # module might come from a theme cp ${themesEnv}/lib/plymouth/*.so $out cp ${plymouth}/lib/plymouth/renderers/*.so $out/renderers + # useless in the initrd, and adds several megabytes to the closure + rm $out/renderers/x11.so ''; "/etc/plymouth/themes".source = pkgs.runCommand "plymouth-initrd-themes" {} '' # Check if the actual requested theme is here @@ -271,6 +273,8 @@ in # module might come from a theme cp ${themesEnv}/lib/plymouth/*.so $out/lib/plymouth cp ${plymouth}/lib/plymouth/renderers/*.so $out/lib/plymouth/renderers + # useless in the initrd, and adds several megabytes to the closure + rm $out/lib/plymouth/renderers/x11.so mkdir -p $out/share/plymouth/themes cp ${plymouth}/share/plymouth/plymouthd.defaults $out/share/plymouth diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix index e990aeea7a14b..90a74c0ac5788 100644 --- a/nixos/modules/system/boot/stage-1.nix +++ b/nixos/modules/system/boot/stage-1.nix @@ -3,7 +3,7 @@ # the modules necessary to mount the root file system, then calls the # init in the root file system to start the second boot stage. -{ config, lib, utils, pkgs, ... }: +{ config, options, lib, utils, pkgs, ... }: with lib; @@ -621,6 +621,11 @@ in path the secret should have inside the initrd, the value is the path it should be copied from (or null for the same path inside and out). + + Note that `nixos-rebuild switch` will generate the initrd + also for past generations, so if secrets are moved or deleted + you will also have to garbage collect the generations that + use those secrets. ''; example = literalExpression '' @@ -631,10 +636,8 @@ in }; boot.initrd.supportedFilesystems = mkOption { - default = [ ]; - example = [ "btrfs" ]; - type = types.listOf types.str; - description = lib.mdDoc "Names of supported filesystem types in the initial ramdisk."; + default = { }; + inherit (options.boot.supportedFilesystems) example type description; }; boot.initrd.verbose = mkOption { diff --git a/nixos/modules/system/boot/systemd/coredump.nix b/nixos/modules/system/boot/systemd/coredump.nix index 03ef00e5683c1..271d8f86d0e61 100644 --- a/nixos/modules/system/boot/systemd/coredump.nix +++ b/nixos/modules/system/boot/systemd/coredump.nix @@ -52,7 +52,7 @@ in { # See: https://github.com/NixOS/nixpkgs/issues/213408 pkgs.substitute { src = "${systemd}/example/sysctl.d/50-coredump.conf"; - replacements = [ + substitutions = [ "--replace" "${systemd}" "${pkgs.symlinkJoin { name = "systemd"; paths = [ systemd ]; }}" diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 9641921fc795a..f83837fbc6d41 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -90,8 +90,6 @@ let inherit (cfg) packages package; }; - fileSystems = filter utils.fsNeededForBoot config.system.build.fileSystems; - kernel-name = config.boot.kernelPackages.kernel.name or "kernel"; modulesTree = config.system.modulesTree.override { name = kernel-name + "-modules"; }; firmware = config.hardware.firmware; diff --git a/nixos/modules/system/boot/systemd/oomd.nix b/nixos/modules/system/boot/systemd/oomd.nix index 000b18c01609a..edc25784367a4 100644 --- a/nixos/modules/system/boot/systemd/oomd.nix +++ b/nixos/modules/system/boot/systemd/oomd.nix @@ -49,15 +49,15 @@ in { systemd.slices."-".sliceConfig = lib.mkIf cfg.enableRootSlice { ManagedOOMMemoryPressure = "kill"; - ManagedOOMMemoryPressureLimit = "80%"; + ManagedOOMMemoryPressureLimit = lib.mkDefault "80%"; }; systemd.slices."system".sliceConfig = lib.mkIf cfg.enableSystemSlice { ManagedOOMMemoryPressure = "kill"; - ManagedOOMMemoryPressureLimit = "80%"; + ManagedOOMMemoryPressureLimit = lib.mkDefault "80%"; }; systemd.slices."user-".sliceConfig = lib.mkIf cfg.enableUserSlices { ManagedOOMMemoryPressure = "kill"; - ManagedOOMMemoryPressureLimit = "80%"; + ManagedOOMMemoryPressureLimit = lib.mkDefault "80%"; }; systemd.user.units."slice" = lib.mkIf cfg.enableUserSlices { text = '' diff --git a/nixos/modules/system/boot/systemd/repart.nix b/nixos/modules/system/boot/systemd/repart.nix index 3be744acd0b3b..6cc387cb6f43f 100644 --- a/nixos/modules/system/boot/systemd/repart.nix +++ b/nixos/modules/system/boot/systemd/repart.nix @@ -10,6 +10,20 @@ let "repart.d" format (lib.mapAttrs (_n: v: { Partition = v; }) cfg.partitions); + + partitionAssertions = lib.mapAttrsToList (fileName: definition: + let + maxLabelLength = 36; # GPT_LABEL_MAX defined in systemd's gpt.h + labelLength = builtins.stringLength definition.Label; + in + { + assertion = definition ? Label -> maxLabelLength >= labelLength; + message = '' + The partition label '${definition.Label}' defined for '${fileName}' is ${toString labelLength} + characters long, but the maximum label length supported by systemd is ${toString maxLabelLength}. + ''; + } + ) cfg.partitions; in { options = { @@ -81,7 +95,7 @@ in 'boot.initrd.systemd.repart.enable' requires 'boot.initrd.systemd.enable' to be enabled. ''; } - ]; + ] ++ partitionAssertions; # systemd-repart uses loopback devices for partition creation boot.initrd.availableKernelModules = lib.optional initrdCfg.enable "loop"; diff --git a/nixos/modules/system/boot/uki.nix b/nixos/modules/system/boot/uki.nix index 63c4e0c0e3913..ce00ac8e63978 100644 --- a/nixos/modules/system/boot/uki.nix +++ b/nixos/modules/system/boot/uki.nix @@ -27,6 +27,20 @@ in description = lib.mdDoc "Version of the image or generation the UKI belongs to"; }; + tries = lib.mkOption { + type = lib.types.nullOr lib.types.ints.unsigned; + default = null; + description = lib.mdDoc '' + Number of boot attempts before this UKI is considered bad. + + If no tries are specified (the default) automatic boot assessment remains inactive. + + See documentation on [Automatic Boot Assessment](https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/) and + [boot counting](https://uapi-group.org/specifications/specs/boot_loader_specification/#boot-counting) + for more information. + ''; + }; + settings = lib.mkOption { type = format.type; description = lib.mdDoc '' @@ -51,16 +65,16 @@ in else "nixos"); - boot.uki.settings = lib.mkOptionDefault { + boot.uki.settings = { UKI = { - Linux = "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}"; - Initrd = "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; - Cmdline = "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}"; - Stub = "${pkgs.systemd}/lib/systemd/boot/efi/linux${efiArch}.efi.stub"; - Uname = "${config.boot.kernelPackages.kernel.modDirVersion}"; - OSRelease = "@${config.system.build.etc}/etc/os-release"; + Linux = lib.mkOptionDefault "${config.boot.kernelPackages.kernel}/${config.system.boot.loader.kernelFile}"; + Initrd = lib.mkOptionDefault "${config.system.build.initialRamdisk}/${config.system.boot.loader.initrdFile}"; + Cmdline = lib.mkOptionDefault "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}"; + Stub = lib.mkOptionDefault "${pkgs.systemd}/lib/systemd/boot/efi/linux${efiArch}.efi.stub"; + Uname = lib.mkOptionDefault "${config.boot.kernelPackages.kernel.modDirVersion}"; + OSRelease = lib.mkOptionDefault "@${config.system.build.etc}/etc/os-release"; # This is needed for cross compiling. - EFIArch = efiArch; + EFIArch = lib.mkOptionDefault efiArch; }; }; @@ -69,8 +83,9 @@ in name = config.boot.uki.name; version = config.boot.uki.version; versionInfix = if version != null then "_${version}" else ""; + triesInfix = if cfg.tries != null then "+${builtins.toString cfg.tries}" else ""; in - name + versionInfix + ".efi"; + name + versionInfix + triesInfix + ".efi"; system.build.uki = pkgs.runCommand config.system.boot.loader.ukiFile { } '' mkdir -p $out diff --git a/nixos/modules/system/etc/build-composefs-dump.py b/nixos/modules/system/etc/build-composefs-dump.py index bf4ec791ecf7d..bba454dd888d6 100644 --- a/nixos/modules/system/etc/build-composefs-dump.py +++ b/nixos/modules/system/etc/build-composefs-dump.py @@ -199,7 +199,8 @@ def main() -> None: size=os.stat(source).st_size, filetype=FileType.file, mode=mode, - payload=target, + # payload needs to be relative path in this case + payload=target.lstrip("/"), ) paths[target] = composefs_path add_leading_directories(target, attrs, paths) diff --git a/nixos/modules/system/etc/etc.nix b/nixos/modules/system/etc/etc.nix index baf37ba6def34..9f735364196c6 100644 --- a/nixos/modules/system/etc/etc.nix +++ b/nixos/modules/system/etc/etc.nix @@ -238,7 +238,9 @@ in # this should not run because /etc is mounted via a systemd mount unit # instead. To a large extent this mimics what composefs does. Because # it's relatively simple, however, we avoid the composefs dependency. - if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]]; then + # Since this script is not idempotent, it should not run when etc hasn't + # changed. + if [[ ! $IN_NIXOS_SYSTEMD_STAGE1 ]] && [[ "${config.system.build.etc}/etc" != "$(readlink -f /run/current-system/etc)" ]]; then echo "remounting /etc..." tmpMetadataMount=$(mktemp --directory) diff --git a/nixos/modules/tasks/filesystems.nix b/nixos/modules/tasks/filesystems.nix index 1378a0090c1df..e72a1e37759e9 100644 --- a/nixos/modules/tasks/filesystems.nix +++ b/nixos/modules/tasks/filesystems.nix @@ -246,10 +246,23 @@ in }; boot.supportedFilesystems = mkOption { - default = [ ]; - example = [ "btrfs" ]; - type = types.listOf types.str; - description = lib.mdDoc "Names of supported filesystem types."; + default = { }; + example = lib.literalExpression '' + { + btrfs = true; + zfs = lib.mkForce false; + } + ''; + type = types.coercedTo + (types.listOf types.str) + (enabled: lib.listToAttrs (map (fs: lib.nameValuePair fs true) enabled)) + (types.attrsOf types.bool); + description = lib.mdDoc '' + Names of supported filesystem types, or an attribute set of file system types + and their state. The set form may be used together with `lib.mkForce` to + explicitly disable support for specific filesystems, e.g. to disable ZFS + with an unsupported kernel. + ''; }; boot.specialFileSystems = mkOption { diff --git a/nixos/modules/tasks/filesystems/apfs.nix b/nixos/modules/tasks/filesystems/apfs.nix index 2f2be351df61b..980a3ad0f9c4b 100644 --- a/nixos/modules/tasks/filesystems/apfs.nix +++ b/nixos/modules/tasks/filesystems/apfs.nix @@ -4,12 +4,12 @@ with lib; let - inInitrd = any (fs: fs == "apfs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.apfs or false; in { - config = mkIf (any (fs: fs == "apfs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.apfs or false) { system.fsPackages = [ pkgs.apfsprogs ]; diff --git a/nixos/modules/tasks/filesystems/bcachefs.nix b/nixos/modules/tasks/filesystems/bcachefs.nix index 3b990ce30b21e..ba33edd702f70 100644 --- a/nixos/modules/tasks/filesystems/bcachefs.nix +++ b/nixos/modules/tasks/filesystems/bcachefs.nix @@ -118,7 +118,7 @@ let in { - config = lib.mkIf (lib.elem "bcachefs" config.boot.supportedFilesystems) (lib.mkMerge [ + config = lib.mkIf (config.boot.supportedFilesystems.bcachefs or false) (lib.mkMerge [ { inherit assertions; # needed for systemd-remount-fs @@ -133,7 +133,7 @@ in }; } - (lib.mkIf ((lib.elem "bcachefs" config.boot.initrd.supportedFilesystems) || (bootFs != {})) { + (lib.mkIf ((config.boot.initrd.supportedFilesystems.bcachefs or false) || (bootFs != {})) { inherit assertions; # chacha20 and poly1305 are required only for decryption attempts boot.initrd.availableKernelModules = [ "bcachefs" "sha256" "chacha20" "poly1305" ]; diff --git a/nixos/modules/tasks/filesystems/btrfs.nix b/nixos/modules/tasks/filesystems/btrfs.nix index 87fe326c09740..8494a06f97a26 100644 --- a/nixos/modules/tasks/filesystems/btrfs.nix +++ b/nixos/modules/tasks/filesystems/btrfs.nix @@ -4,8 +4,8 @@ with lib; let - inInitrd = any (fs: fs == "btrfs") config.boot.initrd.supportedFilesystems; - inSystem = any (fs: fs == "btrfs") config.boot.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.btrfs or false; + inSystem = config.boot.supportedFilesystems.btrfs or false; cfgScrub = config.services.btrfs.autoScrub; diff --git a/nixos/modules/tasks/filesystems/cifs.nix b/nixos/modules/tasks/filesystems/cifs.nix index 837b9e19bfb9d..5a562b2940f7d 100644 --- a/nixos/modules/tasks/filesystems/cifs.nix +++ b/nixos/modules/tasks/filesystems/cifs.nix @@ -4,14 +4,14 @@ with lib; let - inInitrd = any (fs: fs == "cifs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.cifs or false; in { config = { - system.fsPackages = mkIf (any (fs: fs == "cifs") config.boot.supportedFilesystems) [ pkgs.cifs-utils ]; + system.fsPackages = mkIf (config.boot.supportedFilesystems.cifs or false) [ pkgs.cifs-utils ]; boot.initrd.availableKernelModules = mkIf inInitrd [ "cifs" "nls_utf8" "hmac" "md4" "ecb" "des_generic" "sha256" ]; diff --git a/nixos/modules/tasks/filesystems/ecryptfs.nix b/nixos/modules/tasks/filesystems/ecryptfs.nix index 8138e65916109..f966a1be15360 100644 --- a/nixos/modules/tasks/filesystems/ecryptfs.nix +++ b/nixos/modules/tasks/filesystems/ecryptfs.nix @@ -4,7 +4,7 @@ with lib; { - config = mkIf (any (fs: fs == "ecryptfs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.ecryptfs or false) { system.fsPackages = [ pkgs.ecryptfs ]; security.wrappers = { "mount.ecryptfs_private" = diff --git a/nixos/modules/tasks/filesystems/erofs.nix b/nixos/modules/tasks/filesystems/erofs.nix index a3d6576693505..b13fa25315579 100644 --- a/nixos/modules/tasks/filesystems/erofs.nix +++ b/nixos/modules/tasks/filesystems/erofs.nix @@ -2,8 +2,8 @@ let - inInitrd = lib.any (fs: fs == "erofs") config.boot.initrd.supportedFilesystems; - inSystem = lib.any (fs: fs == "erofs") config.boot.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.erofs or false; + inSystem = config.boot.supportedFilesystems.erofs or false; in diff --git a/nixos/modules/tasks/filesystems/exfat.nix b/nixos/modules/tasks/filesystems/exfat.nix index 540b9b91c3ec8..4011653c00df1 100644 --- a/nixos/modules/tasks/filesystems/exfat.nix +++ b/nixos/modules/tasks/filesystems/exfat.nix @@ -3,7 +3,7 @@ with lib; { - config = mkIf (any (fs: fs == "exfat") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.exfat or false) { system.fsPackages = if config.boot.kernelPackages.kernelOlder "5.7" then [ pkgs.exfat # FUSE ] else [ diff --git a/nixos/modules/tasks/filesystems/ext.nix b/nixos/modules/tasks/filesystems/ext.nix index 1c34ee2c70356..165fe9474c3e8 100644 --- a/nixos/modules/tasks/filesystems/ext.nix +++ b/nixos/modules/tasks/filesystems/ext.nix @@ -2,8 +2,10 @@ let - inInitrd = lib.any (fs: fs == "ext2" || fs == "ext3" || fs == "ext4") config.boot.initrd.supportedFilesystems; - inSystem = lib.any (fs: fs == "ext2" || fs == "ext3" || fs == "ext4") config.boot.supportedFilesystems; + hasExtX = s: s.ext2 or s.ext3 or s.ext4 or false; + + inInitrd = hasExtX config.boot.initrd.supportedFilesystems; + inSystem = hasExtX config.boot.supportedFilesystems; in diff --git a/nixos/modules/tasks/filesystems/f2fs.nix b/nixos/modules/tasks/filesystems/f2fs.nix index 4f99f9a57fa6d..f4f5fcab9caef 100644 --- a/nixos/modules/tasks/filesystems/f2fs.nix +++ b/nixos/modules/tasks/filesystems/f2fs.nix @@ -3,11 +3,10 @@ with lib; let - inInitrd = any (fs: fs == "f2fs") config.boot.initrd.supportedFilesystems; - fileSystems = filter (x: x.fsType == "f2fs") config.system.build.fileSystems; + inInitrd = config.boot.initrd.supportedFilesystems.f2fs or false; in { - config = mkIf (any (fs: fs == "f2fs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.f2fs or false) { system.fsPackages = [ pkgs.f2fs-tools ]; diff --git a/nixos/modules/tasks/filesystems/glusterfs.nix b/nixos/modules/tasks/filesystems/glusterfs.nix index e8c7fa8efbae1..02ef95262dbda 100644 --- a/nixos/modules/tasks/filesystems/glusterfs.nix +++ b/nixos/modules/tasks/filesystems/glusterfs.nix @@ -3,7 +3,7 @@ with lib; { - config = mkIf (any (fs: fs == "glusterfs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.glusterfs or false) { system.fsPackages = [ pkgs.glusterfs ]; diff --git a/nixos/modules/tasks/filesystems/jfs.nix b/nixos/modules/tasks/filesystems/jfs.nix index b5132b4caa334..73ddb0fb18bb6 100644 --- a/nixos/modules/tasks/filesystems/jfs.nix +++ b/nixos/modules/tasks/filesystems/jfs.nix @@ -3,10 +3,10 @@ with lib; let - inInitrd = any (fs: fs == "jfs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.jfs or false; in { - config = mkIf (any (fs: fs == "jfs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.jfs or false) { system.fsPackages = [ pkgs.jfsutils ]; diff --git a/nixos/modules/tasks/filesystems/nfs.nix b/nixos/modules/tasks/filesystems/nfs.nix index 8c631f0772db1..462568b5db3e9 100644 --- a/nixos/modules/tasks/filesystems/nfs.nix +++ b/nixos/modules/tasks/filesystems/nfs.nix @@ -4,7 +4,7 @@ with lib; let - inInitrd = any (fs: fs == "nfs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.nfs or false; nfsStateDir = "/var/lib/nfs"; @@ -58,7 +58,7 @@ in ###### implementation - config = mkIf (any (fs: fs == "nfs" || fs == "nfs4") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.nfs or config.boot.supportedFilesystems.nfs4 or false) { services.rpcbind.enable = true; diff --git a/nixos/modules/tasks/filesystems/ntfs.nix b/nixos/modules/tasks/filesystems/ntfs.nix index c40d2a1a80bc5..99ba494a7a39e 100644 --- a/nixos/modules/tasks/filesystems/ntfs.nix +++ b/nixos/modules/tasks/filesystems/ntfs.nix @@ -3,7 +3,7 @@ with lib; { - config = mkIf (any (fs: fs == "ntfs" || fs == "ntfs-3g") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.ntfs or config.boot.supportedFilesystems.ntfs-3g or false) { system.fsPackages = [ pkgs.ntfs3g ]; diff --git a/nixos/modules/tasks/filesystems/overlayfs.nix b/nixos/modules/tasks/filesystems/overlayfs.nix new file mode 100644 index 0000000000000..e71ef9ba62e9c --- /dev/null +++ b/nixos/modules/tasks/filesystems/overlayfs.nix @@ -0,0 +1,144 @@ +{ config, lib, pkgs, utils, ... }: + +let + # The scripted initrd contains some magic to add the prefix to the + # paths just in time, so we don't add it here. + sysrootPrefix = fs: + if config.boot.initrd.systemd.enable && (utils.fsNeededForBoot fs) then + "/sysroot" + else + ""; + + # Returns a service that creates the required directories before the mount is + # created. + preMountService = _name: fs: + let + prefix = sysrootPrefix fs; + + escapedMountpoint = utils.escapeSystemdPath (prefix + fs.mountPoint); + mountUnit = "${escapedMountpoint}.mount"; + + upperdir = prefix + fs.overlay.upperdir; + workdir = prefix + fs.overlay.workdir; + in + lib.mkIf (fs.overlay.upperdir != null) + { + "rw-${escapedMountpoint}" = { + requiredBy = [ mountUnit ]; + before = [ mountUnit ]; + unitConfig = { + DefaultDependencies = false; + RequiresMountsFor = "${upperdir} ${workdir}"; + }; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${pkgs.coreutils}/bin/mkdir -p -m 0755 ${upperdir} ${workdir}"; + }; + }; + }; + + overlayOpts = { config, ... }: { + + options.overlay = { + + lowerdir = lib.mkOption { + type = with lib.types; nullOr (nonEmptyListOf (either str pathInStore)); + default = null; + description = lib.mdDoc '' + The list of path(s) to the lowerdir(s). + + To create a writable overlay, you MUST provide an upperdir and a + workdir. + + You can create a read-only overlay when you provide multiple (at + least 2!) lowerdirs and neither an upperdir nor a workdir. + ''; + }; + + upperdir = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The path to the upperdir. + + If this is null, a read-only overlay is created using the lowerdir. + + If you set this to some value you MUST also set `workdir`. + ''; + }; + + workdir = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = lib.mdDoc '' + The path to the workdir. + + This MUST be set if you set `upperdir`. + ''; + }; + + }; + + config = lib.mkIf (config.overlay.lowerdir != null) { + fsType = "overlay"; + device = lib.mkDefault "overlay"; + + options = + let + prefix = sysrootPrefix config; + + lowerdir = map (s: prefix + s) config.overlay.lowerdir; + upperdir = prefix + config.overlay.upperdir; + workdir = prefix + config.overlay.workdir; + in + [ + "lowerdir=${lib.concatStringsSep ":" lowerdir}" + ] ++ lib.optionals (config.overlay.upperdir != null) [ + "upperdir=${upperdir}" + "workdir=${workdir}" + ] ++ (map (s: "x-systemd.requires-mounts-for=${s}") lowerdir); + }; + + }; +in + +{ + + options = { + + # Merge the overlay options into the fileSystems option. + fileSystems = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule [ overlayOpts ]); + }; + + }; + + config = + let + overlayFileSystems = lib.filterAttrs (_name: fs: (fs.overlay.lowerdir != null)) config.fileSystems; + initrdFileSystems = lib.filterAttrs (_name: utils.fsNeededForBoot) overlayFileSystems; + userspaceFileSystems = lib.filterAttrs (_name: fs: (!utils.fsNeededForBoot fs)) overlayFileSystems; + in + { + + boot.initrd.availableKernelModules = lib.mkIf (initrdFileSystems != { }) [ "overlay" ]; + + assertions = lib.concatLists (lib.mapAttrsToList + (_name: fs: [ + { + assertion = (fs.overlay.upperdir == null) == (fs.overlay.workdir == null); + message = "You cannot define a `lowerdir` without a `workdir` and vice versa for mount point: ${fs.mountPoint}"; + } + { + assertion = (fs.overlay.lowerdir != null && fs.overlay.upperdir == null) -> (lib.length fs.overlay.lowerdir) >= 2; + message = "A read-only overlay (without an `upperdir`) requires at least 2 `lowerdir`s: ${fs.mountPoint}"; + } + ]) + config.fileSystems); + + boot.initrd.systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService initrdFileSystems); + systemd.services = lib.mkMerge (lib.mapAttrsToList preMountService userspaceFileSystems); + + }; + +} diff --git a/nixos/modules/tasks/filesystems/reiserfs.nix b/nixos/modules/tasks/filesystems/reiserfs.nix index 3c6a0f0cd917f..f3f5e6aaa10b9 100644 --- a/nixos/modules/tasks/filesystems/reiserfs.nix +++ b/nixos/modules/tasks/filesystems/reiserfs.nix @@ -4,12 +4,12 @@ with lib; let - inInitrd = any (fs: fs == "reiserfs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.reiserfs or false; in { - config = mkIf (any (fs: fs == "reiserfs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.reiserfs or false) { system.fsPackages = [ pkgs.reiserfsprogs ]; diff --git a/nixos/modules/tasks/filesystems/squashfs.nix b/nixos/modules/tasks/filesystems/squashfs.nix index 10d45a21d3ca8..a0fac904766a4 100644 --- a/nixos/modules/tasks/filesystems/squashfs.nix +++ b/nixos/modules/tasks/filesystems/squashfs.nix @@ -2,7 +2,7 @@ let - inInitrd = lib.any (fs: fs == "squashfs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.squashfs or false; in diff --git a/nixos/modules/tasks/filesystems/sshfs.nix b/nixos/modules/tasks/filesystems/sshfs.nix index cd71dda16d8b4..63ff7f2b6b399 100644 --- a/nixos/modules/tasks/filesystems/sshfs.nix +++ b/nixos/modules/tasks/filesystems/sshfs.nix @@ -1,7 +1,11 @@ { config, lib, pkgs, ... }: { - config = lib.mkIf (lib.any (fs: fs == "sshfs" || fs == "fuse.sshfs") config.boot.supportedFilesystems) { - system.fsPackages = [ pkgs.sshfs ]; - }; + config = lib.mkIf + (config.boot.supportedFilesystems.sshfs + or config.boot.supportedFilesystems."fuse.sshfs" + or false) + { + system.fsPackages = [ pkgs.sshfs ]; + }; } diff --git a/nixos/modules/tasks/filesystems/unionfs-fuse.nix b/nixos/modules/tasks/filesystems/unionfs-fuse.nix index f9954b5182f91..929454ff1529c 100644 --- a/nixos/modules/tasks/filesystems/unionfs-fuse.nix +++ b/nixos/modules/tasks/filesystems/unionfs-fuse.nix @@ -3,7 +3,7 @@ { config = lib.mkMerge [ - (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.initrd.supportedFilesystems) { + (lib.mkIf (config.boot.initrd.supportedFilesystems.unionfs-fuse or false) { boot.initrd.kernelModules = [ "fuse" ]; boot.initrd.extraUtilsCommands = lib.mkIf (!config.boot.initrd.systemd.enable) '' @@ -35,7 +35,7 @@ }; }) - (lib.mkIf (lib.any (fs: fs == "unionfs-fuse") config.boot.supportedFilesystems) { + (lib.mkIf (config.boot.supportedFilesystems.unionfs-fuse or false) { system.fsPackages = [ pkgs.unionfs-fuse ]; }) diff --git a/nixos/modules/tasks/filesystems/vboxsf.nix b/nixos/modules/tasks/filesystems/vboxsf.nix index 5497194f6a8d1..00245b5af2527 100644 --- a/nixos/modules/tasks/filesystems/vboxsf.nix +++ b/nixos/modules/tasks/filesystems/vboxsf.nix @@ -4,7 +4,7 @@ with lib; let - inInitrd = any (fs: fs == "vboxsf") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.vboxsf or false; package = pkgs.runCommand "mount.vboxsf" { preferLocalBuild = true; } '' mkdir -p $out/bin @@ -13,7 +13,7 @@ let in { - config = mkIf (any (fs: fs == "vboxsf") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.vboxsf or false) { system.fsPackages = [ package ]; diff --git a/nixos/modules/tasks/filesystems/vfat.nix b/nixos/modules/tasks/filesystems/vfat.nix index 9281b34633c25..d7acc0c9e50b9 100644 --- a/nixos/modules/tasks/filesystems/vfat.nix +++ b/nixos/modules/tasks/filesystems/vfat.nix @@ -4,12 +4,12 @@ with lib; let - inInitrd = any (fs: fs == "vfat") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.vfat or false; in { - config = mkIf (any (fs: fs == "vfat") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.vfat or false) { system.fsPackages = [ pkgs.dosfstools pkgs.mtools ]; diff --git a/nixos/modules/tasks/filesystems/xfs.nix b/nixos/modules/tasks/filesystems/xfs.nix index 76f31e660ad3d..50dc1b3340aa5 100644 --- a/nixos/modules/tasks/filesystems/xfs.nix +++ b/nixos/modules/tasks/filesystems/xfs.nix @@ -4,12 +4,12 @@ with lib; let - inInitrd = any (fs: fs == "xfs") config.boot.initrd.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.xfs or false; in { - config = mkIf (any (fs: fs == "xfs") config.boot.supportedFilesystems) { + config = mkIf (config.boot.supportedFilesystems.xfs or false) { system.fsPackages = [ pkgs.xfsprogs.bin ]; diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix index b289d2151eb79..d11424c11c810 100644 --- a/nixos/modules/tasks/filesystems/zfs.nix +++ b/nixos/modules/tasks/filesystems/zfs.nix @@ -20,8 +20,8 @@ let clevisDatasets = map (e: e.device) (filter (e: e.device != null && (hasAttr e.device config.boot.initrd.clevis.devices) && e.fsType == "zfs" && (fsNeededForBoot e)) config.system.build.fileSystems); - inInitrd = any (fs: fs == "zfs") config.boot.initrd.supportedFilesystems; - inSystem = any (fs: fs == "zfs") config.boot.supportedFilesystems; + inInitrd = config.boot.initrd.supportedFilesystems.zfs or false; + inSystem = config.boot.supportedFilesystems.zfs or false; autosnapPkg = pkgs.zfstools.override { zfs = cfgZfs.package; @@ -211,6 +211,7 @@ in imports = [ (mkRemovedOptionModule [ "boot" "zfs" "enableLegacyCrypto" ] "The corresponding package was removed from nixpkgs.") + (mkRemovedOptionModule [ "boot" "zfs" "enableUnstable" ] "Instead set `boot.zfs.package = pkgs.zfs_unstable;`") ]; ###### interface @@ -219,9 +220,9 @@ in boot.zfs = { package = mkOption { type = types.package; - default = if cfgZfs.enableUnstable then pkgs.zfsUnstable else pkgs.zfs; - defaultText = literalExpression "if zfsUnstable is enabled then pkgs.zfsUnstable else pkgs.zfs"; - description = lib.mdDoc "Configured ZFS userland tools package, use `pkgs.zfsUnstable` if you want to track the latest staging ZFS branch."; + default = pkgs.zfs; + defaultText = literalExpression "pkgs.zfs"; + description = lib.mdDoc "Configured ZFS userland tools package, use `pkgs.zfs_unstable` if you want to track the latest staging ZFS branch."; }; modulePackage = mkOption { @@ -239,19 +240,6 @@ in description = lib.mdDoc "True if ZFS filesystem support is enabled"; }; - enableUnstable = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Use the unstable zfs package. This might be an option, if the latest - kernel is not yet supported by a published release of ZFS. Enabling - this option will install a development version of ZFS on Linux. The - version will have already passed an extensive test suite, but it is - more likely to hit an undiscovered bug compared to running a released - version of ZFS on Linux. - ''; - }; - allowHibernation = mkOption { type = types.bool; default = false; @@ -347,24 +335,12 @@ in removeLinuxDRM = lib.mkOption { type = types.bool; default = false; - description = lib.mdDoc '' - Linux 6.2 dropped some kernel symbols required on aarch64 required by zfs. - Enabling this option will bring them back to allow this kernel version. - Note that in some jurisdictions this may be illegal as it might be considered - removing copyright protection from the code. - See https://www.ifross.org/?q=en/artikel/ongoing-dispute-over-value-exportsymbolgpl-function for further information. + description = '' + Patch the kernel to change symbols needed by ZFS from + EXPORT_SYMBOL_GPL to EXPORT_SYMBOL. - If configure your kernel package with `zfs.latestCompatibleLinuxPackages`, you will need to also pass removeLinuxDRM to that package like this: - - ``` - { pkgs, ... }: { - boot.kernelPackages = (pkgs.zfs.override { - removeLinuxDRM = pkgs.hostPlatform.isAarch64; - }).latestCompatibleLinuxPackages; - - boot.zfs.removeLinuxDRM = true; - } - ``` + Currently has no effect, but may again in future if a kernel + update breaks ZFS due to symbols being newly changed to GPL. ''; }; }; @@ -588,7 +564,7 @@ in kernelParams = lib.optionals (!config.boot.zfs.allowHibernation) [ "nohibernate" ]; extraModulePackages = [ - (cfgZfs.modulePackage.override { inherit (cfgZfs) removeLinuxDRM; }) + cfgZfs.modulePackage ]; }; @@ -725,21 +701,6 @@ in services.udev.packages = [ cfgZfs.package ]; # to hook zvol naming, etc. systemd.packages = [ cfgZfs.package ]; - # Export kernel_neon_* symbols again. - # This change is necessary until ZFS figures out a solution - # with upstream or in their build system to fill the gap for - # this symbol. - # In the meantime, we restore what was once a working piece of code - # in the kernel. - boot.kernelPatches = lib.optional (cfgZfs.removeLinuxDRM && pkgs.stdenv.hostPlatform.system == "aarch64-linux") { - name = "export-neon-symbols-as-gpl"; - patch = pkgs.fetchpatch { - url = "https://github.com/torvalds/linux/commit/aaeca98456431a8d9382ecf48ac4843e252c07b3.patch"; - hash = "sha256-L2g4G1tlWPIi/QRckMuHDcdWBcKpObSWSRTvbHRIwIk="; - revert = true; - }; - }; - systemd.services = let createImportService' = pool: createImportService { inherit pool; diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix index f0d9b95f81f6b..c7fe1bed51592 100644 --- a/nixos/modules/virtualisation/amazon-image.nix +++ b/nixos/modules/virtualisation/amazon-image.nix @@ -103,4 +103,5 @@ in # (e.g. it depends on GTK). services.udisks2.enable = false; }; + meta.maintainers = with maintainers; [ arianvp ]; } diff --git a/nixos/modules/virtualisation/amazon-init.nix b/nixos/modules/virtualisation/amazon-init.nix index 8b98f2e32dd5d..8475097df07ca 100644 --- a/nixos/modules/virtualisation/amazon-init.nix +++ b/nixos/modules/virtualisation/amazon-init.nix @@ -84,4 +84,5 @@ in { }; }; }; + meta.maintainers = with maintainers; [ arianvp ]; } diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix index 3e33cabf26608..b3d81078eb349 100644 --- a/nixos/modules/virtualisation/containers.nix +++ b/nixos/modules/virtualisation/containers.nix @@ -28,6 +28,43 @@ in description = lib.mdDoc "Enable the OCI seccomp BPF hook"; }; + cdi = { + dynamic.nvidia.enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Enable dynamic CDI configuration for NVidia devices by running nvidia-container-toolkit on boot. + ''; + }; + + static = mkOption { + type = types.attrs; + default = { }; + description = lib.mdDoc '' + Declarative CDI specification. Each key of the attribute set + will be mapped to a file in /etc/cdi. It is required for every + key to be provided in JSON format. + ''; + example = { + some-vendor = builtins.fromJSON '' + { + "cdiVersion": "0.5.0", + "kind": "some-vendor.com/foo", + "devices": [], + "containerEdits": [] + } + ''; + + some-other-vendor = { + cdiVersion = "0.5.0"; + kind = "some-other-vendor.com/bar"; + devices = []; + containerEdits = []; + }; + }; + }; + }; + containersConf.settings = mkOption { type = toml.type; default = { }; @@ -113,6 +150,8 @@ in config = lib.mkIf cfg.enable { + hardware.nvidia-container-toolkit-cdi-generator.enable = lib.mkIf cfg.cdi.dynamic.nvidia.enable true; + virtualisation.containers.containersConf.cniPlugins = [ pkgs.cni-plugins ]; virtualisation.containers.containersConf.settings = { @@ -124,19 +163,28 @@ in }; }; - environment.etc."containers/containers.conf".source = - toml.generate "containers.conf" cfg.containersConf.settings; - - environment.etc."containers/storage.conf".source = - toml.generate "storage.conf" cfg.storage.settings; + environment.etc = let + cdiStaticConfigurationFiles = (lib.attrsets.mapAttrs' + (name: value: + lib.attrsets.nameValuePair "cdi/${name}.json" + { text = builtins.toJSON value; }) + cfg.cdi.static); + in { + "containers/containers.conf".source = + toml.generate "containers.conf" cfg.containersConf.settings; + + "containers/storage.conf".source = + toml.generate "storage.conf" cfg.storage.settings; + + "containers/registries.conf".source = toml.generate "registries.conf" { + registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries; + }; - environment.etc."containers/registries.conf".source = toml.generate "registries.conf" { - registries = lib.mapAttrs (n: v: { registries = v; }) cfg.registries; - }; + "containers/policy.json".source = + if cfg.policy != { } then pkgs.writeText "policy.json" (builtins.toJSON cfg.policy) + else "${pkgs.skopeo.policy}/default-policy.json"; + } // cdiStaticConfigurationFiles; - environment.etc."containers/policy.json".source = - if cfg.policy != { } then pkgs.writeText "policy.json" (builtins.toJSON cfg.policy) - else "${pkgs.skopeo.policy}/default-policy.json"; }; } diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix index dacd700537c78..417cf516c7f4b 100644 --- a/nixos/modules/virtualisation/cri-o.nix +++ b/nixos/modules/virtualisation/cri-o.nix @@ -6,7 +6,7 @@ let crioPackage = pkgs.cri-o.override { extraPackages = cfg.extraPackages - ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package; + ++ lib.optional (config.boot.supportedFilesystems.zfs or false) config.boot.zfs.package; }; format = pkgs.formats.toml { }; diff --git a/nixos/modules/virtualisation/docker.nix b/nixos/modules/virtualisation/docker.nix index b0d61ee06091d..cceb186e0b36e 100644 --- a/nixos/modules/virtualisation/docker.nix +++ b/nixos/modules/virtualisation/docker.nix @@ -72,6 +72,8 @@ in type = types.bool; default = false; description = lib.mdDoc '' + **Deprecated**, please use virtualisation.containers.cdi.dynamic.nvidia.enable instead. + Enable nvidia-docker wrapper, supporting NVIDIA GPUs inside docker containers. ''; }; @@ -93,8 +95,18 @@ in default = null; description = lib.mdDoc '' - This option determines which Docker storage driver to use. By default - it let's docker automatically choose preferred storage driver. + This option determines which Docker + [storage driver](https://docs.docker.com/storage/storagedriver/select-storage-driver/) + to use. + By default it lets docker automatically choose the preferred storage + driver. + However, it is recommended to specify a storage driver explicitly, as + docker's default varies over versions. + + ::: {.warning} + Changing the storage driver will cause any existing containers + and images to become inaccessible. + ::: ''; }; @@ -175,6 +187,16 @@ in users.groups.docker.gid = config.ids.gids.docker; systemd.packages = [ cfg.package ]; + # Docker 25.0.0 supports CDI by default + # (https://docs.docker.com/engine/release-notes/25.0/#new). Encourage + # moving to CDI as opposed to having deprecated runtime + # wrappers. + warnings = lib.optionals (cfg.enableNvidia && (lib.strings.versionAtLeast cfg.package.version "25")) [ + '' + You have set virtualisation.docker.enableNvidia. This option is deprecated, please set virtualisation.containers.cdi.dynamic.nvidia.enable instead. + '' + ]; + systemd.services.docker = { wantedBy = optional cfg.enableOnBoot "multi-user.target"; after = [ "network.target" "docker.socket" ]; diff --git a/nixos/modules/virtualisation/hyperv-image.nix b/nixos/modules/virtualisation/hyperv-image.nix index efaea0c110d23..fddff7bf1c69a 100644 --- a/nixos/modules/virtualisation/hyperv-image.nix +++ b/nixos/modules/virtualisation/hyperv-image.nix @@ -60,7 +60,6 @@ in { boot.growPartition = true; boot.loader.grub = { - version = 2; device = "nodev"; efiSupport = true; efiInstallAsRemovable = true; diff --git a/nixos/modules/virtualisation/incus.nix b/nixos/modules/virtualisation/incus.nix index bbe5b48b95bb1..3bbe0ba458516 100644 --- a/nixos/modules/virtualisation/incus.nix +++ b/nixos/modules/virtualisation/incus.nix @@ -97,6 +97,12 @@ in considered failed and systemd will attempt to restart it. ''; }; + + ui = { + enable = lib.mkEnableOption (lib.mdDoc "(experimental) Incus UI"); + + package = lib.mkPackageOption pkgs [ "incus" "ui" ] { }; + }; }; }; @@ -165,10 +171,12 @@ in "${config.boot.zfs.package}/lib/udev" ]; - environment = { + environment = lib.mkMerge [ { # Override Path to the LXC template configuration directory INCUS_LXC_TEMPLATE_CONFIG = "${pkgs.lxcfs}/share/lxc/config"; - }; + } (lib.mkIf (cfg.ui.enable) { + "INCUS_UI" = cfg.ui.package; + }) ]; serviceConfig = { ExecStart = "${cfg.package}/bin/incusd --group incus-admin"; diff --git a/nixos/modules/virtualisation/linode-config.nix b/nixos/modules/virtualisation/linode-config.nix index bbf81bda9c024..209bff57ea8be 100644 --- a/nixos/modules/virtualisation/linode-config.nix +++ b/nixos/modules/virtualisation/linode-config.nix @@ -59,7 +59,6 @@ with lib; grub = { enable = true; - version = 2; forceInstall = true; device = "nodev"; diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix index 8d3a480e6dc8c..95e3083ff9eda 100644 --- a/nixos/modules/virtualisation/lxc-container.nix +++ b/nixos/modules/virtualisation/lxc-container.nix @@ -14,7 +14,9 @@ options = { }; - config = { + config = let + initScript = if config.boot.initrd.systemd.enable then "prepare-root" else "init"; + in { boot.isContainer = true; boot.postBootCommands = '' @@ -41,7 +43,7 @@ contents = [ { - source = config.system.build.toplevel + "/init"; + source = config.system.build.toplevel + "/${initScript}"; target = "/sbin/init"; } # Technically this is not required for lxc, but having also make this configuration work with systemd-nspawn. @@ -65,7 +67,7 @@ pseudoFiles = [ "/sbin d 0755 0 0" - "/sbin/init s 0555 0 0 ${config.system.build.toplevel}/init" + "/sbin/init s 0555 0 0 ${config.system.build.toplevel}/${initScript}" "/dev d 0755 0 0" "/proc d 0555 0 0" "/sys d 0555 0 0" @@ -74,7 +76,7 @@ system.build.installBootLoader = pkgs.writeScript "install-lxd-sbin-init.sh" '' #!${pkgs.runtimeShell} - ${pkgs.coreutils}/bin/ln -fs "$1/init" /sbin/init + ${pkgs.coreutils}/bin/ln -fs "$1/${initScript}" /sbin/init ''; # networkd depends on this, but systemd module disables this for containers @@ -83,7 +85,7 @@ systemd.packages = [ pkgs.distrobuilder.generator ]; system.activationScripts.installInitScript = lib.mkForce '' - ln -fs $systemConfig/init /sbin/init + ln -fs $systemConfig/${initScript} /sbin/init ''; }; } diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix index 07ed08ab2f84d..a88715587d654 100644 --- a/nixos/modules/virtualisation/oci-containers.nix +++ b/nixos/modules/virtualisation/oci-containers.nix @@ -252,10 +252,13 @@ let text = '' ${cfg.backend} rm -f ${name} || true ${optionalString (isValidLogin container.login) '' + # try logging in, if it fails, check if image exists locally ${cfg.backend} login \ ${container.login.registry} \ --username ${container.login.username} \ - --password-stdin < ${container.login.passwordFile} + --password-stdin < ${container.login.passwordFile} \ + || ${cfg.backend} image inspect ${container.image} >/dev/null \ + || { echo "image doesn't exist locally and login failed" >&2 ; exit 1; } ''} ${optionalString (container.imageFile != null) '' ${cfg.backend} load -i ${container.imageFile} @@ -308,9 +311,10 @@ let ); preStop = if cfg.backend == "podman" - then "[ $SERVICE_RESULT = success ] || podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id" - else "[ $SERVICE_RESULT = success ] || ${cfg.backend} stop ${name}"; - postStop = if cfg.backend == "podman" + then "podman stop --ignore --cidfile=/run/podman-${escapedName}.ctr-id" + else "${cfg.backend} stop ${name}"; + + postStop = if cfg.backend == "podman" then "podman rm -f --ignore --cidfile=/run/podman-${escapedName}.ctr-id" else "${cfg.backend} rm -f ${name} || true"; diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix index 47382f9beab00..a977390542166 100644 --- a/nixos/modules/virtualisation/podman/default.nix +++ b/nixos/modules/virtualisation/podman/default.nix @@ -9,7 +9,7 @@ let extraPackages = cfg.extraPackages # setuid shadow ++ [ "/run/wrappers" ] - ++ lib.optional (builtins.elem "zfs" config.boot.supportedFilesystems) config.boot.zfs.package; + ++ lib.optional (config.boot.supportedFilesystems.zfs or false) config.boot.zfs.package; }); # Provides a fake "docker" binary mapping to podman @@ -82,6 +82,8 @@ in type = types.bool; default = false; description = lib.mdDoc '' + **Deprecated**, please use virtualisation.containers.cdi.dynamic.nvidia.enable instead. + Enable use of NVidia GPUs from within podman containers. ''; }; @@ -166,6 +168,12 @@ in inherit (networkConfig) dns_enabled network_interface; in lib.mkIf cfg.enable { + warnings = lib.optionals cfg.enableNvidia [ + '' + You have set virtualisation.podman.enableNvidia. This option is deprecated, please set virtualisation.containers.cdi.dynamic.nvidia.enable instead. + '' + ]; + environment.systemPackages = [ cfg.package ] ++ lib.optional cfg.dockerCompat dockerCompat; @@ -208,9 +216,11 @@ in requires = [ "podman.service" ]; }; + systemd.services.podman.environment = config.networking.proxy.envVars; systemd.sockets.podman.wantedBy = [ "sockets.target" ]; systemd.sockets.podman.socketConfig.SocketGroup = "podman"; + systemd.user.services.podman.environment = config.networking.proxy.envVars; systemd.user.sockets.podman.wantedBy = [ "sockets.target" ]; systemd.timers.podman-prune.timerConfig = lib.mkIf cfg.autoPrune.enable { diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix index 3d7f3ccb62f84..75ba6dacc122c 100644 --- a/nixos/modules/virtualisation/qemu-vm.nix +++ b/nixos/modules/virtualisation/qemu-vm.nix @@ -701,7 +701,10 @@ in type = types.listOf types.str; default = []; example = [ "-vga std" ]; - description = lib.mdDoc "Options passed to QEMU."; + description = lib.mdDoc '' + Options passed to QEMU. + See [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for a complete list. + ''; }; consoles = mkOption { @@ -732,9 +735,10 @@ in description = lib.mdDoc '' Networking-related command-line options that should be passed to qemu. The default is to use userspace networking (SLiRP). + See the [QEMU Wiki on Networking](https://wiki.qemu.org/Documentation/Networking) for details. If you override this option, be advised to keep - ''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS} (as seen in the example) + `''${QEMU_NET_OPTS:+,$QEMU_NET_OPTS}` (as seen in the example) to keep the default runtime behaviour. ''; }; @@ -809,14 +813,19 @@ in defaultText = "!cfg.useBootLoader"; description = lib.mdDoc '' - If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader. Other relevant parameters such as the initrd are also passed to QEMU. + If enabled, the virtual machine will boot directly into the kernel instead of through a bootloader. + Read more about this feature in the [QEMU documentation on Direct Linux Boot](https://qemu-project.gitlab.io/qemu/system/linuxboot.html) + This is enabled by default. If you want to test netboot, consider disabling this option. + Enable a bootloader with {option}`virtualisation.useBootLoader` if you need. - This will not boot / reboot correctly into a system that has switched to a different configuration on disk. + Relevant parameters such as those set in `boot.initrd` and `boot.kernelParams` are also passed to QEMU. + Additional parameters can be supplied on invocation through the environment variable `$QEMU_KERNEL_PARAMS`. + They are added to the `-append` option, see [QEMU User Documentation](https://www.qemu.org/docs/master/system/qemu-manpage) for details + For example, to let QEMU use the parent terminal as the serial console, set `QEMU_KERNEL_PARAMS="console=ttyS0"`. - This is enabled by default if you don't enable bootloaders, but you can still enable a bootloader if you need. - Read more about this feature: <https://qemu-project.gitlab.io/qemu/system/linuxboot.html>. + This will not (re-)boot correctly into a system that has switched to a different configuration on disk. ''; }; initrd = @@ -846,6 +855,8 @@ in If disabled, the kernel and initrd are directly booted, forgoing any bootloader. + + Check the documentation on {option}`virtualisation.directBoot.enable` for details. ''; }; @@ -866,9 +877,11 @@ in type = types.package; default = (pkgs.OVMF.override { secureBoot = cfg.useSecureBoot; + systemManagementModeRequired = cfg.useSecureBoot; }).fd; defaultText = ''(pkgs.OVMF.override { secureBoot = cfg.useSecureBoot; + systemManagementModeRequired = cfg.useSecureBoot; }).fd''; description = lib.mdDoc "OVMF firmware package, defaults to OVMF configured with secure boot if needed."; @@ -1066,10 +1079,18 @@ in ''} ''; - systemd.tmpfiles.rules = lib.mkIf config.boot.initrd.systemd.enable [ - "f /etc/NIXOS 0644 root root -" - "d /boot 0644 root root -" - ]; + systemd.tmpfiles.settings."10-qemu-vm" = lib.mkIf config.boot.initrd.systemd.enable { + "/etc/NIXOS".f = { + mode = "0644"; + user = "root"; + group = "root"; + }; + "${config.boot.loader.efi.efiSysMountPoint}".d = { + mode = "0644"; + user = "root"; + group = "root"; + }; + }; # After booting, register the closure of the paths in # `virtualisation.additionalPaths' in the Nix database in the VM. This @@ -1164,6 +1185,10 @@ in "-tpmdev emulator,id=tpm_dev_0,chardev=chrtpm" "-device ${cfg.tpm.deviceModel},tpmdev=tpm_dev_0" ]) + (mkIf (cfg.efi.OVMF.systemManagementModeRequired or false) [ + "-machine" "q35,smm=on" + "-global" "driver=cfi.pflash01,property=secure,value=on" + ]) ]; virtualisation.qemu.drives = mkMerge [ diff --git a/nixos/modules/virtualisation/vmware-image.nix b/nixos/modules/virtualisation/vmware-image.nix index a38713b4d4ee1..3674b37d0b97e 100644 --- a/nixos/modules/virtualisation/vmware-image.nix +++ b/nixos/modules/virtualisation/vmware-image.nix @@ -80,7 +80,6 @@ in { boot.growPartition = true; boot.loader.grub = { - version = 2; device = "nodev"; efiSupport = true; efiInstallAsRemovable = true; |