diff options
Diffstat (limited to 'nixos/modules')
62 files changed, 1912 insertions, 781 deletions
diff --git a/nixos/modules/config/update-users-groups.pl b/nixos/modules/config/update-users-groups.pl index 5236264e16b79..3785ba8c5fb80 100644 --- a/nixos/modules/config/update-users-groups.pl +++ b/nixos/modules/config/update-users-groups.pl @@ -4,7 +4,7 @@ use File::Path qw(make_path); use File::Slurp; use Getopt::Long; use JSON; -use DateTime; +use Time::Piece; # Keep track of deleted uids and gids. my $uidMapFile = "/var/lib/nixos/uid-map"; @@ -26,17 +26,8 @@ sub updateFile { # Converts an ISO date to number of days since 1970-01-01 sub dateToDays { my ($date) = @_; - my ($year, $month, $day) = split('-', $date, -3); - my $dt = DateTime->new( - year => $year, - month => $month, - day => $day, - hour => 0, - minute => 0, - second => 0, - time_zone => 'UTC', - ); - return $dt->epoch / 86400; + my $time = Time::Piece->strptime($date, "%Y-%m-%d"); + return $time->epoch / 60 / 60 / 24; } sub nscdInvalidate { diff --git a/nixos/modules/config/users-groups.nix b/nixos/modules/config/users-groups.nix index 9629e3964c96f..684b4bc8fbcc5 100644 --- a/nixos/modules/config/users-groups.nix +++ b/nixos/modules/config/users-groups.nix @@ -648,7 +648,7 @@ in { install -m 0700 -d /root install -m 0755 -d /home - ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON p.DateTime ])}/bin/perl \ + ${pkgs.perl.withPackages (p: [ p.FileSlurp p.JSON ])}/bin/perl \ -w ${./update-users-groups.pl} ${spec} ''; }; diff --git a/nixos/modules/hardware/decklink.nix b/nixos/modules/hardware/decklink.nix new file mode 100644 index 0000000000000..d179e1d7634ff --- /dev/null +++ b/nixos/modules/hardware/decklink.nix @@ -0,0 +1,16 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.hardware.decklink; + kernelPackages = config.boot.kernelPackages; +in +{ + options.hardware.decklink.enable = lib.mkEnableOption "hardware support for the Blackmagic Design Decklink audio/video interfaces"; + + config = lib.mkIf cfg.enable { + boot.kernelModules = [ "blackmagic" "blackmagic-io" "snd_blackmagic-io" ]; + boot.extraModulePackages = [ kernelPackages.decklink ]; + systemd.packages = [ pkgs.blackmagic-desktop-video ]; + systemd.services.DesktopVideoHelper.wantedBy = [ "multi-user.target" ]; + }; +} diff --git a/nixos/modules/hardware/infiniband.nix b/nixos/modules/hardware/infiniband.nix new file mode 100644 index 0000000000000..962883fa79721 --- /dev/null +++ b/nixos/modules/hardware/infiniband.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.hardware.infiniband; + opensm-services = { + "opensm@" = { + enable = true; + description = "Starts OpenSM Infiniband fabric Subnet Managers"; + before = [ "network.target"]; + unitConfig = { + ConditionPathExists = "/sys/class/infiniband_mad/abi_version"; + }; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.opensm}/bin/opensm --guid %I --log_file /var/log/opensm.%I.log"; + }; + }; + } // (builtins.listToAttrs (map (guid: { + name = "opensm@${guid}"; + value = { + enable = true; + wantedBy = [ "machines.target" ]; + overrideStrategy = "asDropin"; + }; + } ) cfg.guids)); + +in + +{ + options.hardware.infiniband = { + enable = mkEnableOption "Infiniband support"; + guids = mkOption { + type = with types; listOf str; + default = []; + example = [ "0xe8ebd30000eee2e1" ]; + description = lib.mdDoc '' + A list of infiniband port guids on the system. This is discoverable using `ibstat -p` + ''; + }; + }; + + config = mkIf cfg.enable { + boot.initrd.kernelModules = [ + "mlx5_core" "mlx5_ib" "ib_cm" + "rdma_cm" "rdma_ucm" "rpcrdma" + "ib_ipoib" "ib_isert" "ib_umad" "ib_uverbs" + ]; + # rdma-core exposes ibstat, mstflint exposes mstconfig (which can be needed for + # setting link configurations), qperf needed to affirm link speeds + environment.systemPackages = with pkgs; [ + rdma-core mstflint qperf + ]; + systemd.services = opensm-services; + }; +} diff --git a/nixos/modules/i18n/input-method/uim.nix b/nixos/modules/i18n/input-method/uim.nix index 9491ab2640fcc..7225783b2a6f8 100644 --- a/nixos/modules/i18n/input-method/uim.nix +++ b/nixos/modules/i18n/input-method/uim.nix @@ -10,7 +10,7 @@ in i18n.inputMethod.uim = { toolbar = mkOption { - type = types.enum [ "gtk" "gtk3" "gtk-systray" "gtk3-systray" "qt4" ]; + type = types.enum [ "gtk" "gtk3" "gtk-systray" "gtk3-systray" "qt5" ]; default = "gtk"; example = "gtk-systray"; description = lib.mdDoc '' diff --git a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix index ea8056ff870c5..573b31b439c2d 100644 --- a/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix +++ b/nixos/modules/installer/cd-dvd/installation-cd-graphical-gnome.nix @@ -6,7 +6,6 @@ imports = [ ./installation-cd-graphical-base.nix ]; isoImage.edition = "gnome"; - isoImage.graphicalGrub = true; services.xserver.desktopManager.gnome = { # Add Firefox and other tools useful for installation to the launcher diff --git a/nixos/modules/installer/cd-dvd/iso-image.nix b/nixos/modules/installer/cd-dvd/iso-image.nix index c430048d6598f..0b5135c088eaf 100644 --- a/nixos/modules/installer/cd-dvd/iso-image.nix +++ b/nixos/modules/installer/cd-dvd/iso-image.nix @@ -24,6 +24,9 @@ let # Name appended to menuentry defaults to params if no specific name given. option.name or (optionalString (option ? params) "(${option.params})") }' ${optionalString (option ? class) " --class ${option.class}"} { + # Fallback to UEFI console for boot, efifb sometimes has difficulties. + terminal_output console + linux ${defaults.image} \''${isoboot} ${defaults.params} ${ option.params or "" } @@ -185,33 +188,25 @@ let # So instead we'll list a lot of possibly valid modes :/ #"3840x2160" #"2560x1440" + "1920x1200" "1920x1080" "1366x768" + "1280x800" "1280x720" + "1200x1920" "1024x768" + "800x1280" "800x600" "auto" ]} - # Fonts can be loaded? - # (This font is assumed to always be provided as a fallback by NixOS) - if loadfont (\$root)/EFI/boot/unicode.pf2; then - set with_fonts=true - fi - if [ "\$textmode" != "true" -a "\$with_fonts" == "true" ]; then - # Use graphical term, it can be either with background image or a theme. - # input is "console", while output is "gfxterm". - # This enables "serial" input and output only when possible. - # Otherwise the failure mode is to not even enable gfxterm. - if test "\$with_serial" == "yes"; then - terminal_output gfxterm serial - terminal_input console serial - else - terminal_output gfxterm - terminal_input console - fi + if [ "\$textmode" == "false" ]; then + terminal_output gfxterm + terminal_input console else - # Sets colors for the non-graphical term. + terminal_output console + terminal_input console + # Sets colors for console term. set menu_color_normal=cyan/blue set menu_color_highlight=white/blue fi @@ -250,18 +245,58 @@ let touch $out/EFI/nixos-installer-image # ALWAYS required modules. - MODULES="fat iso9660 part_gpt part_msdos \ - normal boot linux configfile loopback chain halt \ - efifwsetup efi_gop \ - ls search search_label search_fs_uuid search_fs_file \ - gfxmenu gfxterm gfxterm_background gfxterm_menu test all_video loadenv \ - exfat ext2 ntfs btrfs hfsplus udf \ - videoinfo png \ - echo serial \ - " + MODULES=( + # Basic modules for filesystems and partition schemes + "fat" + "iso9660" + "part_gpt" + "part_msdos" + + # Basic stuff + "normal" + "boot" + "linux" + "configfile" + "loopback" + "chain" + "halt" + + # Allows rebooting into firmware setup interface + "efifwsetup" + + # EFI Graphics Output Protocol + "efi_gop" + + # User commands + "ls" + + # System commands + "search" + "search_label" + "search_fs_uuid" + "search_fs_file" + "echo" + + # We're not using it anymore, but we'll leave it in so it can be used + # by user, with the console using "C" + "serial" + + # Graphical mode stuff + "gfxmenu" + "gfxterm" + "gfxterm_background" + "gfxterm_menu" + "test" + "loadenv" + "all_video" + "videoinfo" + + # File types for graphical mode + "png" + ) echo "Building GRUB with modules:" - for mod in $MODULES; do + for mod in ''${MODULES[@]}; do echo " - $mod" done @@ -270,31 +305,27 @@ let for mod in efi_uga; do if [ -f ${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget}/$mod.mod ]; then echo " - $mod" - MODULES+=" $mod" + MODULES+=("$mod") fi done # Make our own efi program, we can't rely on "grub-install" since it seems to # probe for devices, even with --skip-fs-probe. - grub-mkimage --directory=${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget} -o $out/EFI/boot/boot${targetArch}.efi -p /EFI/boot -O ${grubPkgs.grub2_efi.grubTarget} \ - $MODULES + grub-mkimage \ + --directory=${grubPkgs.grub2_efi}/lib/grub/${grubPkgs.grub2_efi.grubTarget} \ + -o $out/EFI/boot/boot${targetArch}.efi \ + -p /EFI/boot \ + -O ${grubPkgs.grub2_efi.grubTarget} \ + ''${MODULES[@]} cp ${grubPkgs.grub2_efi}/share/grub/unicode.pf2 $out/EFI/boot/ cat <<EOF > $out/EFI/boot/grub.cfg - set with_fonts=false - set textmode=${boolToString (!config.isoImage.graphicalGrub)} - # If you want to use serial for "terminal_*" commands, you need to set one up: - # Example manual configuration: - # → serial --unit=0 --speed=115200 --word=8 --parity=no --stop=1 - # This uses the defaults, and makes the serial terminal available. - set with_serial=no - if serial; then set with_serial=yes ;fi - export with_serial - clear + set textmode=${boolToString (config.isoImage.forceTextMode)} set timeout=${toString grubEfiTimeout} - # This message will only be viewable when "gfxterm" is not used. + clear + # This message will only be viewable on the default (UEFI) console. echo "" echo "Loading graphical boot menu..." echo "" @@ -306,7 +337,7 @@ let hiddenentry 'Text mode' --hotkey 't' { loadfont (\$root)/EFI/boot/unicode.pf2 set textmode=true - terminal_output gfxterm console + terminal_output console } hiddenentry 'GUI mode' --hotkey 'g' { $(find ${config.isoImage.grubTheme} -iname '*.pf2' -printf "loadfont (\$root)/EFI/boot/grub-theme/%P\n") @@ -400,6 +431,8 @@ let } EOF + grub-script-check $out/EFI/boot/grub.cfg + ${refind} ''; @@ -658,13 +691,17 @@ in ''; }; - isoImage.graphicalGrub = mkOption { + isoImage.forceTextMode = mkOption { default = false; type = types.bool; example = true; description = lib.mdDoc '' - Whether to use textmode or graphical grub. - false means we use textmode grub. + Whether to use text mode instead of graphical grub. + A value of `true` means graphical mode is not tried to be used. + + This is useful for validating that graphics mode usage is not at the root cause of a problem with the iso image. + + If text mode is required off-handedly (e.g. for serial use) you can use the `T` key, after being prompted, to use text mode for the current boot. ''; }; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index ec6424682ccce..018b9b6b44c52 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -55,6 +55,7 @@ ./hardware/cpu/amd-sev.nix ./hardware/cpu/intel-microcode.nix ./hardware/cpu/intel-sgx.nix + ./hardware/decklink.nix ./hardware/device-tree.nix ./hardware/digitalbitbox.nix ./hardware/flipperzero.nix @@ -63,6 +64,7 @@ ./hardware/gpgsmartcards.nix ./hardware/hackrf.nix ./hardware/i2c.nix + ./hardware/infiniband.nix ./hardware/keyboard/qmk.nix ./hardware/keyboard/teck.nix ./hardware/keyboard/uhk.nix @@ -163,6 +165,7 @@ ./programs/direnv.nix ./programs/dmrconfig.nix ./programs/droidcam.nix + ./programs/ecryptfs.nix ./programs/environment.nix ./programs/evince.nix ./programs/extra-container.nix @@ -393,7 +396,6 @@ ./services/continuous-integration/gitlab-runner.nix ./services/continuous-integration/gocd-agent/default.nix ./services/continuous-integration/gocd-server/default.nix - ./services/continuous-integration/hail.nix ./services/continuous-integration/hercules-ci-agent/default.nix ./services/continuous-integration/hydra/default.nix ./services/continuous-integration/jenkins/default.nix @@ -594,6 +596,7 @@ ./services/mail/rss2email.nix ./services/mail/schleuder.nix ./services/mail/spamassassin.nix + ./services/mail/stalwart-mail.nix ./services/mail/sympa.nix ./services/mail/zeyple.nix ./services/matrix/appservice-discord.nix @@ -1258,6 +1261,7 @@ ./services/web-apps/node-red.nix ./services/web-apps/onlyoffice.nix ./services/web-apps/openvscode-server.nix + ./services/web-apps/mobilizon.nix ./services/web-apps/openwebrx.nix ./services/web-apps/outline.nix ./services/web-apps/peering-manager.nix diff --git a/nixos/modules/programs/ecryptfs.nix b/nixos/modules/programs/ecryptfs.nix new file mode 100644 index 0000000000000..63c1a3ad44199 --- /dev/null +++ b/nixos/modules/programs/ecryptfs.nix @@ -0,0 +1,31 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.ecryptfs; + +in { + options.programs.ecryptfs = { + enable = mkEnableOption (lib.mdDoc "ecryptfs setuid mount wrappers"); + }; + + config = mkIf cfg.enable { + security.wrappers = { + + "mount.ecryptfs_private" = { + setuid = true; + owner = "root"; + group = "root"; + source = "${lib.getBin pkgs.ecryptfs}/bin/mount.ecryptfs_private"; + }; + "umount.ecryptfs_private" = { + setuid = true; + owner = "root"; + group = "root"; + source = "${lib.getBin pkgs.ecryptfs}/bin/umount.ecryptfs_private"; + }; + + }; + }; +} diff --git a/nixos/modules/programs/streamdeck-ui.nix b/nixos/modules/programs/streamdeck-ui.nix index 4c055029e39b9..220f0a35f1629 100644 --- a/nixos/modules/programs/streamdeck-ui.nix +++ b/nixos/modules/programs/streamdeck-ui.nix @@ -24,7 +24,7 @@ in config = mkIf cfg.enable { environment.systemPackages = with pkgs; [ cfg.package - (mkIf cfg.autoStart (makeAutostartItem { name = "streamdeck-ui"; package = cfg.package; })) + (mkIf cfg.autoStart (makeAutostartItem { name = "streamdeck-ui-noui"; package = cfg.package; })) ]; services.udev.packages = [ cfg.package ]; diff --git a/nixos/modules/programs/yazi.nix b/nixos/modules/programs/yazi.nix new file mode 100644 index 0000000000000..3416bca06679f --- /dev/null +++ b/nixos/modules/programs/yazi.nix @@ -0,0 +1,49 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.programs.yazi; + + settingsFormat = pkgs.formats.toml { }; + + names = [ "yazi" "theme" "keymap" ]; +in +{ + options.programs.yazi = { + enable = lib.mkEnableOption (lib.mdDoc "yazi terminal file manager"); + + package = lib.mkPackageOptionMD pkgs "yazi" { }; + + settings = lib.mkOption { + type = with lib.types; submodule { + options = lib.listToAttrs (map + (name: lib.nameValuePair name (lib.mkOption { + inherit (settingsFormat) type; + default = { }; + 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. + ''; + })) + names); + }; + default = { }; + description = lib.mdDoc '' + Configuration included in `$YAZI_CONFIG_HOME`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + environment = { + systemPackages = [ cfg.package ]; + variables.YAZI_CONFIG_HOME = "/etc/yazi/"; + etc = lib.attrsets.mergeAttrsList (map + (name: lib.optionalAttrs (cfg.settings.${name} != { }) { + "yazi/${name}.toml".source = settingsFormat.generate "${name}.toml" cfg.settings.${name}; + }) + names); + }; + }; + meta.maintainers = with lib.maintainers; [ linsui ]; +} diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix index 8aee71c864d0a..222a25cf11dce 100644 --- a/nixos/modules/security/acme/default.nix +++ b/nixos/modules/security/acme/default.nix @@ -1,6 +1,8 @@ { config, lib, pkgs, options, ... }: with lib; let + + cfg = config.security.acme; opt = options.security.acme; user = if cfg.useRoot then "root" else "acme"; @@ -14,6 +16,36 @@ let mkAccountHash = acmeServer: data: mkHash "${toString acmeServer} ${data.keyType} ${data.email}"; accountDirRoot = "/var/lib/acme/.lego/accounts/"; + lockdir = "/run/acme/"; + concurrencyLockfiles = map (n: "${toString n}.lock") (lib.range 1 cfg.maxConcurrentRenewals); + # Assign elements of `baseList` to each element of `needAssignmentList`, until the latter is exhausted. + # returns: [{fst = "element of baseList"; snd = "element of needAssignmentList"}] + roundRobinAssign = baseList: needAssignmentList: + if baseList == [] then [] + else _rrCycler baseList baseList needAssignmentList; + _rrCycler = with builtins; origBaseList: workingBaseList: needAssignmentList: + if (workingBaseList == [] || needAssignmentList == []) + then [] + else + [{ fst = head workingBaseList; snd = head needAssignmentList;}] ++ + _rrCycler origBaseList (if (tail workingBaseList == []) then origBaseList else tail workingBaseList) (tail needAssignmentList); + attrsToList = mapAttrsToList (attrname: attrval: {name = attrname; value = attrval;}); + # for an AttrSet `funcsAttrs` having functions as values, apply single arguments from + # `argsList` to them in a round-robin manner. + # Returns an attribute set with the applied functions as values. + roundRobinApplyAttrs = funcsAttrs: argsList: lib.listToAttrs (map (x: {inherit (x.snd) name; value = x.snd.value x.fst;}) (roundRobinAssign argsList (attrsToList funcsAttrs))); + wrapInFlock = lockfilePath: script: + # explainer: https://stackoverflow.com/a/60896531 + '' + exec {LOCKFD}> ${lockfilePath} + echo "Waiting to acquire lock ${lockfilePath}" + ${pkgs.flock}/bin/flock ''${LOCKFD} || exit 1 + echo "Acquired lock ${lockfilePath}" + '' + + script + "\n" + + ''echo "Releasing lock ${lockfilePath}" # only released after process exit''; + + # There are many services required to make cert renewals work. # They all follow a common structure: # - They inherit this commonServiceConfig @@ -31,6 +63,7 @@ let ProtectSystem = "strict"; ReadWritePaths = [ "/var/lib/acme" + lockdir ]; PrivateTmp = true; @@ -118,7 +151,8 @@ let # We don't want this to run every time a renewal happens RemainAfterExit = true; - # These StateDirectory entries negate the need for tmpfiles + # StateDirectory entries are a cleaner, service-level mechanism + # for dealing with persistent service data StateDirectory = [ "acme" "acme/.lego" "acme/.lego/accounts" ]; StateDirectoryMode = 755; WorkingDirectory = "/var/lib/acme"; @@ -127,6 +161,25 @@ let ExecStart = "+" + (pkgs.writeShellScript "acme-fixperms" script); }; }; + lockfilePrepareService = { + description = "Manage lock files for acme services"; + + # ensure all required lock files exist, but none more + script = '' + GLOBIGNORE="${concatStringsSep ":" concurrencyLockfiles}" + rm -f * + unset GLOBIGNORE + + xargs touch <<< "${toString concurrencyLockfiles}" + ''; + + serviceConfig = commonServiceConfig // { + # We don't want this to run every time a renewal happens + RemainAfterExit = true; + WorkingDirectory = lockdir; + }; + }; + certToConfig = cert: data: let acmeServer = data.server; @@ -229,10 +282,10 @@ let }; }; - selfsignService = { + selfsignService = lockfileName: { description = "Generate self-signed certificate for ${cert}"; - after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ]; - requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ]; + after = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ] ++ optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service"; + requires = [ "acme-selfsigned-ca.service" "acme-fixperms.service" ] ++ optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service"; path = with pkgs; [ minica ]; @@ -256,7 +309,7 @@ let # Working directory will be /tmp # minica will output to a folder sharing the name of the first domain # in the list, which will be ${data.domain} - script = '' + script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") '' minica \ --ca-key ca/key.pem \ --ca-cert ca/cert.pem \ @@ -277,10 +330,10 @@ let ''; }; - renewService = { + renewService = lockfileName: { description = "Renew ACME certificate for ${cert}"; - after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps; - wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps; + after = [ "network.target" "network-online.target" "acme-fixperms.service" "nss-lookup.target" ] ++ selfsignedDeps ++ optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service"; + wants = [ "network-online.target" "acme-fixperms.service" ] ++ selfsignedDeps ++ optional (cfg.maxConcurrentRenewals > 0) "acme-lockfiles.service"; # https://github.com/NixOS/nixpkgs/pull/81371#issuecomment-605526099 wantedBy = optionals (!config.boot.isContainer) [ "multi-user.target" ]; @@ -329,7 +382,7 @@ let }; # Working directory will be /tmp - script = '' + script = (if (lockfileName == null) then lib.id else wrapInFlock "${lockdir}${lockfileName}") '' ${optionalString data.enableDebugLogs "set -x"} set -euo pipefail @@ -755,6 +808,17 @@ in { } ''; }; + maxConcurrentRenewals = mkOption { + default = 5; + type = types.int; + description = lib.mdDoc '' + Maximum number of concurrent certificate generation or renewal jobs. All other + jobs will queue and wait running jobs to finish. Reduces the system load of + certificate generation. + + Set to `0` to allow unlimited number of concurrent job runs." + ''; + }; }; }; @@ -875,12 +939,28 @@ in { users.groups.acme = {}; - systemd.services = { - "acme-fixperms" = userMigrationService; - } // (mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs) + # for lock files, still use tmpfiles as they should better reside in /run + systemd.tmpfiles.rules = [ + "d ${lockdir} 0700 ${user} - - -" + "Z ${lockdir} 0700 ${user} - - -" + ]; + + systemd.services = let + renewServiceFunctions = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewService) certConfigs; + renewServices = if cfg.maxConcurrentRenewals > 0 + then roundRobinApplyAttrs renewServiceFunctions concurrencyLockfiles + else mapAttrs (_: f: f null) renewServiceFunctions; + selfsignServiceFunctions = mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs; + selfsignServices = if cfg.maxConcurrentRenewals > 0 + then roundRobinApplyAttrs selfsignServiceFunctions concurrencyLockfiles + else mapAttrs (_: f: f null) selfsignServiceFunctions; + in + { "acme-fixperms" = userMigrationService; } + // (optionalAttrs (cfg.maxConcurrentRenewals > 0) {"acme-lockfiles" = lockfilePrepareService; }) + // renewServices // (optionalAttrs (cfg.preliminarySelfsigned) ({ "acme-selfsigned-ca" = selfsignCAService; - } // (mapAttrs' (cert: conf: nameValuePair "acme-selfsigned-${cert}" conf.selfsignService) certConfigs))); + } // selfsignServices)); systemd.timers = mapAttrs' (cert: conf: nameValuePair "acme-${cert}" conf.renewTimer) certConfigs; diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index a431817fe1bb3..d83259ccbebc1 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -1303,7 +1303,7 @@ in security.pam.enableEcryptfs = mkEnableOption (lib.mdDoc "eCryptfs PAM module (mounting ecryptfs home directory on login)"); security.pam.enableFscrypt = mkEnableOption (lib.mdDoc '' - Enables fscrypt to automatically unlock directories with the user's login password. + fscrypt to automatically unlock directories with the user's login password. This also enables a service at security.pam.services.fscrypt which is used by fscrypt to verify the user's password when setting up a new protector. If you diff --git a/nixos/modules/security/sudo.nix b/nixos/modules/security/sudo.nix index 9ac91bd0d3686..d225442773c69 100644 --- a/nixos/modules/security/sudo.nix +++ b/nixos/modules/security/sudo.nix @@ -192,6 +192,10 @@ in ###### implementation config = mkIf cfg.enable { + assertions = [ + { assertion = cfg.package.pname != "sudo-rs"; + message = "The NixOS `sudo` module does not work with `sudo-rs` yet."; } + ]; # We `mkOrder 600` so that the default rule shows up first, but there is # still enough room for a user to `mkBefore` it. diff --git a/nixos/modules/security/wrappers/default.nix b/nixos/modules/security/wrappers/default.nix index 12255d8392fe9..ad65f80bb2ca5 100644 --- a/nixos/modules/security/wrappers/default.nix +++ b/nixos/modules/security/wrappers/default.nix @@ -5,8 +5,8 @@ let parentWrapperDir = dirOf wrapperDir; - securityWrapper = pkgs.callPackage ./wrapper.nix { - inherit parentWrapperDir; + securityWrapper = sourceProg : pkgs.callPackage ./wrapper.nix { + inherit sourceProg; }; fileModeType = @@ -91,8 +91,7 @@ let , ... }: '' - cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" - echo -n "${source}" > "$wrapperDir/${program}.real" + cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" # Prevent races chmod 0000 "$wrapperDir/${program}" @@ -119,8 +118,7 @@ let , ... }: '' - cp ${securityWrapper}/bin/security-wrapper "$wrapperDir/${program}" - echo -n "${source}" > "$wrapperDir/${program}.real" + cp ${securityWrapper source}/bin/security-wrapper "$wrapperDir/${program}" # Prevent races chmod 0000 "$wrapperDir/${program}" @@ -248,11 +246,13 @@ in export PATH="${wrapperDir}:$PATH" ''; - security.apparmor.includes."nixos/security.wrappers" = '' - include "${pkgs.apparmorRulesFromClosure { name="security.wrappers"; } [ - securityWrapper + security.apparmor.includes = lib.mapAttrs' (wrapName: wrap: lib.nameValuePair + "nixos/security.wrappers/${wrapName}" '' + include "${pkgs.apparmorRulesFromClosure { name="security.wrappers.${wrapName}"; } [ + (securityWrapper wrap.source) ]}" - ''; + mrpx ${wrap.source}, + '') wrappers; ###### wrappers activation script system.activationScripts.wrappers = diff --git a/nixos/modules/security/wrappers/wrapper.c b/nixos/modules/security/wrappers/wrapper.c index 17776a97af81e..2cf1727a31c8c 100644 --- a/nixos/modules/security/wrappers/wrapper.c +++ b/nixos/modules/security/wrappers/wrapper.c @@ -17,6 +17,10 @@ #include <syscall.h> #include <byteswap.h> +#ifndef SOURCE_PROG +#error SOURCE_PROG should be defined via preprocessor commandline +#endif + // aborts when false, printing the failed expression #define ASSERT(expr) ((expr) ? (void) 0 : assert_failure(#expr)) // aborts when returns non-zero, printing the failed expression and errno @@ -24,10 +28,6 @@ extern char **environ; -// The WRAPPER_DIR macro is supplied at compile time so that it cannot -// be changed at runtime -static char *wrapper_dir = WRAPPER_DIR; - // Wrapper debug variable name static char *wrapper_debug = "WRAPPER_DEBUG"; @@ -151,115 +151,20 @@ static int make_caps_ambient(const char *self_path) { return 0; } -int readlink_malloc(const char *p, char **ret) { - size_t l = FILENAME_MAX+1; - int r; - - for (;;) { - char *c = calloc(l, sizeof(char)); - if (!c) { - return -ENOMEM; - } - - ssize_t n = readlink(p, c, l-1); - if (n < 0) { - r = -errno; - free(c); - return r; - } - - if ((size_t) n < l-1) { - c[n] = 0; - *ret = c; - return 0; - } - - free(c); - l *= 2; - } -} - int main(int argc, char **argv) { ASSERT(argc >= 1); - char *self_path = NULL; - int self_path_size = readlink_malloc("/proc/self/exe", &self_path); - if (self_path_size < 0) { - fprintf(stderr, "cannot readlink /proc/self/exe: %s", strerror(-self_path_size)); - } - - unsigned int ruid, euid, suid, rgid, egid, sgid; - MUSTSUCCEED(getresuid(&ruid, &euid, &suid)); - MUSTSUCCEED(getresgid(&rgid, &egid, &sgid)); - - // If true, then we did not benefit from setuid privilege escalation, - // where the original uid is still in ruid and different from euid == suid. - int didnt_suid = (ruid == euid) && (euid == suid); - // If true, then we did not benefit from setgid privilege escalation - int didnt_sgid = (rgid == egid) && (egid == sgid); - - - // Make sure that we are being executed from the right location, - // i.e., `safe_wrapper_dir'. This is to prevent someone from creating - // hard link `X' from some other location, along with a false - // `X.real' file, to allow arbitrary programs from being executed - // with elevated capabilities. - int len = strlen(wrapper_dir); - if (len > 0 && '/' == wrapper_dir[len - 1]) - --len; - ASSERT(!strncmp(self_path, wrapper_dir, len)); - ASSERT('/' == wrapper_dir[0]); - ASSERT('/' == self_path[len]); - - // If we got privileges with the fs set[ug]id bit, check that the privilege we - // got matches the one one we expected, ie that our effective uid/gid - // matches the uid/gid of `self_path`. This ensures that we were executed as - // `self_path', and not, say, as some other setuid program. - // We don't check that if we did not benefit from the set[ug]id bit, as - // can be the case in nosuid mounts or user namespaces. - struct stat st; - ASSERT(lstat(self_path, &st) != -1); - - // if the wrapper gained privilege with suid, check that we got the uid of the file owner - ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == euid)); - // if the wrapper gained privilege with sgid, check that we got the gid of the file group - ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == egid)); - // same, but with suid instead of euid - ASSERT(!((st.st_mode & S_ISUID) && !didnt_suid) || (st.st_uid == suid)); - ASSERT(!((st.st_mode & S_ISGID) && !didnt_sgid) || (st.st_gid == sgid)); - - // And, of course, we shouldn't be writable. - ASSERT(!(st.st_mode & (S_IWGRP | S_IWOTH))); - - // Read the path of the real (wrapped) program from <self>.real. - char real_fn[PATH_MAX + 10]; - int real_fn_size = snprintf(real_fn, sizeof(real_fn), "%s.real", self_path); - ASSERT(real_fn_size < sizeof(real_fn)); - - int fd_self = open(real_fn, O_RDONLY); - ASSERT(fd_self != -1); - - char source_prog[PATH_MAX]; - len = read(fd_self, source_prog, PATH_MAX); - ASSERT(len != -1); - ASSERT(len < sizeof(source_prog)); - ASSERT(len > 0); - source_prog[len] = 0; - - close(fd_self); // Read the capabilities set on the wrapper and raise them in to // the ambient set so the program we're wrapping receives the // capabilities too! - if (make_caps_ambient(self_path) != 0) { - free(self_path); + if (make_caps_ambient("/proc/self/exe") != 0) { return 1; } - free(self_path); - execve(source_prog, argv, environ); + execve(SOURCE_PROG, argv, environ); fprintf(stderr, "%s: cannot run `%s': %s\n", - argv[0], source_prog, strerror(errno)); + argv[0], SOURCE_PROG, strerror(errno)); return 1; } diff --git a/nixos/modules/security/wrappers/wrapper.nix b/nixos/modules/security/wrappers/wrapper.nix index e3620fb222d2c..aec43412404ea 100644 --- a/nixos/modules/security/wrappers/wrapper.nix +++ b/nixos/modules/security/wrappers/wrapper.nix @@ -1,4 +1,4 @@ -{ stdenv, linuxHeaders, parentWrapperDir, debug ? false }: +{ stdenv, linuxHeaders, sourceProg, debug ? false }: # For testing: # $ nix-build -E 'with import <nixpkgs> {}; pkgs.callPackage ./wrapper.nix { parentWrapperDir = "/run/wrappers"; debug = true; }' stdenv.mkDerivation { @@ -7,7 +7,7 @@ stdenv.mkDerivation { dontUnpack = true; hardeningEnable = [ "pie" ]; CFLAGS = [ - ''-DWRAPPER_DIR="${parentWrapperDir}"'' + ''-DSOURCE_PROG="${sourceProg}"'' ] ++ (if debug then [ "-Werror" "-Og" "-g" ] else [ diff --git a/nixos/modules/services/backup/duplicati.nix b/nixos/modules/services/backup/duplicati.nix index 007396ebfc9b4..9b422635e7f08 100644 --- a/nixos/modules/services/backup/duplicati.nix +++ b/nixos/modules/services/backup/duplicati.nix @@ -10,6 +10,8 @@ in services.duplicati = { enable = mkEnableOption (lib.mdDoc "Duplicati"); + package = mkPackageOptionMD pkgs "duplicati" { }; + port = mkOption { default = 8200; type = types.port; @@ -53,7 +55,7 @@ in }; config = mkIf cfg.enable { - environment.systemPackages = [ pkgs.duplicati ]; + environment.systemPackages = [ cfg.package ]; systemd.services.duplicati = { description = "Duplicati backup"; @@ -63,7 +65,7 @@ in { User = cfg.user; Group = "duplicati"; - ExecStart = "${pkgs.duplicati}/bin/duplicati-server --webservice-interface=${cfg.interface} --webservice-port=${toString cfg.port} --server-datafolder=${cfg.dataDir}"; + ExecStart = "${cfg.package}/bin/duplicati-server --webservice-interface=${cfg.interface} --webservice-port=${toString cfg.port} --server-datafolder=${cfg.dataDir}"; Restart = "on-failure"; } (mkIf (cfg.dataDir == "/var/lib/duplicati") { @@ -83,4 +85,3 @@ in }; } - diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index 6f4cbab817267..78220e99c3d1f 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -260,6 +260,16 @@ in Restic package to use. ''; }; + + createWrapper = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether to generate and add a script to the system path, that has the same environment variables set + as the systemd service. This can be used to e.g. mount snapshots or perform other opterations, without + having to manually specify most options. + ''; + }; }; })); default = { }; @@ -316,7 +326,8 @@ in in nameValuePair "restic-backups-${name}" ({ environment = { - RESTIC_CACHE_DIR = "%C/restic-backups-${name}"; + # not %C, because that wouldn't work in the wrapper script + RESTIC_CACHE_DIR = "/var/cache/restic-backups-${name}"; RESTIC_PASSWORD_FILE = backup.passwordFile; RESTIC_REPOSITORY = backup.repository; RESTIC_REPOSITORY_FILE = backup.repositoryFile; @@ -331,7 +342,7 @@ in nameValuePair (rcloneAttrToConf name) (toRcloneVal value) ) backup.rcloneConfig); - path = [ pkgs.openssh ]; + path = [ config.programs.ssh.package ]; restartIfChanged = false; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; @@ -378,5 +389,22 @@ in timerConfig = backup.timerConfig; }) config.services.restic.backups; + + # generate wrapper scripts, as described in the createWrapper option + environment.systemPackages = lib.mapAttrsToList (name: backup: let + extraOptions = lib.concatMapStrings (arg: " -o ${arg}") backup.extraOptions; + resticCmd = "${backup.package}/bin/restic${extraOptions}"; + in pkgs.writeShellScriptBin "restic-${name}" '' + set -a # automatically export variables + ${lib.optionalString (backup.environmentFile != null) "source ${backup.environmentFile}"} + # set same environment variables as the systemd service + ${lib.pipe config.systemd.services."restic-backups-${name}".environment [ + (lib.filterAttrs (_: v: v != null)) + (lib.mapAttrsToList (n: v: "${n}=${v}")) + (lib.concatStringsSep "\n") + ]} + + exec ${resticCmd} $@ + '') (lib.filterAttrs (_: v: v.createWrapper) config.services.restic.backups); }; } diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix index 595374ea1e5b3..b4b997201c8f5 100644 --- a/nixos/modules/services/continuous-integration/buildbot/master.nix +++ b/nixos/modules/services/continuous-integration/buildbot/master.nix @@ -272,7 +272,13 @@ in { Group = cfg.group; WorkingDirectory = cfg.home; # NOTE: call twistd directly with stdout logging for systemd - ExecStart = "${python.pkgs.twisted}/bin/twistd -o --nodaemon --pidfile= --logfile - --python ${tacFile}"; + ExecStart = "${python.pkgs.twisted}/bin/twistd -o --nodaemon --pidfile= --logfile - --python ${cfg.buildbotDir}/buildbot.tac"; + # To reload on upgrade, set the following in your configuration: + # systemd.services.buildbot-master.reloadIfChanged = true; + ExecReload = [ + "${pkgs.coreutils}/bin/ln -sf ${tacFile} ${cfg.buildbotDir}/buildbot.tac" + "${pkgs.coreutils}/bin/kill -HUP $MAINPID" + ]; }; }; }; diff --git a/nixos/modules/services/continuous-integration/hail.nix b/nixos/modules/services/continuous-integration/hail.nix deleted file mode 100644 index 62e8b8077c079..0000000000000 --- a/nixos/modules/services/continuous-integration/hail.nix +++ /dev/null @@ -1,61 +0,0 @@ -{ config, lib, pkgs, ...}: - -with lib; - -let - cfg = config.services.hail; -in { - - - ###### interface - - options.services.hail = { - enable = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Enables the Hail Auto Update Service. Hail can automatically deploy artifacts - built by a Hydra Continuous Integration server. A common use case is to provide - continuous deployment for single services or a full NixOS configuration.''; - }; - profile = mkOption { - type = types.str; - default = "hail-profile"; - description = lib.mdDoc "The name of the Nix profile used by Hail."; - }; - hydraJobUri = mkOption { - type = types.str; - description = lib.mdDoc "The URI of the Hydra Job."; - }; - netrc = mkOption { - type = types.nullOr types.path; - description = lib.mdDoc "The netrc file to use when fetching data from Hydra."; - default = null; - }; - package = mkOption { - type = types.package; - default = pkgs.haskellPackages.hail; - defaultText = literalExpression "pkgs.haskellPackages.hail"; - description = lib.mdDoc "Hail package to use."; - }; - }; - - - ###### implementation - - config = mkIf cfg.enable { - systemd.services.hail = { - description = "Hail Auto Update Service"; - wants = [ "network-online.target" ]; - wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ nix ]; - environment = { - HOME = "/var/lib/empty"; - }; - serviceConfig = { - ExecStart = "${cfg.package}/bin/hail --profile ${cfg.profile} --job-uri ${cfg.hydraJobUri}" - + lib.optionalString (cfg.netrc != null) " --netrc-file ${cfg.netrc}"; - }; - }; - }; -} diff --git a/nixos/modules/services/continuous-integration/woodpecker/agents.nix b/nixos/modules/services/continuous-integration/woodpecker/agents.nix index cc5b903afd595..3b883c72ff07f 100644 --- a/nixos/modules/services/continuous-integration/woodpecker/agents.nix +++ b/nixos/modules/services/continuous-integration/woodpecker/agents.nix @@ -35,6 +35,16 @@ let ''; }; + path = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + example = [ "" ]; + description = lib.mdDoc '' + Additional packages that should be added to the agent's `PATH`. + Mostly useful for the `local` backend. + ''; + }; + environmentFile = lib.mkOption { type = lib.types.listOf lib.types.path; default = [ ]; @@ -94,7 +104,7 @@ let "-/etc/localtime" ]; }; - inherit (agentCfg) environment; + inherit (agentCfg) environment path; }; }; in @@ -106,28 +116,41 @@ in agents = lib.mkOption { default = { }; type = lib.types.attrsOf agentModule; - example = { - docker = { - environment = { - WOODPECKER_SERVER = "localhost:9000"; - WOODPECKER_BACKEND = "docker"; - DOCKER_HOST = "unix:///run/podman/podman.sock"; + example = lib.literalExpression '' + { + podman = { + environment = { + WOODPECKER_SERVER = "localhost:9000"; + WOODPECKER_BACKEND = "docker"; + DOCKER_HOST = "unix:///run/podman/podman.sock"; + }; + + extraGroups = [ "podman" ]; + + environmentFile = [ "/run/secrets/woodpecker/agent-secret.txt" ]; }; - extraGroups = [ "docker" ]; + exec = { + environment = { + WOODPECKER_SERVER = "localhost:9000"; + WOODPECKER_BACKEND = "local"; + }; - environmentFile = "/run/secrets/woodpecker/agent-secret.txt"; - }; + environmentFile = [ "/run/secrets/woodpecker/agent-secret.txt" ]; - exec = { - environment = { - WOODPECKER_SERVER = "localhost:9000"; - WOODPECKER_BACKEND = "exec"; + path = [ + # Needed to clone repos + git + git-lfs + woodpecker-plugin-git + # Used by the runner as the default shell + bash + # Most likely to be used in pipeline definitions + coreutils + ]; }; - - environmentFile = "/run/secrets/woodpecker/agent-secret.txt"; - }; - }; + } + ''; description = lib.mdDoc "woodpecker-agents configurations"; }; }; diff --git a/nixos/modules/services/databases/surrealdb.nix b/nixos/modules/services/databases/surrealdb.nix index 050a5336cb4c6..28bd97cd731ea 100644 --- a/nixos/modules/services/databases/surrealdb.nix +++ b/nixos/modules/services/databases/surrealdb.nix @@ -47,17 +47,13 @@ in { example = 8000; }; - userNamePath = mkOption { - type = types.path; + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + example = [ "--allow-all" "--auth" "--user root" "--pass root" ]; description = lib.mdDoc '' - Path to read the username from. - ''; - }; - - passwordPath = mkOption { - type = types.path; - description = lib.mdDoc '' - Path to read the password from. + Specify a list of additional command line flags, + which get escaped and are then passed to surrealdb. ''; }; }; @@ -73,19 +69,8 @@ in { wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - script = '' - ${cfg.package}/bin/surreal start \ - --user $(${pkgs.systemd}/bin/systemd-creds cat SURREALDB_USERNAME) \ - --pass $(${pkgs.systemd}/bin/systemd-creds cat SURREALDB_PASSWORD) \ - --bind ${cfg.host}:${toString cfg.port} \ - -- ${cfg.dbPath} - ''; serviceConfig = { - LoadCredential = [ - "SURREALDB_USERNAME:${cfg.userNamePath}" - "SURREALDB_PASSWORD:${cfg.passwordPath}" - ]; - + ExecStart = "${cfg.package}/bin/surreal start --bind ${cfg.host}:${toString cfg.port} ${escapeShellArgs cfg.extraFlags} -- ${cfg.dbPath}"; DynamicUser = true; Restart = "on-failure"; StateDirectory = "surrealdb"; diff --git a/nixos/modules/services/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix index fd2e03ef12f5d..cf27bdd8b6eb7 100644 --- a/nixos/modules/services/hardware/auto-cpufreq.nix +++ b/nixos/modules/services/hardware/auto-cpufreq.nix @@ -15,8 +15,7 @@ in { description = lib.mdDoc '' Configuration for `auto-cpufreq`. - See its [example configuration file] for supported settings. - [example configuration file]: https://github.com/AdnanHodzic/auto-cpufreq/blob/master/auto-cpufreq.conf-example + The available options can be found in [the example configuration file](https://github.com/AdnanHodzic/auto-cpufreq/blob/v${pkgs.auto-cpufreq.version}/auto-cpufreq.conf-example). ''; default = {}; @@ -35,6 +34,7 @@ in { wantedBy = [ "multi-user.target" ]; path = with pkgs; [ bash coreutils ]; + serviceConfig.WorkingDirectory = ""; serviceConfig.ExecStart = [ "" "${lib.getExe pkgs.auto-cpufreq} --daemon --config ${cfgFile}" @@ -42,4 +42,7 @@ in { }; }; }; + + # uses attributes of the linked package + meta.buildDocsInSandbox = false; } diff --git a/nixos/modules/services/hardware/openrgb.nix b/nixos/modules/services/hardware/openrgb.nix index 310615ecc5396..13b1d07e53b7d 100644 --- a/nixos/modules/services/hardware/openrgb.nix +++ b/nixos/modules/services/hardware/openrgb.nix @@ -17,7 +17,14 @@ in { motherboard = mkOption { type = types.nullOr (types.enum [ "amd" "intel" ]); - default = null; + default = if config.hardware.cpu.intel.updateMicrocode then "intel" + else if config.hardware.cpu.amd.updateMicrocode then "amd" + else null; + defaultText = literalMD '' + if config.hardware.cpu.intel.updateMicrocode then "intel" + else if config.hardware.cpu.amd.updateMicrocode then "amd" + else null; + ''; description = lib.mdDoc "CPU family of motherboard. Allows for addition motherboard i2c support."; }; diff --git a/nixos/modules/services/home-automation/zigbee2mqtt.nix b/nixos/modules/services/home-automation/zigbee2mqtt.nix index 796de3a491e49..6b5bd8a0d9bbc 100644 --- a/nixos/modules/services/home-automation/zigbee2mqtt.nix +++ b/nixos/modules/services/home-automation/zigbee2mqtt.nix @@ -66,9 +66,10 @@ in server = mkDefault "mqtt://localhost:1883"; }; serial.port = mkDefault "/dev/ttyACM0"; - # reference device configuration, that is kept in a separate file + # reference device/group configuration, that is kept in a separate file # to prevent it being overwritten in the units ExecStartPre script devices = mkDefault "devices.yaml"; + groups = mkDefault "groups.yaml"; }; systemd.services.zigbee2mqtt = { diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix index 342ac5ec6e049..ba1445f083975 100644 --- a/nixos/modules/services/logging/logrotate.nix +++ b/nixos/modules/services/logging/logrotate.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, utils, ... }: with lib; @@ -220,6 +220,12 @@ in in this case you can disable the failing check with this option. ''; }; + + extraArgs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + description = "Additional command line arguments to pass on logrotate invocation"; + }; }; }; @@ -231,7 +237,7 @@ in serviceConfig = { Restart = "no"; User = "root"; - ExecStart = "${pkgs.logrotate}/sbin/logrotate ${mailOption} ${cfg.configFile}"; + ExecStart = "${pkgs.logrotate}/sbin/logrotate ${utils.escapeSystemdExecArgs cfg.extraArgs} ${mailOption} ${cfg.configFile}"; }; }; systemd.services.logrotate-checkconf = { @@ -240,7 +246,7 @@ in serviceConfig = { Type = "oneshot"; RemainAfterExit = true; - ExecStart = "${pkgs.logrotate}/sbin/logrotate --debug ${cfg.configFile}"; + ExecStart = "${pkgs.logrotate}/sbin/logrotate ${utils.escapeSystemdExecArgs cfg.extraArgs} --debug ${cfg.configFile}"; }; }; }; diff --git a/nixos/modules/services/mail/listmonk.nix b/nixos/modules/services/mail/listmonk.nix index 251362fdd89d3..11b2a5186229b 100644 --- a/nixos/modules/services/mail/listmonk.nix +++ b/nixos/modules/services/mail/listmonk.nix @@ -54,7 +54,7 @@ let smtp = mkOption { type = listOf (submodule { - freeformType = with types; attrsOf (oneOf [ str int bool ]); + freeformType = with types; attrsOf anything; options = { enabled = mkEnableOption (lib.mdDoc "this SMTP server for listmonk"); @@ -86,7 +86,7 @@ let # TODO: refine this type based on the smtp one. "bounce.mailboxes" = mkOption { type = listOf - (submodule { freeformType = with types; oneOf [ str int bool ]; }); + (submodule { freeformType = with types; listOf (attrsOf anything); }); default = [ ]; description = lib.mdDoc "List of bounce mailboxes"; }; diff --git a/nixos/modules/services/mail/stalwart-mail.nix b/nixos/modules/services/mail/stalwart-mail.nix new file mode 100644 index 0000000000000..eb87d9f6f695e --- /dev/null +++ b/nixos/modules/services/mail/stalwart-mail.nix @@ -0,0 +1,106 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.stalwart-mail; + configFormat = pkgs.formats.toml { }; + configFile = configFormat.generate "stalwart-mail.toml" cfg.settings; + dataDir = "/var/lib/stalwart-mail"; + +in { + options.services.stalwart-mail = { + enable = mkEnableOption (mdDoc "the Stalwart all-in-one email server"); + package = mkPackageOptionMD pkgs "stalwart-mail" { }; + + settings = mkOption { + inherit (configFormat) type; + default = { }; + description = mdDoc '' + Configuration options for the Stalwart email server. + See <https://stalw.art/docs/category/configuration> for available options. + + By default, the module is configured to store everything locally. + ''; + }; + }; + + config = mkIf cfg.enable { + # Default config: all local + services.stalwart-mail.settings = { + global.tracing.method = mkDefault "stdout"; + global.tracing.level = mkDefault "info"; + queue.path = mkDefault "${dataDir}/queue"; + report.path = mkDefault "${dataDir}/reports"; + store.db.path = mkDefault "${dataDir}/data/index.sqlite3"; + store.blob.type = mkDefault "local"; + store.blob.local.path = mkDefault "${dataDir}/data/blobs"; + resolver.type = mkDefault "system"; + }; + + systemd.services.stalwart-mail = { + wantedBy = [ "multi-user.target" ]; + after = [ "local-fs.target" "network.target" ]; + + preStart = '' + mkdir -p ${dataDir}/{queue,reports,data/blobs} + ''; + + serviceConfig = { + ExecStart = + "${cfg.package}/bin/stalwart-mail --config=${configFile}"; + + # Base from template resources/systemd/stalwart-mail.service + Type = "simple"; + LimitNOFILE = 65536; + KillMode = "process"; + KillSignal = "SIGINT"; + Restart = "on-failure"; + RestartSec = 5; + StandardOutput = "syslog"; + StandardError = "syslog"; + SyslogIdentifier = "stalwart-mail"; + + DynamicUser = true; + User = "stalwart-mail"; + StateDirectory = "stalwart-mail"; + + # Bind standard privileged ports + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + + # Hardening + DeviceAllow = [ "" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = false; # incompatible with CAP_NET_BIND_SERVICE + ProcSubset = "pid"; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + UMask = "0077"; + }; + }; + + # Make admin commands available in the shell + environment.systemPackages = [ cfg.package ]; + }; + + meta = { + maintainers = with maintainers; [ happysalada pacien ]; + }; +} diff --git a/nixos/modules/services/matrix/mautrix-telegram.nix b/nixos/modules/services/matrix/mautrix-telegram.nix index 17032ed808e93..97a6ba858e004 100644 --- a/nixos/modules/services/matrix/mautrix-telegram.nix +++ b/nixos/modules/services/matrix/mautrix-telegram.nix @@ -159,7 +159,6 @@ in { if [ ! -f '${registrationFile}' ]; then ${pkgs.mautrix-telegram}/bin/mautrix-telegram \ --generate-registration \ - --base-config='${pkgs.mautrix-telegram}/${pkgs.mautrix-telegram.pythonModule.sitePackages}/mautrix_telegram/example-config.yaml' \ --config='${settingsFile}' \ --registration='${registrationFile}' fi diff --git a/nixos/modules/services/matrix/mautrix-whatsapp.nix b/nixos/modules/services/matrix/mautrix-whatsapp.nix index 80c85980196f3..c4dc482134956 100644 --- a/nixos/modules/services/matrix/mautrix-whatsapp.nix +++ b/nixos/modules/services/matrix/mautrix-whatsapp.nix @@ -11,53 +11,47 @@ settingsFileUnsubstituted = settingsFormat.generate "mautrix-whatsapp-config-unsubstituted.json" cfg.settings; settingsFormat = pkgs.formats.json {}; appservicePort = 29318; + + mkDefaults = lib.mapAttrsRecursive (n: v: lib.mkDefault v); + defaultConfig = { + homeserver.address = "http://localhost:8448"; + appservice = { + hostname = "[::]"; + port = appservicePort; + database.type = "sqlite3"; + database.uri = "${dataDir}/mautrix-whatsapp.db"; + id = "whatsapp"; + bot.username = "whatsappbot"; + bot.displayname = "WhatsApp Bridge Bot"; + as_token = ""; + hs_token = ""; + }; + bridge = { + username_template = "whatsapp_{{.}}"; + displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)"; + double_puppet_server_map = {}; + login_shared_secret_map = {}; + command_prefix = "!wa"; + permissions."*" = "relay"; + relay.enabled = true; + }; + logging = { + min_level = "info"; + writers = lib.singleton { + type = "stdout"; + format = "pretty-colored"; + time_format = " "; + }; + }; + }; + in { - imports = []; options.services.mautrix-whatsapp = { - enable = lib.mkEnableOption "mautrix-whatsapp, a puppeting/relaybot bridge between Matrix and WhatsApp."; + enable = lib.mkEnableOption (lib.mdDoc "mautrix-whatsapp, a puppeting/relaybot bridge between Matrix and WhatsApp."); settings = lib.mkOption { type = settingsFormat.type; - default = { - appservice = { - address = "http://localhost:${toString appservicePort}"; - hostname = "[::]"; - port = appservicePort; - database = { - type = "sqlite3"; - uri = "${dataDir}/mautrix-whatsapp.db"; - }; - id = "whatsapp"; - bot = { - username = "whatsappbot"; - displayname = "WhatsApp Bridge Bot"; - }; - as_token = ""; - hs_token = ""; - }; - bridge = { - username_template = "whatsapp_{{.}}"; - displayname_template = "{{if .BusinessName}}{{.BusinessName}}{{else if .PushName}}{{.PushName}}{{else}}{{.JID}}{{end}} (WA)"; - double_puppet_server_map = {}; - login_shared_secret_map = {}; - command_prefix = "!wa"; - permissions."*" = "relay"; - relay.enabled = true; - }; - logging = { - min_level = "info"; - writers = [ - { - type = "stdout"; - format = "pretty-colored"; - } - { - type = "file"; - format = "json"; - } - ]; - }; - }; + default = defaultConfig; description = lib.mdDoc '' {file}`config.yaml` configuration as a Nix attribute set. Configuration options should match those described in @@ -117,10 +111,22 @@ in { }; config = lib.mkIf cfg.enable { - services.mautrix-whatsapp.settings = { - homeserver.domain = lib.mkDefault config.services.matrix-synapse.settings.server_name; + + users.users.mautrix-whatsapp = { + isSystemUser = true; + group = "mautrix-whatsapp"; + home = dataDir; + description = "Mautrix-WhatsApp bridge user"; }; + users.groups.mautrix-whatsapp = {}; + + services.mautrix-whatsapp.settings = lib.mkMerge (map mkDefaults [ + defaultConfig + # Note: this is defined here to avoid the docs depending on `config` + { homeserver.domain = config.services.matrix-synapse.settings.server_name; } + ]); + systemd.services.mautrix-whatsapp = { description = "Mautrix-WhatsApp Service - A WhatsApp bridge for Matrix"; @@ -158,10 +164,11 @@ in { ''; serviceConfig = { - DynamicUser = true; + User = "mautrix-whatsapp"; + Group = "mautrix-whatsapp"; EnvironmentFile = cfg.environmentFile; StateDirectory = baseNameOf dataDir; - WorkingDirectory = "${dataDir}"; + WorkingDirectory = dataDir; ExecStart = '' ${pkgs.mautrix-whatsapp}/bin/mautrix-whatsapp \ --config='${settingsFile}' \ diff --git a/nixos/modules/services/misc/atuin.nix b/nixos/modules/services/misc/atuin.nix index 57ff02df7d68b..8d2c1b5242ffa 100644 --- a/nixos/modules/services/misc/atuin.nix +++ b/nixos/modules/services/misc/atuin.nix @@ -6,7 +6,7 @@ in { options = { services.atuin = { - enable = lib.mkEnableOption (mdDoc "Enable server for shell history sync with atuin"); + enable = lib.mkEnableOption (mdDoc "Atuin server for shell history sync"); openRegistration = mkOption { type = types.bool; diff --git a/nixos/modules/services/misc/cfdyndns.nix b/nixos/modules/services/misc/cfdyndns.nix index 9cd8b188ffaec..5a02de2aad213 100644 --- a/nixos/modules/services/misc/cfdyndns.nix +++ b/nixos/modules/services/misc/cfdyndns.nix @@ -23,6 +23,15 @@ in ''; }; + apiTokenFile = mkOption { + default = null; + type = types.nullOr types.str; + description = lib.mdDoc '' + The path to a file containing the API Token + used to authenticate with CloudFlare. + ''; + }; + apikeyFile = mkOption { default = null; type = types.nullOr types.str; @@ -55,12 +64,15 @@ in Group = config.ids.gids.cfdyndns; }; environment = { - CLOUDFLARE_EMAIL="${cfg.email}"; CLOUDFLARE_RECORDS="${concatStringsSep "," cfg.records}"; }; script = '' ${optionalString (cfg.apikeyFile != null) '' export CLOUDFLARE_APIKEY="$(cat ${escapeShellArg cfg.apikeyFile})" + export CLOUDFLARE_EMAIL="${cfg.email}" + ''} + ${optionalString (cfg.apiTokenFile != null) '' + export CLOUDFLARE_APITOKEN="$(cat ${escapeShellArg cfg.apiTokenFile})" ''} ${pkgs.cfdyndns}/bin/cfdyndns ''; diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index c5e38b4988298..b399ccc38f58d 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -1088,6 +1088,11 @@ in { ''Support for container registries other than gitlab-container-registry has ended since GitLab 16.0.0 and is scheduled for removal in a future release. Please back up your data and migrate to the gitlab-container-registry package.'' ) + (mkIf + (versionAtLeast (getVersion cfg.packages.gitlab) "16.2.0" && versionOlder (getVersion cfg.packages.gitlab) "16.5.0") + ''GitLab instances created or updated between versions [15.11.0, 15.11.2] have an incorrect database schema. + Check the upstream documentation for a workaround: https://docs.gitlab.com/ee/update/versions/gitlab_16_changes.html#undefined-column-error-upgrading-to-162-or-later'' + ) ]; assertions = [ @@ -1655,7 +1660,7 @@ in { Restart = "on-failure"; WorkingDirectory = "${cfg.packages.gitlab}/share/gitlab"; ExecStart = concatStringsSep " " [ - "${cfg.packages.gitlab.rubyEnv}/bin/puma" + "${cfg.packages.gitlab.rubyEnv}/bin/bundle" "exec" "puma" "-e production" "-C ${cfg.statePath}/config/puma.rb" "-w ${cfg.puma.workers}" diff --git a/nixos/modules/services/monitoring/mimir.nix b/nixos/modules/services/monitoring/mimir.nix index edca9b7be4ff0..6ed139b229748 100644 --- a/nixos/modules/services/monitoring/mimir.nix +++ b/nixos/modules/services/monitoring/mimir.nix @@ -32,11 +32,21 @@ in { type = types.package; description = lib.mdDoc ''Mimir package to use.''; }; + + extraFlags = mkOption { + type = types.listOf types.str; + default = []; + example = [ "--config.expand-env=true" ]; + description = lib.mdDoc '' + Specify a list of additional command line flags, + which get escaped and are then passed to Mimir. + ''; + }; }; config = mkIf cfg.enable { # for mimirtool - environment.systemPackages = [ pkgs.mimir ]; + environment.systemPackages = [ cfg.package ]; assertions = [{ assertion = ( @@ -60,7 +70,7 @@ in { else cfg.configFile; in { - ExecStart = "${cfg.package}/bin/mimir --config.file=${conf}"; + ExecStart = "${cfg.package}/bin/mimir --config.file=${conf} ${escapeShellArgs cfg.extraFlags}"; DynamicUser = true; Restart = "always"; ProtectSystem = "full"; diff --git a/nixos/modules/services/networking/dae.nix b/nixos/modules/services/networking/dae.nix index 231c555b33030..42ed3c7f8d4aa 100644 --- a/nixos/modules/services/networking/dae.nix +++ b/nixos/modules/services/networking/dae.nix @@ -1,41 +1,161 @@ -{ config, pkgs, lib, ... }: +{ config, lib, pkgs, ... }: + let cfg = config.services.dae; + assets = cfg.assets; + genAssetsDrv = paths: pkgs.symlinkJoin { + name = "dae-assets"; + inherit paths; + }; in { - meta.maintainers = with lib.maintainers; [ pokon548 ]; + meta.maintainers = with lib.maintainers; [ pokon548 oluceps ]; options = { - services.dae = { - enable = lib.options.mkEnableOption (lib.mdDoc "the dae service"); - package = lib.mkPackageOptionMD pkgs "dae" { }; + services.dae = with lib;{ + enable = mkEnableOption + (mdDoc "A Linux high-performance transparent proxy solution based on eBPF"); + + package = mkPackageOptionMD pkgs "dae" { }; + + assets = mkOption { + type = with types;(listOf path); + default = with pkgs; [ v2ray-geoip v2ray-domain-list-community ]; + defaultText = literalExpression "with pkgs; [ v2ray-geoip v2ray-domain-list-community ]"; + description = mdDoc '' + Assets required to run dae. + ''; + }; + + assetsPath = mkOption { + type = types.str; + default = "${genAssetsDrv assets}/share/v2ray"; + defaultText = literalExpression '' + (symlinkJoin { + name = "dae-assets"; + paths = assets; + })/share/v2ray + ''; + description = mdDoc '' + The path which contains geolocation database. + This option will override `assets`. + ''; + }; + + openFirewall = mkOption { + type = with types; submodule { + options = { + enable = mkEnableOption "enable"; + port = mkOption { + type = types.int; + description = '' + Port to be opened. Consist with field `tproxy_port` in config file. + ''; + }; + }; + }; + default = { + enable = true; + port = 12345; + }; + defaultText = literalExpression '' + { + enable = true; + port = 12345; + } + ''; + description = mdDoc '' + Open the firewall port. + ''; + }; + + configFile = mkOption { + type = types.path; + default = "/etc/dae/config.dae"; + example = "/path/to/your/config.dae"; + description = mdDoc '' + The path of dae config file, end with `.dae`. + ''; + }; + + config = mkOption { + type = types.str; + default = '' + global{} + routing{} + ''; + description = mdDoc '' + Config text for dae. + + See <https://github.com/daeuniverse/dae/blob/main/example.dae>. + ''; + }; + + disableTxChecksumIpGeneric = + mkEnableOption (mdDoc "See <https://github.com/daeuniverse/dae/issues/43>"); + }; }; - config = lib.mkIf config.services.dae.enable { - networking.firewall.allowedTCPPorts = [ 12345 ]; - networking.firewall.allowedUDPPorts = [ 12345 ]; + config = lib.mkIf cfg.enable + + { + environment.systemPackages = [ cfg.package ]; + systemd.packages = [ cfg.package ]; - systemd.services.dae = { - unitConfig = { - Description = "dae Service"; - Documentation = "https://github.com/daeuniverse/dae"; - After = [ "network-online.target" "systemd-sysctl.service" ]; - Wants = [ "network-online.target" ]; + environment.etc."dae/config.dae" = { + mode = "0400"; + source = pkgs.writeText "config.dae" cfg.config; }; - serviceConfig = { - User = "root"; - ExecStartPre = "${lib.getExe cfg.package} validate -c /etc/dae/config.dae"; - ExecStart = "${lib.getExe cfg.package} run --disable-timestamp -c /etc/dae/config.dae"; - ExecReload = "${lib.getExe cfg.package} reload $MAINPID"; - LimitNPROC = 512; - LimitNOFILE = 1048576; - Restart = "on-abnormal"; - Type = "notify"; + networking = lib.mkIf cfg.openFirewall.enable { + firewall = + let portToOpen = cfg.openFirewall.port; + in + { + allowedTCPPorts = [ portToOpen ]; + allowedUDPPorts = [ portToOpen ]; + }; }; - wantedBy = [ "multi-user.target" ]; + systemd.services.dae = + let + daeBin = lib.getExe cfg.package; + TxChecksumIpGenericWorkaround = with lib;(getExe pkgs.writeShellApplication { + name = "disable-tx-checksum-ip-generic"; + text = with pkgs; '' + iface=$(${iproute2}/bin/ip route | ${lib.getExe gawk} '/default/ {print $5}') + ${lib.getExe ethtool} -K "$iface" tx-checksum-ip-generic off + ''; + }); + in + { + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStartPre = [ "" "${daeBin} validate -c ${cfg.configFile}" ] + ++ (with lib; optional cfg.disableTxChecksumIpGeneric TxChecksumIpGenericWorkaround); + ExecStart = [ "" "${daeBin} run --disable-timestamp -c ${cfg.configFile}" ]; + Environment = "DAE_LOCATION_ASSET=${cfg.assetsPath}"; + }; + }; + + assertions = [ + { + assertion = lib.pathExists (toString (genAssetsDrv cfg.assets) + "/share/v2ray"); + message = '' + Packages in `assets` has no preset paths included. + Please set `assetsPath` instead. + ''; + } + + { + assertion = !((config.services.dae.config != "global{}\nrouting{}\n") + && (config.services.dae.configFile != "/etc/dae/config.dae")); + message = '' + Option `config` and `configFile` could not be set + at the same time. + ''; + } + ]; }; - }; } diff --git a/nixos/modules/services/networking/jool.nix b/nixos/modules/services/networking/jool.nix index 3aafbe40967ce..d2d2b0956e8aa 100644 --- a/nixos/modules/services/networking/jool.nix +++ b/nixos/modules/services/networking/jool.nix @@ -16,7 +16,7 @@ let TemporaryFileSystem = [ "/" ]; BindReadOnlyPaths = [ builtins.storeDir - "/run/current-system/kernel-modules" + "/run/booted-system/kernel-modules" ]; # Give capabilities to load the module and configure it @@ -31,26 +31,96 @@ let configFormat = pkgs.formats.json {}; - mkDefaultAttrs = lib.mapAttrs (n: v: lib.mkDefault v); + # Generate the config file of instance `name` + nat64Conf = name: + configFormat.generate "jool-nat64-${name}.conf" + (cfg.nat64.${name} // { instance = name; }); + siitConf = name: + configFormat.generate "jool-siit-${name}.conf" + (cfg.siit.${name} // { instance = name; }); + + # NAT64 config type + nat64Options = lib.types.submodule { + # The format is plain JSON + freeformType = configFormat.type; + # Some options with a default value + options.framework = lib.mkOption { + type = lib.types.enum [ "netfilter" "iptables" ]; + default = "netfilter"; + description = lib.mdDoc '' + The framework to use for attaching Jool's translation to the exist + kernel packet processing rules. See the + [documentation](https://nicmx.github.io/Jool/en/intro-jool.html#design) + for the differences between the two options. + ''; + }; + options.global.pool6 = lib.mkOption { + type = lib.types.strMatching "[[:xdigit:]:]+/[[:digit:]]+" + // { description = "Network prefix in CIDR notation"; }; + default = "64:ff9b::/96"; + description = lib.mdDoc '' + The prefix used for embedding IPv4 into IPv6 addresses. + Defaults to the well-known NAT64 prefix, defined by + [RFC 6052](https://datatracker.ietf.org/doc/html/rfc6052). + ''; + }; + }; - defaultNat64 = { - instance = "default"; - framework = "netfilter"; - global.pool6 = "64:ff9b::/96"; + # SIIT config type + siitOptions = lib.types.submodule { + # The format is, again, plain JSON + freeformType = configFormat.type; + # Some options with a default value + options = { inherit (nat64Options.getSubOptions []) framework; }; + }; + + makeNat64Unit = name: opts: { + "jool-nat64-${name}" = { + description = "Jool, NAT64 setup of instance ${name}"; + documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.kmod}/bin/modprobe jool"; + ExecStart = "${jool-cli}/bin/jool file handle ${nat64Conf name}"; + ExecStop = "${jool-cli}/bin/jool -f ${nat64Conf name} instance remove"; + } // hardening; + }; }; - defaultSiit = { - instance = "default"; - framework = "netfilter"; + + makeSiitUnit = name: opts: { + "jool-siit-${name}" = { + description = "Jool, SIIT setup of instance ${name}"; + documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ]; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStartPre = "${pkgs.kmod}/bin/modprobe jool_siit"; + ExecStart = "${jool-cli}/bin/jool_siit file handle ${siitConf name}"; + ExecStop = "${jool-cli}/bin/jool_siit -f ${siitConf name} instance remove"; + } // hardening; + }; }; - nat64Conf = configFormat.generate "jool-nat64.conf" cfg.nat64.config; - siitConf = configFormat.generate "jool-siit.conf" cfg.siit.config; + checkNat64 = name: _: '' + printf 'Validating Jool configuration for NAT64 instance "${name}"... ' + jool file check ${nat64Conf name} + printf 'Ok.\n'; touch "$out" + ''; + + checkSiit = name: _: '' + printf 'Validating Jool configuration for SIIT instance "${name}"... ' + jool_siit file check ${siitConf name} + printf 'Ok.\n'; touch "$out" + ''; in { - ###### interface - options = { networking.jool.enable = lib.mkOption { type = lib.types.bool; @@ -64,157 +134,146 @@ in NAT64, analogous to the IPv4 NAPT. Refer to the upstream [documentation](https://nicmx.github.io/Jool/en/intro-xlat.html) for the supported modes of translation and how to configure them. + + Enabling this option will install the Jool kernel module and the + command line tools for controlling it. ''; }; - networking.jool.nat64.enable = lib.mkEnableOption (lib.mdDoc "a NAT64 instance of Jool."); - networking.jool.nat64.config = lib.mkOption { - type = configFormat.type; - default = defaultNat64; + networking.jool.nat64 = lib.mkOption { + type = lib.types.attrsOf nat64Options; + default = { }; example = lib.literalExpression '' { - # custom NAT64 prefix - global.pool6 = "2001:db8:64::/96"; - - # Port forwarding - bib = [ - { # SSH 192.0.2.16 → 2001:db8:a::1 - "protocol" = "TCP"; - "ipv4 address" = "192.0.2.16#22"; - "ipv6 address" = "2001:db8:a::1#22"; - } - { # DNS (TCP) 192.0.2.16 → 2001:db8:a::2 - "protocol" = "TCP"; - "ipv4 address" = "192.0.2.16#53"; - "ipv6 address" = "2001:db8:a::2#53"; - } - { # DNS (UDP) 192.0.2.16 → 2001:db8:a::2 - "protocol" = "UDP"; - "ipv4 address" = "192.0.2.16#53"; - "ipv6 address" = "2001:db8:a::2#53"; - } - ]; - - pool4 = [ - # Ports for dynamic translation - { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } - { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } - { protocol = "ICMP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } - - # Ports for static BIB entries - { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "22"; } - { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "53"; } - ]; + default = { + # custom NAT64 prefix + global.pool6 = "2001:db8:64::/96"; + + # Port forwarding + bib = [ + { # SSH 192.0.2.16 → 2001:db8:a::1 + "protocol" = "TCP"; + "ipv4 address" = "192.0.2.16#22"; + "ipv6 address" = "2001:db8:a::1#22"; + } + { # DNS (TCP) 192.0.2.16 → 2001:db8:a::2 + "protocol" = "TCP"; + "ipv4 address" = "192.0.2.16#53"; + "ipv6 address" = "2001:db8:a::2#53"; + } + { # DNS (UDP) 192.0.2.16 → 2001:db8:a::2 + "protocol" = "UDP"; + "ipv4 address" = "192.0.2.16#53"; + "ipv6 address" = "2001:db8:a::2#53"; + } + ]; + + pool4 = [ + # Port ranges for dynamic translation + { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } + { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } + { protocol = "ICMP"; prefix = "192.0.2.16/32"; "port range" = "40001-65535"; } + + # Ports for static BIB entries + { protocol = "TCP"; prefix = "192.0.2.16/32"; "port range" = "22"; } + { protocol = "UDP"; prefix = "192.0.2.16/32"; "port range" = "53"; } + ]; + }; } ''; description = lib.mdDoc '' - The configuration of a stateful NAT64 instance of Jool managed through - NixOS. See https://nicmx.github.io/Jool/en/config-atomic.html for the - available options. + Definitions of NAT64 instances of Jool. + See the + [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for + the available options. Also check out the + [tutorial](https://nicmx.github.io/Jool/en/run-nat64.html) for an + introduction to NAT64 and how to troubleshoot the setup. + + The attribute name defines the name of the instance, with the main one + being `default`: this can be accessed from the command line without + specifying the name with `-i`. ::: {.note} - Existing or more instances created manually will not interfere with the - NixOS instance, provided the respective `pool4` addresses and port - ranges are not overlapping. + Instances created imperatively from the command line will not interfere + with the NixOS instances, provided the respective `pool4` addresses and + port ranges are not overlapping. ::: ::: {.warning} - Changes to the NixOS instance performed via `jool instance nixos-nat64` - are applied correctly but will be lost after restarting - `jool-nat64.service`. + Changes to an instance performed via `jool -i <name>` are applied + correctly but will be lost after restarting the respective + `jool-nat64-<name>.service`. ::: ''; }; - networking.jool.siit.enable = lib.mkEnableOption (lib.mdDoc "a SIIT instance of Jool."); - networking.jool.siit.config = lib.mkOption { - type = configFormat.type; - default = defaultSiit; + networking.jool.siit = lib.mkOption { + type = lib.types.attrsOf siitOptions; + default = { }; example = lib.literalExpression '' { - # Maps any IPv4 address x.y.z.t to 2001:db8::x.y.z.t and v.v. - pool6 = "2001:db8::/96"; - - # Explicit address mappings - eamt = [ - # 2001:db8:1:: ←→ 192.0.2.0 - { "ipv6 prefix": "2001:db8:1::/128", "ipv4 prefix": "192.0.2.0" } - # 2001:db8:1::x ←→ 198.51.100.x - { "ipv6 prefix": "2001:db8:2::/120", "ipv4 prefix": "198.51.100.0/24" } - ] + default = { + # Maps any IPv4 address x.y.z.t to 2001:db8::x.y.z.t and v.v. + global.pool6 = "2001:db8::/96"; + + # Explicit address mappings + eamt = [ + # 2001:db8:1:: ←→ 192.0.2.0 + { "ipv6 prefix" = "2001:db8:1::/128"; "ipv4 prefix" = "192.0.2.0"; } + # 2001:db8:1::x ←→ 198.51.100.x + { "ipv6 prefix" = "2001:db8:2::/120"; "ipv4 prefix" = "198.51.100.0/24"; } + ]; + }; } ''; description = lib.mdDoc '' - The configuration of a SIIT instance of Jool managed through - NixOS. See https://nicmx.github.io/Jool/en/config-atomic.html for the - available options. + Definitions of SIIT instances of Jool. + See the + [documentation](https://nicmx.github.io/Jool/en/config-atomic.html) for + the available options. Also check out the + [tutorial](https://nicmx.github.io/Jool/en/run-vanilla.html) for an + introduction to SIIT and how to troubleshoot the setup. + + The attribute name defines the name of the instance, with the main one + being `default`: this can be accessed from the command line without + specifying the name with `-i`. ::: {.note} - Existing or more instances created manually will not interfere with the - NixOS instance, provided the respective `EAMT` address mappings are not - overlapping. + Instances created imperatively from the command line will not interfere + with the NixOS instances, provided the respective EAMT addresses and + port ranges are not overlapping. ::: ::: {.warning} - Changes to the NixOS instance performed via `jool instance nixos-siit` - are applied correctly but will be lost after restarting - `jool-siit.service`. + Changes to an instance performed via `jool -i <name>` are applied + correctly but will be lost after restarting the respective + `jool-siit-<name>.service`. ::: ''; }; }; - ###### implementation - config = lib.mkIf cfg.enable { - environment.systemPackages = [ jool-cli ]; + # Install kernel module and cli tools boot.extraModulePackages = [ jool ]; + environment.systemPackages = [ jool-cli ]; - systemd.services.jool-nat64 = lib.mkIf cfg.nat64.enable { - description = "Jool, NAT64 setup"; - documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ]; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - reloadIfChanged = true; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStartPre = "${pkgs.kmod}/bin/modprobe jool"; - ExecStart = "${jool-cli}/bin/jool file handle ${nat64Conf}"; - ExecStop = "${jool-cli}/bin/jool -f ${nat64Conf} instance remove"; - } // hardening; - }; - - systemd.services.jool-siit = lib.mkIf cfg.siit.enable { - description = "Jool, SIIT setup"; - documentation = [ "https://nicmx.github.io/Jool/en/documentation.html" ]; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - reloadIfChanged = true; - serviceConfig = { - Type = "oneshot"; - RemainAfterExit = true; - ExecStartPre = "${pkgs.kmod}/bin/modprobe jool_siit"; - ExecStart = "${jool-cli}/bin/jool_siit file handle ${siitConf}"; - ExecStop = "${jool-cli}/bin/jool_siit -f ${siitConf} instance remove"; - } // hardening; - }; - - system.checks = lib.singleton (pkgs.runCommand "jool-validated" { - nativeBuildInputs = [ pkgs.buildPackages.jool-cli ]; - preferLocalBuild = true; - } '' - printf 'Validating Jool configuration... ' - ${lib.optionalString cfg.siit.enable "jool_siit file check ${siitConf}"} - ${lib.optionalString cfg.nat64.enable "jool file check ${nat64Conf}"} - printf 'ok\n' - touch "$out" - ''); - - networking.jool.nat64.config = mkDefaultAttrs defaultNat64; - networking.jool.siit.config = mkDefaultAttrs defaultSiit; + # Install services for each instance + systemd.services = lib.mkMerge + (lib.mapAttrsToList makeNat64Unit cfg.nat64 ++ + lib.mapAttrsToList makeSiitUnit cfg.siit); + # Check the configuration of each instance + system.checks = lib.optional (cfg.nat64 != {} || cfg.siit != {}) + (pkgs.runCommand "jool-validated" + { + nativeBuildInputs = with pkgs.buildPackages; [ jool-cli ]; + preferLocalBuild = true; + } + (lib.concatStrings + (lib.mapAttrsToList checkNat64 cfg.nat64 ++ + lib.mapAttrsToList checkSiit cfg.siit))); }; meta.maintainers = with lib.maintainers; [ rnhmjoj ]; diff --git a/nixos/modules/services/networking/nftables.nix b/nixos/modules/services/networking/nftables.nix index 0e4cd6fa1503b..47159ade328c8 100644 --- a/nixos/modules/services/networking/nftables.nix +++ b/nixos/modules/services/networking/nftables.nix @@ -70,6 +70,26 @@ in ''; }; + networking.nftables.checkRulesetRedirects = mkOption { + type = types.addCheck (types.attrsOf types.path) (attrs: all types.path.check (attrNames attrs)); + default = { + "/etc/hosts" = config.environment.etc.hosts.source; + "/etc/protocols" = config.environment.etc.protocols.source; + "/etc/services" = config.environment.etc.services.source; + }; + defaultText = literalExpression '' + { + "/etc/hosts" = config.environment.etc.hosts.source; + "/etc/protocols" = config.environment.etc.protocols.source; + "/etc/services" = config.environment.etc.services.source; + } + ''; + description = mdDoc '' + Set of paths that should be intercepted and rewritten while checking the ruleset + using `pkgs.buildPackages.libredirect`. + ''; + }; + networking.nftables.preCheckRuleset = mkOption { type = types.lines; default = ""; @@ -282,7 +302,7 @@ in cp $out ruleset.conf sed 's|include "${deletionsScriptVar}"||' -i ruleset.conf ${cfg.preCheckRuleset} - export NIX_REDIRECTS=/etc/protocols=${pkgs.buildPackages.iana-etc}/etc/protocols:/etc/services=${pkgs.buildPackages.iana-etc}/etc/services + export NIX_REDIRECTS=${escapeShellArg (concatStringsSep ":" (mapAttrsToList (n: v: "${n}=${v}") cfg.checkRulesetRedirects))} LD_PRELOAD="${pkgs.buildPackages.libredirect}/lib/libredirect.so ${pkgs.buildPackages.lklWithFirewall.lib}/lib/liblkl-hijack.so" \ ${pkgs.buildPackages.nftables}/bin/nft --check --file ruleset.conf ''; diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix index 067d5df487255..3c3ecc3e04d79 100644 --- a/nixos/modules/services/networking/wstunnel.nix +++ b/nixos/modules/services/networking/wstunnel.nix @@ -86,12 +86,12 @@ let description = mdDoc "Address and port to listen on. Setting the port to a value below 1024 will also give the process the required `CAP_NET_BIND_SERVICE` capability."; type = types.submodule hostPortSubmodule; default = { - address = "0.0.0.0"; + host = "0.0.0.0"; port = if config.enableHTTPS then 443 else 80; }; defaultText = literalExpression '' { - address = "0.0.0.0"; + host = "0.0.0.0"; port = if enableHTTPS then 443 else 80; } ''; diff --git a/nixos/modules/services/web-apps/galene.nix b/nixos/modules/services/web-apps/galene.nix index 747b85f94c65a..81fed8a0b99a5 100644 --- a/nixos/modules/services/web-apps/galene.nix +++ b/nixos/modules/services/web-apps/galene.nix @@ -186,7 +186,7 @@ in ProtectSystem = "strict"; ReadWritePaths = cfg.recordingsDir; RemoveIPC = true; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_NETLINK" ]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; diff --git a/nixos/modules/services/web-apps/grocy.nix b/nixos/modules/services/web-apps/grocy.nix index b0d3c44cea81c..4d1084e295ff3 100644 --- a/nixos/modules/services/web-apps/grocy.nix +++ b/nixos/modules/services/web-apps/grocy.nix @@ -115,9 +115,9 @@ in { user = "grocy"; group = "nginx"; - # PHP 8.1 is the only version which is supported/tested by upstream: - # https://github.com/grocy/grocy/blob/v4.0.0/README.md#platform-support - phpPackage = pkgs.php81; + # PHP 8.1 and 8.2 are the only version which are supported/tested by upstream: + # https://github.com/grocy/grocy/blob/v4.0.2/README.md#platform-support + phpPackage = pkgs.php82; inherit (cfg.phpfpm) settings; @@ -130,6 +130,16 @@ in { }; }; + # After an update of grocy, the viewcache needs to be deleted. Otherwise grocy will not work + # https://github.com/grocy/grocy#how-to-update + systemd.services.grocy-setup = { + wantedBy = [ "multi-user.target" ]; + before = [ "phpfpm-grocy.service" ]; + script = '' + rm -rf ${cfg.dataDir}/viewcache/* + ''; + }; + services.nginx = { enable = true; virtualHosts."${cfg.hostName}" = mkMerge [ diff --git a/nixos/modules/services/web-apps/healthchecks.nix b/nixos/modules/services/web-apps/healthchecks.nix index b3fdb681e2f30..b92525075541a 100644 --- a/nixos/modules/services/web-apps/healthchecks.nix +++ b/nixos/modules/services/web-apps/healthchecks.nix @@ -1,16 +1,16 @@ -{ config, lib, pkgs, buildEnv, ... }: +{ config, lib, options, pkgs, buildEnv, ... }: with lib; let defaultUser = "healthchecks"; cfg = config.services.healthchecks; + opt = options.services.healthchecks; pkg = cfg.package; boolToPython = b: if b then "True" else "False"; environment = { PYTHONPATH = pkg.pythonPath; STATIC_ROOT = cfg.dataDir + "/static"; - DB_NAME = "${cfg.dataDir}/healthchecks.sqlite"; } // cfg.settings; environmentFile = pkgs.writeText "healthchecks-environment" (lib.generators.toKeyValue { } environment); @@ -98,17 +98,24 @@ in description = lib.mdDoc '' Environment variables which are read by healthchecks `(local)_settings.py`. - Settings which are explicitly covered in options bewlow, are type-checked and/or transformed + Settings which are explicitly covered in options below, are type-checked and/or transformed before added to the environment, everything else is passed as a string. See <https://healthchecks.io/docs/self_hosted_configuration/> for a full documentation of settings. - We add two variables to this list inside the packages `local_settings.py.` - - STATIC_ROOT to set a state directory for dynamically generated static files. - - SECRET_KEY_FILE to read SECRET_KEY from a file at runtime and keep it out of /nix/store. + We add additional variables to this list inside the packages `local_settings.py.` + - `STATIC_ROOT` to set a state directory for dynamically generated static files. + - `SECRET_KEY_FILE` to read `SECRET_KEY` from a file at runtime and keep it out of + /nix/store. + - `_FILE` variants for several values that hold sensitive information in + [Healthchecks configuration](https://healthchecks.io/docs/self_hosted_configuration/) so + that they also can be read from a file and kept out of /nix/store. To see which values + have support for a `_FILE` variant, run: + - `nix-instantiate --eval --expr '(import <nixpkgs> {}).healthchecks.secrets'` + - or `nix eval 'nixpkgs#healthchecks.secrets'` if the flake support has been enabled. ''; - type = types.submodule { + type = types.submodule (settings: { freeformType = types.attrsOf types.str; options = { ALLOWED_HOSTS = lib.mkOption { @@ -143,8 +150,28 @@ in ''; apply = boolToPython; }; + + DB = mkOption { + type = types.enum [ "sqlite" "postgres" "mysql" ]; + default = "sqlite"; + description = lib.mdDoc "Database engine to use."; + }; + + DB_NAME = mkOption { + type = types.str; + default = + if settings.config.DB == "sqlite" + then "${cfg.dataDir}/healthchecks.sqlite" + else "hc"; + defaultText = lib.literalExpression '' + if config.${settings.options.DB} == "sqlite" + then "''${config.${opt.dataDir}}/healthchecks.sqlite" + else "hc" + ''; + description = lib.mdDoc "Database name."; + }; }; - }; + }); }; }; @@ -168,7 +195,7 @@ in StateDirectoryMode = mkIf (cfg.dataDir == "/var/lib/healthchecks") "0750"; }; in - { + { healthchecks-migration = { description = "Healthchecks migrations"; wantedBy = [ "healthchecks.target" ]; diff --git a/nixos/modules/services/web-apps/mobilizon.nix b/nixos/modules/services/web-apps/mobilizon.nix new file mode 100644 index 0000000000000..4e796e2bc80cf --- /dev/null +++ b/nixos/modules/services/web-apps/mobilizon.nix @@ -0,0 +1,442 @@ +{ pkgs, lib, config, ... }: + +with lib; + +let + cfg = config.services.mobilizon; + + user = "mobilizon"; + group = "mobilizon"; + + settingsFormat = pkgs.formats.elixirConf { elixir = pkgs.elixir_1_14; }; + + configFile = settingsFormat.generate "mobilizon-config.exs" cfg.settings; + + # Make a package containing launchers with the correct envirenment, instead of + # setting it with systemd services, so that the user can also use them without + # troubles + launchers = pkgs.stdenv.mkDerivation rec { + pname = "${cfg.package.pname}-launchers"; + inherit (cfg.package) version; + + src = cfg.package; + + nativeBuildInputs = with pkgs; [ makeWrapper ]; + + dontBuild = true; + + installPhase = '' + mkdir -p $out/bin + + makeWrapper \ + $src/bin/mobilizon \ + $out/bin/mobilizon \ + --run '. ${secretEnvFile}' \ + --set MOBILIZON_CONFIG_PATH "${configFile}" \ + --set-default RELEASE_TMP "/tmp" + + makeWrapper \ + $src/bin/mobilizon_ctl \ + $out/bin/mobilizon_ctl \ + --run '. ${secretEnvFile}' \ + --set MOBILIZON_CONFIG_PATH "${configFile}" \ + --set-default RELEASE_TMP "/tmp" + ''; + }; + + repoSettings = cfg.settings.":mobilizon"."Mobilizon.Storage.Repo"; + instanceSettings = cfg.settings.":mobilizon".":instance"; + + isLocalPostgres = repoSettings.socket_dir != null; + + dbUser = if repoSettings.username != null then repoSettings.username else "mobilizon"; + + postgresql = config.services.postgresql.package; + postgresqlSocketDir = "/var/run/postgresql"; + + secretEnvFile = "/var/lib/mobilizon/secret-env.sh"; +in +{ + options = { + services.mobilizon = { + enable = mkEnableOption + "Mobilizon federated organization and mobilization platform"; + + nginx.enable = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Whether an <literal>nginx</literal> virtual host should be + set up to serve Mobilizon. + ''; + }; + + package = mkPackageOptionMD pkgs "mobilizon" { }; + + settings = mkOption { + type = + let + elixirTypes = settingsFormat.lib.types; + in + types.submodule { + freeformType = settingsFormat.type; + + options = { + ":mobilizon" = { + + "Mobilizon.Web.Endpoint" = { + url.host = mkOption { + type = elixirTypes.str; + defaultText = literalExpression '' + ''${settings.":mobilizon".":instance".hostname} + ''; + description = '' + Your instance's hostname for generating URLs throughout the app + ''; + }; + + http = { + port = mkOption { + type = elixirTypes.port; + default = 4000; + description = '' + The port to run the server + ''; + }; + ip = mkOption { + type = elixirTypes.tuple; + default = settingsFormat.lib.mkTuple [ 0 0 0 0 0 0 0 1 ]; + description = '' + The IP address to listen on. Defaults to [::1] notated as a byte tuple. + ''; + }; + }; + + has_reverse_proxy = mkOption { + type = elixirTypes.bool; + default = true; + description = '' + Whether you use a reverse proxy + ''; + }; + }; + + ":instance" = { + name = mkOption { + type = elixirTypes.str; + description = '' + The fallback instance name if not configured into the admin UI + ''; + }; + + hostname = mkOption { + type = elixirTypes.str; + description = '' + Your instance's hostname + ''; + }; + + email_from = mkOption { + type = elixirTypes.str; + defaultText = literalExpression '' + noreply@''${settings.":mobilizon".":instance".hostname} + ''; + description = '' + The email for the From: header in emails + ''; + }; + + email_reply_to = mkOption { + type = elixirTypes.str; + defaultText = literalExpression '' + ''${email_from} + ''; + description = '' + The email for the Reply-To: header in emails + ''; + }; + }; + + "Mobilizon.Storage.Repo" = { + socket_dir = mkOption { + type = types.nullOr elixirTypes.str; + default = postgresqlSocketDir; + description = '' + Path to the postgres socket directory. + + Set this to null if you want to connect to a remote database. + + If non-null, the local PostgreSQL server will be configured with + the configured database, permissions, and required extensions. + + If connecting to a remote database, please follow the + instructions on how to setup your database: + <link xlink:href="https://docs.joinmobilizon.org/administration/install/release/#database-setup"/> + ''; + }; + + username = mkOption { + type = types.nullOr elixirTypes.str; + default = user; + description = '' + User used to connect to the database + ''; + }; + + database = mkOption { + type = types.nullOr elixirTypes.str; + default = "mobilizon_prod"; + description = '' + Name of the database + ''; + }; + }; + }; + }; + }; + default = { }; + + description = '' + Mobilizon Elixir documentation, see + <link xlink:href="https://docs.joinmobilizon.org/administration/configure/reference/"/> + for supported values. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + assertions = [ + { + assertion = cfg.nginx.enable -> (cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.ip == settingsFormat.lib.mkTuple [ 0 0 0 0 0 0 0 1 ]); + message = "Setting the IP mobilizon listens on is only possible when the nginx config is not used, as it is hardcoded there."; + } + ]; + + services.mobilizon.settings = { + ":mobilizon" = { + "Mobilizon.Web.Endpoint" = { + server = true; + url.host = mkDefault instanceSettings.hostname; + secret_key_base = + settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_INSTANCE_SECRET"; }; + }; + + "Mobilizon.Web.Auth.Guardian".secret_key = + settingsFormat.lib.mkGetEnv { envVariable = "MOBILIZON_AUTH_SECRET"; }; + + ":instance" = { + registrations_open = mkDefault false; + demo = mkDefault false; + email_from = mkDefault "noreply@${instanceSettings.hostname}"; + email_reply_to = mkDefault instanceSettings.email_from; + }; + + "Mobilizon.Storage.Repo" = { + # Forced by upstream since it uses PostgreSQL-specific extensions + adapter = settingsFormat.lib.mkAtom "Ecto.Adapters.Postgres"; + pool_size = mkDefault 10; + }; + }; + + ":tzdata".":data_dir" = "/var/lib/mobilizon/tzdata/"; + }; + + # This somewhat follows upstream's systemd service here: + # https://framagit.org/framasoft/mobilizon/-/blob/master/support/systemd/mobilizon.service + systemd.services.mobilizon = { + description = "Mobilizon federated organization and mobilization platform"; + + wantedBy = [ "multi-user.target" ]; + + path = with pkgs; [ + gawk + imagemagick + libwebp + file + + # Optional: + gifsicle + jpegoptim + optipng + pngquant + ]; + + serviceConfig = { + ExecStartPre = "${launchers}/bin/mobilizon_ctl migrate"; + ExecStart = "${launchers}/bin/mobilizon start"; + ExecStop = "${launchers}/bin/mobilizon stop"; + + User = user; + Group = group; + + StateDirectory = "mobilizon"; + + Restart = "on-failure"; + + PrivateTmp = true; + ProtectSystem = "full"; + NoNewPrivileges = true; + + ReadWritePaths = mkIf isLocalPostgres postgresqlSocketDir; + }; + }; + + # Create the needed secrets before running Mobilizon, so that they are not + # in the nix store + # + # Since some of these tasks are quite common for Elixir projects (COOKIE for + # every BEAM project, Phoenix and Guardian are also quite common), this + # service could be abstracted in the future, and used by other Elixir + # projects. + systemd.services.mobilizon-setup-secrets = { + description = "Mobilizon setup secrets"; + before = [ "mobilizon.service" ]; + wantedBy = [ "mobilizon.service" ]; + + script = + let + # Taken from here: + # https://framagit.org/framasoft/mobilizon/-/blob/1.0.7/lib/mix/tasks/mobilizon/instance.ex#L132-133 + genSecret = + "IO.puts(:crypto.strong_rand_bytes(64)" + + "|> Base.encode64()" + + "|> binary_part(0, 64))"; + + # Taken from here: + # https://github.com/elixir-lang/elixir/blob/v1.11.3/lib/mix/lib/mix/release.ex#L499 + genCookie = "IO.puts(Base.encode32(:crypto.strong_rand_bytes(32)))"; + + evalElixir = str: '' + ${pkgs.elixir_1_14}/bin/elixir --eval '${str}' + ''; + in + '' + set -euxo pipefail + + if [ ! -f "${secretEnvFile}" ]; then + install -m 600 /dev/null "${secretEnvFile}" + cat > "${secretEnvFile}" <<EOF + # This file was automatically generated by mobilizon-setup-secrets.service + export MOBILIZON_AUTH_SECRET='$(${evalElixir genSecret})' + export MOBILIZON_INSTANCE_SECRET='$(${evalElixir genSecret})' + export RELEASE_COOKIE='$(${evalElixir genCookie})' + EOF + fi + ''; + + serviceConfig = { + Type = "oneshot"; + User = user; + Group = group; + StateDirectory = "mobilizon"; + }; + }; + + # Add the required PostgreSQL extensions to the local PostgreSQL server, + # if local PostgreSQL is configured. + systemd.services.mobilizon-postgresql = mkIf isLocalPostgres { + description = "Mobilizon PostgreSQL setup"; + + after = [ "postgresql.service" ]; + before = [ "mobilizon.service" "mobilizon-setup-secrets.service" ]; + wantedBy = [ "mobilizon.service" ]; + + path = [ postgresql ]; + + # Taken from here: + # https://framagit.org/framasoft/mobilizon/-/blob/1.1.0/priv/templates/setup_db.eex + script = + '' + psql "${repoSettings.database}" -c "\ + CREATE EXTENSION IF NOT EXISTS postgis; \ + CREATE EXTENSION IF NOT EXISTS pg_trgm; \ + CREATE EXTENSION IF NOT EXISTS unaccent;" + ''; + + serviceConfig = { + Type = "oneshot"; + User = config.services.postgresql.superUser; + }; + }; + + systemd.tmpfiles.rules = [ + "d /var/lib/mobilizon/uploads/exports/csv 700 mobilizon mobilizon - -" + "Z /var/lib/mobilizon 700 mobilizon mobilizon - -" + ]; + + services.postgresql = mkIf isLocalPostgres { + enable = true; + ensureDatabases = [ repoSettings.database ]; + ensureUsers = [ + { + name = dbUser; + ensurePermissions = { + "DATABASE \"${repoSettings.database}\"" = "ALL PRIVILEGES"; + }; + } + ]; + extraPlugins = with postgresql.pkgs; [ postgis ]; + }; + + # Nginx config taken from support/nginx/mobilizon-release.conf + services.nginx = + let + inherit (cfg.settings.":mobilizon".":instance") hostname; + proxyPass = "http://[::1]:" + + toString cfg.settings.":mobilizon"."Mobilizon.Web.Endpoint".http.port; + in + lib.mkIf cfg.nginx.enable { + enable = true; + virtualHosts."${hostname}" = { + enableACME = lib.mkDefault true; + forceSSL = lib.mkDefault true; + extraConfig = '' + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + ''; + locations."/" = { + inherit proxyPass; + }; + locations."~ ^/(js|css|img)" = { + root = "${cfg.package}/lib/mobilizon-${cfg.package.version}/priv/static"; + extraConfig = '' + etag off; + access_log off; + add_header Cache-Control "public, max-age=31536000, immutable"; + ''; + }; + locations."~ ^/(media|proxy)" = { + inherit proxyPass; + extraConfig = '' + etag off; + access_log off; + add_header Cache-Control "public, max-age=31536000, immutable"; + ''; + }; + }; + }; + + users.users.${user} = { + description = "Mobilizon daemon user"; + group = group; + isSystemUser = true; + }; + + users.groups.${group} = { }; + + # So that we have the `mobilizon` and `mobilizon_ctl` commands. + # The `mobilizon remote` command is useful for dropping a shell into the + # running Mobilizon instance, and `mobilizon_ctl` is used for common + # management tasks (e.g. adding users). + environment.systemPackages = [ launchers ]; + }; + + meta.maintainers = with lib.maintainers; [ minijackson erictapen ]; +} diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix index 911daa53e6587..4b308d2ee56ee 100644 --- a/nixos/modules/services/web-apps/plausible.nix +++ b/nixos/modules/services/web-apps/plausible.nix @@ -248,6 +248,7 @@ in { # setup ${cfg.package}/createdb.sh ${cfg.package}/migrate.sh + ${cfg.package}/bin/plausible eval "(Plausible.Release.prepare() ; Plausible.Auth.create_user(\"$ADMIN_USER_NAME\", \"$ADMIN_USER_EMAIL\", \"$ADMIN_USER_PWD\"))" ${optionalString cfg.adminUser.activate '' if ! ${cfg.package}/init-admin.sh | grep 'already exists'; then psql -d plausible <<< "UPDATE users SET email_verified=true;" diff --git a/nixos/modules/services/web-apps/tt-rss.nix b/nixos/modules/services/web-apps/tt-rss.nix index 3102e6a469536..592ab253f7daf 100644 --- a/nixos/modules/services/web-apps/tt-rss.nix +++ b/nixos/modules/services/web-apps/tt-rss.nix @@ -595,47 +595,9 @@ let tt-rss = { description = "Tiny Tiny RSS feeds update daemon"; - preStart = let - callSql = e: - if cfg.database.type == "pgsql" then '' - ${optionalString (cfg.database.password != null) "PGPASSWORD=${cfg.database.password}"} \ - ${optionalString (cfg.database.passwordFile != null) "PGPASSWORD=$(cat ${cfg.database.passwordFile})"} \ - ${config.services.postgresql.package}/bin/psql \ - -U ${cfg.database.user} \ - ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} --port ${toString dbPort}"} \ - -c '${e}' \ - ${cfg.database.name}'' - - else if cfg.database.type == "mysql" then '' - echo '${e}' | ${config.services.mysql.package}/bin/mysql \ - -u ${cfg.database.user} \ - ${optionalString (cfg.database.password != null) "-p${cfg.database.password}"} \ - ${optionalString (cfg.database.host != null) "-h ${cfg.database.host} -P ${toString dbPort}"} \ - ${cfg.database.name}'' - - else ""; - - in (optionalString (cfg.database.type == "pgsql") '' - exists=$(${callSql "select count(*) > 0 from pg_tables where tableowner = user"} \ - | tail -n+3 | head -n-2 | sed -e 's/[ \n\t]*//') - - if [ "$exists" == 'f' ]; then - ${callSql "\\i ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} - else - echo 'The database contains some data. Leaving it as it is.' - fi; - '') - - + (optionalString (cfg.database.type == "mysql") '' - exists=$(${callSql "select count(*) > 0 from information_schema.tables where table_schema = schema()"} \ - | tail -n+2 | sed -e 's/[ \n\t]*//') - - if [ "$exists" == '0' ]; then - ${callSql "\\. ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} - else - echo 'The database contains some data. Leaving it as it is.' - fi; - ''); + preStart = '' + ${pkgs.php81}/bin/php ${cfg.root}/www/update.php --update-schema + ''; serviceConfig = { User = "${cfg.user}"; diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index fccc31b5116cb..7a7fb4061eea5 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -578,11 +578,13 @@ in }; }); default = []; - example = literalExpression ''[ - { addr = "10.0.0.12"; proxyProtocol = true; ssl = true; } - { addr = "0.0.0.0"; } - { addr = "[::0]"; } - ]''; + example = literalExpression '' + [ + { addr = "10.0.0.12"; proxyProtocol = true; ssl = true; } + { addr = "0.0.0.0"; } + { addr = "[::0]"; } + ] + ''; description = lib.mdDoc '' If vhosts do not specify listen, use these addresses by default. This option takes precedence over {option}`defaultListenAddresses` and diff --git a/nixos/modules/services/x11/desktop-managers/budgie.nix b/nixos/modules/services/x11/desktop-managers/budgie.nix index bee627ec76c01..a4f8bd5051ecc 100644 --- a/nixos/modules/services/x11/desktop-managers/budgie.nix +++ b/nixos/modules/services/x11/desktop-managers/budgie.nix @@ -134,6 +134,7 @@ in { # Update user directories. xdg-user-dirs ] + ++ lib.optional config.networking.networkmanager.enable pkgs.networkmanagerapplet ++ (utils.removePackagesByName [ cinnamon.nemo mate.eom @@ -192,7 +193,7 @@ in { # Required by Budgie Panel plugins and/or Budgie Control Center panels. networking.networkmanager.enable = mkDefault true; # for BCC's Network panel. programs.nm-applet.enable = config.networking.networkmanager.enable; # Budgie has no Network applet. - programs.nm-applet.indicator = false; # Budgie doesn't support AppIndicators. + programs.nm-applet.indicator = true; # Budgie uses AppIndicators. hardware.bluetooth.enable = mkDefault true; # for Budgie's Status Indicator and BCC's Bluetooth panel. hardware.pulseaudio.enable = mkDefault true; # for Budgie's Status Indicator and BCC's Sound panel. diff --git a/nixos/modules/services/x11/window-managers/oroborus.nix b/nixos/modules/services/x11/window-managers/oroborus.nix deleted file mode 100644 index 654b8708e48fe..0000000000000 --- a/nixos/modules/services/x11/window-managers/oroborus.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - cfg = config.services.xserver.windowManager.oroborus; -in -{ - ###### interface - options = { - services.xserver.windowManager.oroborus.enable = mkEnableOption (lib.mdDoc "oroborus"); - }; - - ###### implementation - config = mkIf cfg.enable { - services.xserver.windowManager.session = singleton { - name = "oroborus"; - start = '' - ${pkgs.oroborus}/bin/oroborus & - waitPID=$! - ''; - }; - environment.systemPackages = [ pkgs.oroborus ]; - }; -} diff --git a/nixos/modules/system/boot/binfmt.nix b/nixos/modules/system/boot/binfmt.nix index fb7afce4a5808..8c9483f01c102 100644 --- a/nixos/modules/system/boot/binfmt.nix +++ b/nixos/modules/system/boot/binfmt.nix @@ -279,7 +279,7 @@ in { support your new systems. Warning: the builder can execute all emulated systems within the same build, which introduces impurities in the case of cross compilation. ''; - type = types.listOf types.str; + type = types.listOf (types.enum (builtins.attrNames magics)); }; }; }; diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix index 6d0afcc57fcc6..87afc5023253f 100644 --- a/nixos/modules/system/boot/networkd.nix +++ b/nixos/modules/system/boot/networkd.nix @@ -2812,9 +2812,15 @@ let environment.etc."systemd/networkd.conf" = renderConfig cfg.config; - systemd.services.systemd-networkd = { + systemd.services.systemd-networkd = let + isReloadableUnitFileName = unitFileName: strings.hasSuffix ".network" unitFileName; + reloadableUnitFiles = attrsets.filterAttrs (k: v: isReloadableUnitFileName k) unitFiles; + nonReloadableUnitFiles = attrsets.filterAttrs (k: v: !isReloadableUnitFileName k) unitFiles; + unitFileSources = unitFiles: map (x: x.source) (attrValues unitFiles); + in { wantedBy = [ "multi-user.target" ]; - restartTriggers = map (x: x.source) (attrValues unitFiles) ++ [ + reloadTriggers = unitFileSources reloadableUnitFiles; + restartTriggers = unitFileSources nonReloadableUnitFiles ++ [ config.environment.etc."systemd/networkd.conf".source ]; aliases = [ "dbus-org.freedesktop.network1.service" ]; diff --git a/nixos/modules/system/boot/stage-2-init.sh b/nixos/modules/system/boot/stage-2-init.sh index c0af29e3b9909..5a2133f960e2b 100755 --- a/nixos/modules/system/boot/stage-2-init.sh +++ b/nixos/modules/system/boot/stage-2-init.sh @@ -105,7 +105,7 @@ fi # Required by the activation script install -m 0755 -d /etc -if [ -d "/etc/nixos" ]; then +if [ ! -h "/etc/nixos" ]; then install -m 0755 -d /etc/nixos fi install -m 01777 -d /tmp diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix index ac55461107fb9..b6c3085c4f16f 100644 --- a/nixos/modules/system/boot/systemd.nix +++ b/nixos/modules/system/boot/systemd.nix @@ -395,7 +395,9 @@ in description = lib.mdDoc '' The amount of time which can elapse after a reboot has been triggered before a watchdog hardware device will automatically reboot the system. - Valid time units include "ms", "s", "min", "h", "d", and "w". + Valid time units include "ms", "s", "min", "h", "d", and "w". If left + `null`, systemd will use its default of `10min`; see also {command}`man + 5 systemd-system.conf`. ''; }; diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 3f40a5b2dfa0c..5d9fca7a605ee 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -333,6 +333,14 @@ in { visible = "shallow"; description = lib.mdDoc "Definition of slice configurations."; }; + + enableTpm2 = mkOption { + default = true; + type = types.bool; + description = lib.mdDoc '' + Whether to enable TPM2 support in the initrd. + ''; + }; }; config = mkIf (config.boot.initrd.enable && cfg.enable) { @@ -342,8 +350,8 @@ in { # systemd needs this for some features "autofs4" # systemd-cryptenroll - "tpm-tis" - ] ++ lib.optional (pkgs.stdenv.hostPlatform.system != "riscv64-linux") "tpm-crb"; + ] ++ lib.optional cfg.enableTpm2 "tpm-tis" + ++ lib.optional (cfg.enableTpm2 && !(pkgs.stdenv.hostPlatform.isRiscV64 || pkgs.stdenv.hostPlatform.isArmv7)) "tpm-crb"; boot.initrd.systemd = { initrdBin = [pkgs.bash pkgs.coreutils cfg.package.kmod cfg.package] ++ config.system.fsPackages; @@ -421,11 +429,11 @@ in { # so NSS can look up usernames "${pkgs.glibc}/lib/libnss_files.so.2" - ] ++ optionals cfg.package.withCryptsetup [ + ] ++ optionals (cfg.package.withCryptsetup && cfg.enableTpm2) [ # tpm2 support "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-tpm2.so" pkgs.tpm2-tss - + ] ++ optionals cfg.package.withCryptsetup [ # fido2 support "${cfg.package}/lib/cryptsetup/libcryptsetup-token-systemd-fido2.so" "${pkgs.libfido2}/lib/libfido2.so.1" diff --git a/nixos/modules/tasks/bcache.nix b/nixos/modules/tasks/bcache.nix index 35b922dc8a1d3..68531a4d2fed7 100644 --- a/nixos/modules/tasks/bcache.nix +++ b/nixos/modules/tasks/bcache.nix @@ -1,6 +1,10 @@ -{ config, lib, pkgs, ... }: - -{ +{ config, lib, pkgs, ... }: let + cfg = config.boot.bcache; +in { + options.boot.bcache.enable = lib.mkEnableOption (lib.mdDoc "bcache mount support") // { + default = true; + example = false; + }; options.boot.initrd.services.bcache.enable = lib.mkEnableOption (lib.mdDoc "bcache support in the initrd") // { description = lib.mdDoc '' *This will only be used when systemd is used in stage 1.* @@ -9,7 +13,7 @@ ''; }; - config = { + config = lib.mkIf cfg.enable { environment.systemPackages = [ pkgs.bcache-tools ]; diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix index eb1c7512d9200..0d4033ca9430a 100644 --- a/nixos/modules/tasks/network-interfaces.nix +++ b/nixos/modules/tasks/network-interfaces.nix @@ -1396,14 +1396,12 @@ in security.apparmor.policies."bin.ping".profile = lib.mkIf config.security.apparmor.policies."bin.ping".enable (lib.mkAfter '' /run/wrappers/bin/ping { include <abstractions/base> - include <nixos/security.wrappers> + include <nixos/security.wrappers/ping> rpx /run/wrappers/wrappers.*/ping, } /run/wrappers/wrappers.*/ping { include <abstractions/base> - include <nixos/security.wrappers> - r /run/wrappers/wrappers.*/ping.real, - mrpx ${config.security.wrappers.ping.source}, + include <nixos/security.wrappers/ping> capability net_raw, capability setpcap, } diff --git a/nixos/modules/tasks/swraid.nix b/nixos/modules/tasks/swraid.nix index f624294565b06..1174187062d74 100644 --- a/nixos/modules/tasks/swraid.nix +++ b/nixos/modules/tasks/swraid.nix @@ -2,6 +2,8 @@ cfg = config.boot.swraid; + mdadm_conf = config.environment.etc."mdadm.conf"; + in { imports = [ (lib.mkRenamedOptionModule [ "boot" "initrd" "services" "swraid" "enable" ] [ "boot" "swraid" "enable" ]) @@ -36,8 +38,14 @@ in { }; config = lib.mkIf cfg.enable { + warnings = lib.mkIf + ((builtins.match ".*(MAILADDR|PROGRAM).*" mdadm_conf.text) == null) + [ "mdadm: Neither MAILADDR nor PROGRAM has been set. This will cause the `mdmon` service to crash." ]; + environment.systemPackages = [ pkgs.mdadm ]; + environment.etc."mdadm.conf".text = lib.mkAfter cfg.mdadmConf; + services.udev.packages = [ pkgs.mdadm ]; systemd.packages = [ pkgs.mdadm ]; @@ -59,12 +67,10 @@ in { $out/bin/mdadm --version ''; - extraFiles."/etc/mdadm.conf".source = pkgs.writeText "mdadm.conf" config.boot.swraid.mdadmConf; + extraFiles."/etc/mdadm.conf".source = pkgs.writeText "mdadm.conf" mdadm_conf.text; systemd = { - contents."/etc/mdadm.conf" = lib.mkIf (cfg.mdadmConf != "") { - text = cfg.mdadmConf; - }; + contents."/etc/mdadm.conf".text = mdadm_conf.text; packages = [ pkgs.mdadm ]; initrdBin = [ pkgs.mdadm ]; diff --git a/nixos/modules/virtualisation/anbox.nix b/nixos/modules/virtualisation/anbox.nix index c7e9e23c4c928..523d9a9576ef3 100644 --- a/nixos/modules/virtualisation/anbox.nix +++ b/nixos/modules/virtualisation/anbox.nix @@ -5,7 +5,7 @@ with lib; let cfg = config.virtualisation.anbox; - kernelPackages = config.boot.kernelPackages; + addrOpts = v: addr: pref: name: { address = mkOption { default = addr; @@ -25,6 +25,28 @@ let }; }; + finalImage = if cfg.imageModifications == "" then cfg.image else ( pkgs.callPackage ( + { runCommandNoCC, squashfsTools }: + + runCommandNoCC "${cfg.image.name}-modified.img" { + nativeBuildInputs = [ + squashfsTools + ]; + } '' + echo "-> Extracting Anbox root image..." + unsquashfs -dest rootfs ${cfg.image} + + echo "-> Modifying Anbox root image..." + ( + cd rootfs + ${cfg.imageModifications} + ) + + echo "-> Packing modified Anbox root image..." + mksquashfs rootfs $out -comp xz -no-xattrs -all-root + '' + ) { }); + in { @@ -42,6 +64,18 @@ in ''; }; + imageModifications = mkOption { + default = ""; + type = types.lines; + description = lib.mdDoc '' + Commands to edit the image filesystem. + + This can be used to e.g. bundle a privileged F-Droid. + + Commands are ran with PWD being at the root of the filesystem. + ''; + }; + extraInit = mkOption { type = types.lines; default = ""; @@ -67,16 +101,19 @@ in config = mkIf cfg.enable { assertions = singleton { - assertion = versionAtLeast (getVersion config.boot.kernelPackages.kernel) "4.18"; - message = "Anbox needs user namespace support to work properly"; + assertion = with config.boot.kernelPackages; kernelAtLeast "5.5" && kernelOlder "5.18"; + message = "Anbox needs a kernel with binder and ashmem support"; }; environment.systemPackages = with pkgs; [ anbox ]; - services.udev.extraRules = '' - KERNEL=="ashmem", NAME="%k", MODE="0666" - KERNEL=="binder*", NAME="%k", MODE="0666" - ''; + systemd.mounts = singleton { + requiredBy = [ "anbox-container-manager.service" ]; + description = "Anbox Binder File System"; + what = "binder"; + where = "/dev/binderfs"; + type = "binder"; + }; virtualisation.lxc.enable = true; networking.bridges.anbox0.interfaces = []; @@ -87,6 +124,9 @@ in internalInterfaces = [ "anbox0" ]; }; + # Ensures NetworkManager doesn't touch anbox0 + networking.networkmanager.unmanaged = [ "anbox0" ]; + systemd.services.anbox-container-manager = let anboxloc = "/var/lib/anbox"; in { @@ -121,12 +161,13 @@ in ExecStart = '' ${pkgs.anbox}/bin/anbox container-manager \ --data-path=${anboxloc} \ - --android-image=${cfg.image} \ + --android-image=${finalImage} \ --container-network-address=${cfg.ipv4.container.address} \ --container-network-gateway=${cfg.ipv4.gateway.address} \ --container-network-dns-servers=${cfg.ipv4.dns} \ --use-rootfs-overlay \ - --privileged + --privileged \ + --daemon ''; }; }; diff --git a/nixos/modules/virtualisation/lxc-container.nix b/nixos/modules/virtualisation/lxc-container.nix index 55b285b69147a..9402d3bf37d0c 100644 --- a/nixos/modules/virtualisation/lxc-container.nix +++ b/nixos/modules/virtualisation/lxc-container.nix @@ -1,96 +1,16 @@ { lib, config, pkgs, ... }: -with lib; - let - templateSubmodule = { ... }: { - options = { - enable = mkEnableOption (lib.mdDoc "this template"); - - target = mkOption { - description = lib.mdDoc "Path in the container"; - type = types.path; - }; - template = mkOption { - description = lib.mdDoc ".tpl file for rendering the target"; - type = types.path; - }; - when = mkOption { - description = lib.mdDoc "Events which trigger a rewrite (create, copy)"; - type = types.listOf (types.str); - }; - properties = mkOption { - description = lib.mdDoc "Additional properties"; - type = types.attrs; - default = {}; - }; - }; - }; - - toYAML = name: data: pkgs.writeText name (generators.toYAML {} data); - cfg = config.virtualisation.lxc; - templates = if cfg.templates != {} then let - list = mapAttrsToList (name: value: { inherit name; } // value) - (filterAttrs (name: value: value.enable) cfg.templates); - in - { - files = map (tpl: { - source = tpl.template; - target = "/templates/${tpl.name}.tpl"; - }) list; - properties = listToAttrs (map (tpl: nameValuePair tpl.target { - when = tpl.when; - template = "${tpl.name}.tpl"; - properties = tpl.properties; - }) list); - } - else { files = []; properties = {}; }; - -in -{ +in { imports = [ - ../installer/cd-dvd/channel.nix - ../profiles/clone-config.nix - ../profiles/minimal.nix + ./lxc-instance-common.nix ]; options = { virtualisation.lxc = { - templates = mkOption { - description = lib.mdDoc "Templates for LXD"; - type = types.attrsOf (types.submodule (templateSubmodule)); - default = {}; - example = literalExpression '' - { - # create /etc/hostname on container creation. also requires networking.hostName = "" to be set - "hostname" = { - enable = true; - target = "/etc/hostname"; - template = builtins.toFile "hostname.tpl" "{{ container.name }}"; - when = [ "create" ]; - }; - # create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied - "hostname-nix" = { - enable = true; - target = "/etc/nixos/hostname.nix"; - template = builtins.toFile "hostname-nix.tpl" "{ ... }: { networking.hostName = \"{{ container.name }}\"; }"; - # copy keeps the file updated when the container is changed - when = [ "create" "copy" ]; - }; - # copy allow the user to specify a custom configuration.nix - "configuration-nix" = { - enable = true; - target = "/etc/nixos/configuration.nix"; - template = builtins.toFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}"; - when = [ "create" ]; - }; - }; - ''; - }; - - privilegedContainer = mkOption { - type = types.bool; + privilegedContainer = lib.mkOption { + type = lib.types.bool; default = false; description = lib.mdDoc '' Whether this LXC container will be running as a privileged container or not. If set to `true` then @@ -116,24 +36,6 @@ in ${config.nix.package.out}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system ''; - system.build.metadata = pkgs.callPackage ../../lib/make-system-tarball.nix { - contents = [ - { - source = toYAML "metadata.yaml" { - architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0; - creation_date = 1; - properties = { - description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}"; - os = "${config.system.nixos.distroId}"; - release = "${config.system.nixos.codeName}"; - }; - templates = templates.properties; - }; - target = "/metadata.yaml"; - } - ] ++ templates.files; - }; - # TODO: build rootfs as squashfs for faster unpack system.build.tarball = pkgs.callPackage ../../lib/make-system-tarball.nix { extraArgs = "--owner=0"; @@ -180,7 +82,7 @@ in ProtectKernelTunables=no NoNewPrivileges=no LoadCredential= - '' + optionalString cfg.privilegedContainer '' + '' + lib.optionalString cfg.privilegedContainer '' # Additional settings for privileged containers ProtectHome=no ProtectSystem=no @@ -193,28 +95,8 @@ in }) ]; - # Allow the user to login as root without password. - users.users.root.initialHashedPassword = mkOverride 150 ""; - - system.activationScripts.installInitScript = mkForce '' + system.activationScripts.installInitScript = lib.mkForce '' ln -fs $systemConfig/init /sbin/init ''; - - # Some more help text. - services.getty.helpLine = - '' - - Log in as "root" with an empty password. - ''; - - # Containers should be light-weight, so start sshd on demand. - services.openssh.enable = mkDefault true; - services.openssh.startWhenNeeded = mkDefault true; - - # As this is intended as a standalone image, undo some of the minimal profile stuff - environment.noXlibs = false; - documentation.enable = true; - documentation.nixos.enable = true; - services.logrotate.enable = true; }; } diff --git a/nixos/modules/virtualisation/lxc-image-metadata.nix b/nixos/modules/virtualisation/lxc-image-metadata.nix new file mode 100644 index 0000000000000..2c0568b4c4682 --- /dev/null +++ b/nixos/modules/virtualisation/lxc-image-metadata.nix @@ -0,0 +1,104 @@ +{ lib, config, pkgs, ... }: + +let + templateSubmodule = {...}: { + options = { + enable = lib.mkEnableOption "this template"; + + target = lib.mkOption { + description = "Path in the container"; + type = lib.types.path; + }; + template = lib.mkOption { + description = ".tpl file for rendering the target"; + type = lib.types.path; + }; + when = lib.mkOption { + description = "Events which trigger a rewrite (create, copy)"; + type = lib.types.listOf (lib.types.str); + }; + properties = lib.mkOption { + description = "Additional properties"; + type = lib.types.attrs; + default = {}; + }; + }; + }; + + toYAML = name: data: pkgs.writeText name (lib.generators.toYAML {} data); + + cfg = config.virtualisation.lxc; + templates = if cfg.templates != {} then let + list = lib.mapAttrsToList (name: value: { inherit name; } // value) + (lib.filterAttrs (name: value: value.enable) cfg.templates); + in + { + files = map (tpl: { + source = tpl.template; + target = "/templates/${tpl.name}.tpl"; + }) list; + properties = lib.listToAttrs (map (tpl: lib.nameValuePair tpl.target { + when = tpl.when; + template = "${tpl.name}.tpl"; + properties = tpl.properties; + }) list); + } + else { files = []; properties = {}; }; + +in { + options = { + virtualisation.lxc = { + templates = lib.mkOption { + description = "Templates for LXD"; + type = lib.types.attrsOf (lib.types.submodule templateSubmodule); + default = {}; + example = lib.literalExpression '' + { + # create /etc/hostname on container creation + "hostname" = { + enable = true; + target = "/etc/hostname"; + template = builtins.writeFile "hostname.tpl" "{{ container.name }}"; + when = [ "create" ]; + }; + # create /etc/nixos/hostname.nix with a configuration for keeping the hostname applied + "hostname-nix" = { + enable = true; + target = "/etc/nixos/hostname.nix"; + template = builtins.writeFile "hostname-nix.tpl" "{ ... }: { networking.hostName = "{{ container.name }}"; }"; + # copy keeps the file updated when the container is changed + when = [ "create" "copy" ]; + }; + # copy allow the user to specify a custom configuration.nix + "configuration-nix" = { + enable = true; + target = "/etc/nixos/configuration.nix"; + template = builtins.writeFile "configuration-nix" "{{ config_get(\"user.user-data\", properties.default) }}"; + when = [ "create" ]; + }; + }; + ''; + }; + }; + }; + + config = { + system.build.metadata = pkgs.callPackage ../../lib/make-system-tarball.nix { + contents = [ + { + source = toYAML "metadata.yaml" { + architecture = builtins.elemAt (builtins.match "^([a-z0-9_]+).+" (toString pkgs.system)) 0; + creation_date = 1; + properties = { + description = "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} ${pkgs.system}"; + os = "${config.system.nixos.distroId}"; + release = "${config.system.nixos.codeName}"; + }; + templates = templates.properties; + }; + target = "/metadata.yaml"; + } + ] ++ templates.files; + }; + }; +} diff --git a/nixos/modules/virtualisation/lxc-instance-common.nix b/nixos/modules/virtualisation/lxc-instance-common.nix new file mode 100644 index 0000000000000..d6a0e05fb1c96 --- /dev/null +++ b/nixos/modules/virtualisation/lxc-instance-common.nix @@ -0,0 +1,30 @@ +{lib, ...}: + +{ + imports = [ + ./lxc-image-metadata.nix + + ../installer/cd-dvd/channel.nix + ../profiles/clone-config.nix + ../profiles/minimal.nix + ]; + + # Allow the user to login as root without password. + users.users.root.initialHashedPassword = lib.mkOverride 150 ""; + + # Some more help text. + services.getty.helpLine = '' + + Log in as "root" with an empty password. + ''; + + # Containers should be light-weight, so start sshd on demand. + services.openssh.enable = lib.mkDefault true; + services.openssh.startWhenNeeded = lib.mkDefault true; + + # As this is intended as a standalone image, undo some of the minimal profile stuff + environment.noXlibs = false; + documentation.enable = true; + documentation.nixos.enable = true; + services.logrotate.enable = true; +} diff --git a/nixos/modules/virtualisation/lxd-virtual-machine.nix b/nixos/modules/virtualisation/lxd-virtual-machine.nix new file mode 100644 index 0000000000000..ba729465ec2f8 --- /dev/null +++ b/nixos/modules/virtualisation/lxd-virtual-machine.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +let + serialDevice = + if pkgs.stdenv.hostPlatform.isx86 + then "ttyS0" + else "ttyAMA0"; # aarch64 +in { + imports = [ + ./lxc-instance-common.nix + + ../profiles/qemu-guest.nix + ]; + + config = { + system.build.qemuImage = import ../../lib/make-disk-image.nix { + inherit pkgs lib config; + + partitionTableType = "efi"; + format = "qcow2-compressed"; + copyChannel = true; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/nixos"; + autoResize = true; + fsType = "ext4"; + }; + "/boot" = { + device = "/dev/disk/by-label/ESP"; + fsType = "vfat"; + }; + }; + + boot.growPartition = true; + boot.loader.systemd-boot.enable = true; + + # image building needs to know what device to install bootloader on + boot.loader.grub.device = "/dev/vda"; + + boot.kernelParams = ["console=tty1" "console=${serialDevice}"]; + + virtualisation.lxd.agent.enable = lib.mkDefault true; + }; +} diff --git a/nixos/modules/virtualisation/lxd.nix b/nixos/modules/virtualisation/lxd.nix index e22ba9a0ae2ca..e30fbebb662c7 100644 --- a/nixos/modules/virtualisation/lxd.nix +++ b/nixos/modules/virtualisation/lxd.nix @@ -2,21 +2,20 @@ { config, lib, pkgs, ... }: -with lib; - let cfg = config.virtualisation.lxd; + preseedFormat = pkgs.formats.yaml {}; in { imports = [ - (mkRemovedOptionModule [ "virtualisation" "lxd" "zfsPackage" ] "Override zfs in an overlay instead to override it globally") + (lib.mkRemovedOptionModule [ "virtualisation" "lxd" "zfsPackage" ] "Override zfs in an overlay instead to override it globally") ]; ###### interface options = { virtualisation.lxd = { - enable = mkOption { - type = types.bool; + enable = lib.mkOption { + type = lib.types.bool; default = false; description = lib.mdDoc '' This option enables lxd, a daemon that manages @@ -32,28 +31,28 @@ in { ''; }; - package = mkOption { - type = types.package; + package = lib.mkOption { + type = lib.types.package; default = pkgs.lxd; - defaultText = literalExpression "pkgs.lxd"; + defaultText = lib.literalExpression "pkgs.lxd"; description = lib.mdDoc '' The LXD package to use. ''; }; - lxcPackage = mkOption { - type = types.package; + lxcPackage = lib.mkOption { + type = lib.types.package; default = pkgs.lxc; - defaultText = literalExpression "pkgs.lxc"; + defaultText = lib.literalExpression "pkgs.lxc"; description = lib.mdDoc '' The LXC package to use with LXD (required for AppArmor profiles). ''; }; - zfsSupport = mkOption { - type = types.bool; + zfsSupport = lib.mkOption { + type = lib.types.bool; default = config.boot.zfs.enabled; - defaultText = literalExpression "config.boot.zfs.enabled"; + defaultText = lib.literalExpression "config.boot.zfs.enabled"; description = lib.mdDoc '' Enables lxd to use zfs as a storage for containers. @@ -62,8 +61,8 @@ in { ''; }; - recommendedSysctlSettings = mkOption { - type = types.bool; + recommendedSysctlSettings = lib.mkOption { + type = lib.types.bool; default = false; description = lib.mdDoc '' Enables various settings to avoid common pitfalls when @@ -75,8 +74,67 @@ in { ''; }; - startTimeout = mkOption { - type = types.int; + preseed = lib.mkOption { + type = lib.types.nullOr (lib.types.submodule { + freeformType = preseedFormat.type; + }); + + default = null; + + description = lib.mdDoc '' + Configuration for LXD preseed, see + <https://documentation.ubuntu.com/lxd/en/latest/howto/initialize/#initialize-preseed> + for supported values. + + Changes to this will be re-applied to LXD which will overwrite existing entities or create missing ones, + but entities will *not* be removed by preseed. + ''; + + example = lib.literalExpression '' + { + networks = [ + { + name = "lxdbr0"; + type = "bridge"; + config = { + "ipv4.address" = "10.0.100.1/24"; + "ipv4.nat" = "true"; + }; + } + ]; + profiles = [ + { + name = "default"; + devices = { + eth0 = { + name = "eth0"; + network = "lxdbr0"; + type = "nic"; + }; + root = { + path = "/"; + pool = "default"; + size = "35GiB"; + type = "disk"; + }; + }; + } + ]; + storage_pools = [ + { + name = "default"; + driver = "dir"; + config = { + source = "/var/lib/lxd/storage-pools/default"; + }; + } + ]; + } + ''; + }; + + startTimeout = lib.mkOption { + type = lib.types.int; default = 600; apply = toString; description = lib.mdDoc '' @@ -91,13 +149,13 @@ in { Enables the (experimental) LXD UI. ''); - package = mkPackageOption pkgs.lxd-unwrapped "ui" { }; + package = lib.mkPackageOption pkgs.lxd-unwrapped "ui" { }; }; }; }; ###### implementation - config = mkIf cfg.enable { + config = lib.mkIf cfg.enable { environment.systemPackages = [ cfg.package ]; # Note: the following options are also declared in virtualisation.lxc, but @@ -139,19 +197,19 @@ in { wantedBy = [ "multi-user.target" ]; after = [ "network-online.target" - (mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service") + (lib.mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service") ]; requires = [ "network-online.target" "lxd.socket" - (mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service") + (lib.mkIf config.virtualisation.lxc.lxcfs.enable "lxcfs.service") ]; documentation = [ "man:lxd(1)" ]; path = [ pkgs.util-linux ] - ++ optional cfg.zfsSupport config.boot.zfs.package; + ++ lib.optional cfg.zfsSupport config.boot.zfs.package; - environment = mkIf (cfg.ui.enable) { + environment = lib.mkIf (cfg.ui.enable) { "LXD_UI" = cfg.ui.package; }; @@ -173,11 +231,26 @@ in { # By default, `lxd` loads configuration files from hard-coded # `/usr/share/lxc/config` - since this is a no-go for us, we have to # explicitly tell it where the actual configuration files are - Environment = mkIf (config.virtualisation.lxc.lxcfs.enable) + Environment = lib.mkIf (config.virtualisation.lxc.lxcfs.enable) "LXD_LXC_TEMPLATE_CONFIG=${pkgs.lxcfs}/share/lxc/config"; }; }; + systemd.services.lxd-preseed = lib.mkIf (cfg.preseed != null) { + description = "LXD initialization with preseed file"; + wantedBy = ["multi-user.target"]; + requires = ["lxd.service"]; + after = ["lxd.service"]; + + script = '' + ${pkgs.coreutils}/bin/cat ${preseedFormat.generate "lxd-preseed.yaml" cfg.preseed} | ${cfg.package}/bin/lxd init --preseed + ''; + + serviceConfig = { + Type = "oneshot"; + }; + }; + users.groups.lxd = {}; users.users.root = { @@ -185,7 +258,7 @@ in { subGidRanges = [ { startGid = 1000000; count = 65536; } ]; }; - boot.kernel.sysctl = mkIf cfg.recommendedSysctlSettings { + boot.kernel.sysctl = lib.mkIf cfg.recommendedSysctlSettings { "fs.inotify.max_queued_events" = 1048576; "fs.inotify.max_user_instances" = 1048576; "fs.inotify.max_user_watches" = 1048576; @@ -196,7 +269,7 @@ in { "kernel.keys.maxkeys" = 2000; }; - boot.kernelModules = [ "veth" "xt_comment" "xt_CHECKSUM" "xt_MASQUERADE" ] - ++ optionals (!config.networking.nftables.enable) [ "iptable_mangle" ]; + boot.kernelModules = [ "veth" "xt_comment" "xt_CHECKSUM" "xt_MASQUERADE" "vhost_vsock" ] + ++ lib.optionals (!config.networking.nftables.enable) [ "iptable_mangle" ]; }; } |