diff options
Diffstat (limited to 'nixos/modules/services')
64 files changed, 1447 insertions, 453 deletions
diff --git a/nixos/modules/services/audio/snapserver.nix b/nixos/modules/services/audio/snapserver.nix index 2af42eeb3705b..dbab741bf6fc7 100644 --- a/nixos/modules/services/audio/snapserver.nix +++ b/nixos/modules/services/audio/snapserver.nix @@ -275,9 +275,9 @@ in { warnings = # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85 - filter (w: w != "") (mapAttrsToList (k: v: if v.type == "spotify" then '' + filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") '' services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead. - '' else "") cfg.streams); + '') cfg.streams); systemd.services.snapserver = { after = [ "network.target" ]; diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix index e7cd6ae4bb573..5ee036e68c7bc 100644 --- a/nixos/modules/services/backup/borgmatic.nix +++ b/nixos/modules/services/backup/borgmatic.nix @@ -72,5 +72,8 @@ in cfg.configurations; systemd.packages = [ pkgs.borgmatic ]; + + # Workaround: https://github.com/NixOS/nixpkgs/issues/81138 + systemd.timers.borgmatic.wantedBy = [ "timers.target" ]; }; } diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix index 289291c6bd2f9..9fbc599cd41af 100644 --- a/nixos/modules/services/backup/mysql-backup.nix +++ b/nixos/modules/services/backup/mysql-backup.nix @@ -20,7 +20,7 @@ let ''; backupDatabaseScript = db: '' dest="${cfg.location}/${db}.gz" - if ${mariadb}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then + if ${mariadb}/bin/mysqldump ${optionalString cfg.singleTransaction "--single-transaction"} ${db} | ${gzip}/bin/gzip -c > $dest.tmp; then mv $dest.tmp $dest echo "Backed up to $dest" else diff --git a/nixos/modules/services/backup/restic.nix b/nixos/modules/services/backup/restic.nix index ca796cf7797e6..d19b98a3e4bbb 100644 --- a/nixos/modules/services/backup/restic.nix +++ b/nixos/modules/services/backup/restic.nix @@ -300,7 +300,7 @@ in filesFromTmpFile = "/run/restic-backups-${name}/includes"; backupPaths = if (backup.dynamicFilesFrom == null) - then if (backup.paths != null) then concatStringsSep " " backup.paths else "" + then optionalString (backup.paths != null) (concatStringsSep " " backup.paths) else "--files-from ${filesFromTmpFile}"; pruneCmd = optionals (builtins.length backup.pruneOpts > 0) [ (resticCmd + " forget --prune " + (concatStringsSep " " backup.pruneOpts)) diff --git a/nixos/modules/services/blockchain/ethereum/geth.nix b/nixos/modules/services/blockchain/ethereum/geth.nix index eca308dc366d1..d12516ca2f249 100644 --- a/nixos/modules/services/blockchain/ethereum/geth.nix +++ b/nixos/modules/services/blockchain/ethereum/geth.nix @@ -196,9 +196,9 @@ in --gcmode ${cfg.gcmode} \ --port ${toString cfg.port} \ --maxpeers ${toString cfg.maxpeers} \ - ${if cfg.http.enable then ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}'' else ""} \ + ${optionalString cfg.http.enable ''--http --http.addr ${cfg.http.address} --http.port ${toString cfg.http.port}''} \ ${optionalString (cfg.http.apis != null) ''--http.api ${lib.concatStringsSep "," cfg.http.apis}''} \ - ${if cfg.websocket.enable then ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}'' else ""} \ + ${optionalString cfg.websocket.enable ''--ws --ws.addr ${cfg.websocket.address} --ws.port ${toString cfg.websocket.port}''} \ ${optionalString (cfg.websocket.apis != null) ''--ws.api ${lib.concatStringsSep "," cfg.websocket.apis}''} \ ${optionalString cfg.metrics.enable ''--metrics --metrics.addr ${cfg.metrics.address} --metrics.port ${toString cfg.metrics.port}''} \ --authrpc.addr ${cfg.authrpc.address} --authrpc.port ${toString cfg.authrpc.port} --authrpc.vhosts ${lib.concatStringsSep "," cfg.authrpc.vhosts} \ diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix index 5fb715f4d779a..1879fef9666f7 100644 --- a/nixos/modules/services/computing/boinc/client.nix +++ b/nixos/modules/services/computing/boinc/client.nix @@ -6,7 +6,7 @@ let cfg = config.services.boinc; allowRemoteGuiRpcFlag = optionalString cfg.allowRemoteGuiRpc "--allow_remote_gui_rpc"; - fhsEnv = pkgs.buildFHSUserEnv { + fhsEnv = pkgs.buildFHSEnv { name = "boinc-fhs-env"; targetPkgs = pkgs': [ cfg.package ] ++ cfg.extraEnvPackages; runScript = "/bin/boinc_client"; diff --git a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix index 3a1c6c1a371df..d6a8c2a3f7cce 100644 --- a/nixos/modules/services/continuous-integration/jenkins/job-builder.nix +++ b/nixos/modules/services/continuous-integration/jenkins/job-builder.nix @@ -242,7 +242,7 @@ in { jobdir="${jenkinsCfg.home}/$jenkinsjobname" rm -rf "$jobdir" done - '' + (if cfg.accessUser != "" then reloadScript else ""); + '' + (optionalString (cfg.accessUser != "") reloadScript); serviceConfig = { Type = "oneshot"; User = jenkinsCfg.user; diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix index 8c64e3d9a5605..74f56f5890fce 100644 --- a/nixos/modules/services/development/lorri.nix +++ b/nixos/modules/services/development/lorri.nix @@ -50,6 +50,6 @@ in { }; }; - environment.systemPackages = [ cfg.package ]; + environment.systemPackages = [ cfg.package pkgs.direnv ]; }; } diff --git a/nixos/modules/services/games/minetest-server.nix b/nixos/modules/services/games/minetest-server.nix index e8c96881673b5..578364ec542bb 100644 --- a/nixos/modules/services/games/minetest-server.nix +++ b/nixos/modules/services/games/minetest-server.nix @@ -4,7 +4,7 @@ with lib; let cfg = config.services.minetest-server; - flag = val: name: if val != null then "--${name} ${toString val} " else ""; + flag = val: name: optionalString (val != null) "--${name} ${toString val} "; flags = [ (flag cfg.gameId "gameid") (flag cfg.world "world") diff --git a/nixos/modules/services/hardware/udev.nix b/nixos/modules/services/hardware/udev.nix index d95261332419d..95c2a4fc5c3e1 100644 --- a/nixos/modules/services/hardware/udev.nix +++ b/nixos/modules/services/hardware/udev.nix @@ -16,16 +16,6 @@ let ''; - # networkd link files are used early by udev to set up interfaces early. - # This must be done in stage 1 to avoid race conditions between udev and - # network daemons. - # TODO move this into the initrd-network module when it exists - initrdLinkUnits = pkgs.runCommand "initrd-link-units" {} '' - mkdir -p $out - ln -s ${udev}/lib/systemd/network/*.link $out/ - ${lib.concatMapStringsSep "\n" (file: "ln -s ${file} $out/") (lib.mapAttrsToList (n: v: "${v.unit}/${n}") (lib.filterAttrs (n: _: hasSuffix ".link" n) config.systemd.network.units))} - ''; - extraUdevRules = pkgs.writeTextFile { name = "extra-udev-rules"; text = cfg.extraRules; @@ -398,7 +388,6 @@ in systemd = config.boot.initrd.systemd.package; binPackages = config.boot.initrd.services.udev.binPackages ++ [ config.boot.initrd.systemd.contents."/bin".source ]; }; - "/etc/systemd/network".source = initrdLinkUnits; }; # Insert initrd rules boot.initrd.services.udev.packages = [ diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix new file mode 100644 index 0000000000000..d7dbb6f0b90e3 --- /dev/null +++ b/nixos/modules/services/home-automation/esphome.nix @@ -0,0 +1,136 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) + literalExpression + maintainers + mkEnableOption + mkIf + mkOption + mdDoc + types + ; + + cfg = config.services.esphome; + + stateDir = "/var/lib/esphome"; + + esphomeParams = + if cfg.enableUnixSocket + then "--socket /run/esphome/esphome.sock" + else "--address ${cfg.address} --port ${toString cfg.port}"; +in +{ + meta.maintainers = with maintainers; [ oddlama ]; + + options.services.esphome = { + enable = mkEnableOption (mdDoc "esphome"); + + package = mkOption { + type = types.package; + default = pkgs.esphome; + defaultText = literalExpression "pkgs.esphome"; + description = mdDoc "The package to use for the esphome command."; + }; + + enableUnixSocket = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Listen on a unix socket `/run/esphome/esphome.sock` instead of the TCP port."; + }; + + address = mkOption { + type = types.str; + default = "localhost"; + description = mdDoc "esphome address"; + }; + + port = mkOption { + type = types.port; + default = 6052; + description = mdDoc "esphome port"; + }; + + openFirewall = mkOption { + default = false; + type = types.bool; + description = mdDoc "Whether to open the firewall for the specified port."; + }; + + allowedDevices = mkOption { + default = ["char-ttyS" "char-ttyUSB"]; + example = ["/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"]; + description = lib.mdDoc '' + A list of device nodes to which {command}`esphome` has access to. + Refer to DeviceAllow in systemd.resource-control(5) for more information. + Beware that if a device is referred to by an absolute path instead of a device category, + it will only allow devices that already are plugged in when the service is started. + ''; + type = types.listOf types.str; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port]; + + systemd.services.esphome = { + description = "ESPHome dashboard"; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + path = [cfg.package]; + + # platformio fails to determine the home directory when using DynamicUser + environment.PLATFORMIO_CORE_DIR = "${stateDir}/.platformio"; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/esphome dashboard ${esphomeParams} ${stateDir}"; + DynamicUser = true; + User = "esphome"; + Group = "esphome"; + WorkingDirectory = stateDir; + StateDirectory = "esphome"; + StateDirectoryMode = "0750"; + Restart = "on-failure"; + RuntimeDirectory = mkIf cfg.enableUnixSocket "esphome"; + RuntimeDirectoryMode = "0750"; + + # Hardening + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + DevicePolicy = "closed"; + DeviceAllow = map (d: "${d} rw") cfg.allowedDevices; + SupplementaryGroups = ["dialout"]; + #NoNewPrivileges = true; # Implied by DynamicUser + PrivateUsers = true; + #PrivateTmp = true; # Implied by DynamicUser + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + ProtectSystem = "strict"; + #RemoveIPC = true; # Implied by DynamicUser + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + RestrictNamespaces = false; # Required by platformio for chroot + RestrictRealtime = true; + #RestrictSUIDSGID = true; # Implied by DynamicUser + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "@mount" # Required by platformio for chroot + ]; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixos/modules/services/logging/logrotate.nix b/nixos/modules/services/logging/logrotate.nix index b056f96c3630b..342ac5ec6e049 100644 --- a/nixos/modules/services/logging/logrotate.nix +++ b/nixos/modules/services/logging/logrotate.nix @@ -83,9 +83,8 @@ let }; mailOption = - if foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings) - then "--mail=${pkgs.mailutils}/bin/mail" - else ""; + optionalString (foldr (n: a: a || (n.mail or false) != false) false (attrValues cfg.settings)) + "--mail=${pkgs.mailutils}/bin/mail"; in { imports = [ diff --git a/nixos/modules/services/logging/syslogd.nix b/nixos/modules/services/logging/syslogd.nix index 43969402588db..553973e255f7e 100644 --- a/nixos/modules/services/logging/syslogd.nix +++ b/nixos/modules/services/logging/syslogd.nix @@ -7,7 +7,7 @@ let cfg = config.services.syslogd; syslogConf = pkgs.writeText "syslog.conf" '' - ${if (cfg.tty != "") then "kern.warning;*.err;authpriv.none /dev/${cfg.tty}" else ""} + ${optionalString (cfg.tty != "") "kern.warning;*.err;authpriv.none /dev/${cfg.tty}"} ${cfg.defaultConfig} ${cfg.extraConfig} ''; diff --git a/nixos/modules/services/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix index 5f3a9b56292d2..d0b525bcb0027 100644 --- a/nixos/modules/services/mail/maddy.nix +++ b/nixos/modules/services/mail/maddy.nix @@ -228,8 +228,8 @@ in { default = []; description = lib.mdDoc '' List of IMAP accounts which get automatically created. Note that for - a complete setup, user credentials for these accounts are required too - and can be created using the command `maddyctl creds`. + a complete setup, user credentials for these accounts are required + and can be created using the `ensureCredentials` option. This option does not delete accounts which are not (anymore) listed. ''; example = [ @@ -238,6 +238,33 @@ in { ]; }; + ensureCredentials = mkOption { + default = {}; + description = lib.mdDoc '' + List of user accounts which get automatically created if they don't + exist yet. Note that for a complete setup, corresponding mail boxes + have to get created using the `ensureAccounts` option. + This option does not delete accounts which are not (anymore) listed. + ''; + example = { + "user1@localhost".passwordFile = /secrets/user1-localhost; + "user2@localhost".passwordFile = /secrets/user2-localhost; + }; + type = types.attrsOf (types.submodule { + options = { + passwordFile = mkOption { + type = types.path; + example = "/path/to/file"; + default = null; + description = lib.mdDoc '' + Specifies the path to a file containing the + clear text password for the user. + ''; + }; + }; + }); + }; + }; }; @@ -265,6 +292,13 @@ in { fi '') cfg.ensureAccounts} ''} + ${optionalString (cfg.ensureCredentials != {}) '' + ${concatStringsSep "\n" (mapAttrsToList (name: cfg: '' + if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then + ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${escapeShellArg cfg.passwordFile}) ${name} + fi + '') cfg.ensureCredentials)} + ''} ''; serviceConfig = { Type = "oneshot"; diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix index 852340c05aa7a..23c47aaca7e23 100644 --- a/nixos/modules/services/mail/postfix.nix +++ b/nixos/modules/services/mail/postfix.nix @@ -234,7 +234,7 @@ let headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks; - aliases = let separator = if cfg.aliasMapType == "hash" then ":" else ""; in + aliases = let separator = optionalString (cfg.aliasMapType == "hash") ":"; in optionalString (cfg.postmasterAlias != "") '' postmaster${separator} ${cfg.postmasterAlias} '' diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix index 7b6d82219298c..3aaec145930db 100644 --- a/nixos/modules/services/mail/roundcube.nix +++ b/nixos/modules/services/mail/roundcube.nix @@ -7,7 +7,7 @@ let fpm = config.services.phpfpm.pools.roundcube; localDB = cfg.database.host == "localhost"; user = cfg.database.username; - phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled); + phpWithPspell = pkgs.php81.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled); in { options.services.roundcube = { diff --git a/nixos/modules/services/misc/gammu-smsd.nix b/nixos/modules/services/misc/gammu-smsd.nix index 83f4efe695a27..eff725f5a8685 100644 --- a/nixos/modules/services/misc/gammu-smsd.nix +++ b/nixos/modules/services/misc/gammu-smsd.nix @@ -10,7 +10,7 @@ let Connection = ${cfg.device.connection} SynchronizeTime = ${if cfg.device.synchronizeTime then "yes" else "no"} LogFormat = ${cfg.log.format} - ${if (cfg.device.pin != null) then "PIN = ${cfg.device.pin}" else ""} + ${optionalString (cfg.device.pin != null) "PIN = ${cfg.device.pin}"} ${cfg.extraConfig.gammu} @@ -33,10 +33,10 @@ let ${optionalString (cfg.backend.service == "sql" && cfg.backend.sql.driver == "native_pgsql") ( with cfg.backend; '' Driver = ${sql.driver} - ${if (sql.database!= null) then "Database = ${sql.database}" else ""} - ${if (sql.host != null) then "Host = ${sql.host}" else ""} - ${if (sql.user != null) then "User = ${sql.user}" else ""} - ${if (sql.password != null) then "Password = ${sql.password}" else ""} + ${optionalString (sql.database!= null) "Database = ${sql.database}"} + ${optionalString (sql.host != null) "Host = ${sql.host}"} + ${optionalString (sql.user != null) "User = ${sql.user}"} + ${optionalString (sql.password != null) "Password = ${sql.password}"} '')} ${cfg.extraConfig.smsd} diff --git a/nixos/modules/services/misc/gitea.nix b/nixos/modules/services/misc/gitea.nix index e019e431a1890..a5d7a73dd06aa 100644 --- a/nixos/modules/services/misc/gitea.nix +++ b/nixos/modules/services/misc/gitea.nix @@ -26,9 +26,18 @@ in imports = [ (mkRenamedOptionModule [ "services" "gitea" "cookieSecure" ] [ "services" "gitea" "settings" "session" "COOKIE_SECURE" ]) (mkRenamedOptionModule [ "services" "gitea" "disableRegistration" ] [ "services" "gitea" "settings" "service" "DISABLE_REGISTRATION" ]) + (mkRenamedOptionModule [ "services" "gitea" "domain" ] [ "services" "gitea" "settings" "server" "DOMAIN" ]) + (mkRenamedOptionModule [ "services" "gitea" "httpAddress" ] [ "services" "gitea" "settings" "server" "HTTP_ADDR" ]) + (mkRenamedOptionModule [ "services" "gitea" "httpPort" ] [ "services" "gitea" "settings" "server" "HTTP_PORT" ]) (mkRenamedOptionModule [ "services" "gitea" "log" "level" ] [ "services" "gitea" "settings" "log" "LEVEL" ]) (mkRenamedOptionModule [ "services" "gitea" "log" "rootPath" ] [ "services" "gitea" "settings" "log" "ROOT_PATH" ]) + (mkRenamedOptionModule [ "services" "gitea" "rootUrl" ] [ "services" "gitea" "settings" "server" "ROOT_URL" ]) (mkRenamedOptionModule [ "services" "gitea" "ssh" "clonePort" ] [ "services" "gitea" "settings" "server" "SSH_PORT" ]) + (mkRenamedOptionModule [ "services" "gitea" "staticRootPath" ] [ "services" "gitea" "settings" "server" "STATIC_ROOT_PATH" ]) + + (mkChangedOptionModule [ "services" "gitea" "enableUnixSocket" ] [ "services" "gitea" "settings" "server" "PROTOCOL" ] ( + config: if config.services.gitea.enableUnixSocket then "http+unix" else "http" + )) (mkRemovedOptionModule [ "services" "gitea" "ssh" "enable" ] "services.gitea.ssh.enable has been migrated into freeform setting services.gitea.settings.server.DISABLE_SSH. Keep in mind that the setting is inverted") ]; @@ -57,7 +66,14 @@ in stateDir = mkOption { default = "/var/lib/gitea"; type = types.str; - description = lib.mdDoc "gitea data directory."; + description = lib.mdDoc "Gitea data directory."; + }; + + customDir = mkOption { + default = "${cfg.stateDir}/custom"; + defaultText = literalExpression ''"''${config.${opt.stateDir}}/custom"''; + type = types.str; + description = lib.mdDoc "Gitea custom directory. Used for config, custom templates and other options."; }; user = mkOption { @@ -66,6 +82,12 @@ in description = lib.mdDoc "User account under which gitea runs."; }; + group = mkOption { + type = types.str; + default = "gitea"; + description = lib.mdDoc "Group under which gitea runs."; + }; + database = { type = mkOption { type = types.enum [ "sqlite3" "mysql" "postgres" ]; @@ -216,44 +238,6 @@ in description = lib.mdDoc "Path to the git repositories."; }; - domain = mkOption { - type = types.str; - default = "localhost"; - description = lib.mdDoc "Domain name of your server."; - }; - - rootUrl = mkOption { - type = types.str; - default = "http://localhost:3000/"; - description = lib.mdDoc "Full public URL of gitea server."; - }; - - httpAddress = mkOption { - type = types.str; - default = "0.0.0.0"; - description = lib.mdDoc "HTTP listen address."; - }; - - httpPort = mkOption { - type = types.port; - default = 3000; - description = lib.mdDoc "HTTP listen port."; - }; - - enableUnixSocket = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc "Configure Gitea to listen on a unix socket instead of the default TCP port."; - }; - - staticRootPath = mkOption { - type = types.either types.str types.path; - default = cfg.package.data; - defaultText = literalExpression "package.data"; - example = "/var/lib/gitea/data"; - description = lib.mdDoc "Upper level of template and static files path."; - }; - mailerPasswordFile = mkOption { type = types.nullOr types.str; default = null; @@ -285,7 +269,7 @@ in }; } ''; - type = with types; submodule { + type = types.submodule { freeformType = format.type; options = { log = { @@ -303,6 +287,46 @@ in }; server = { + PROTOCOL = mkOption { + type = types.enum [ "http" "https" "fcgi" "http+unix" "fcgi+unix" ]; + default = "http"; + description = lib.mdDoc ''Listen protocol. `+unix` means "over unix", not "in addition to."''; + }; + + HTTP_ADDR = mkOption { + type = types.either types.str types.path; + default = if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"; + defaultText = literalExpression ''if lib.hasSuffix "+unix" cfg.settings.server.PROTOCOL then "/run/gitea/gitea.sock" else "0.0.0.0"''; + description = lib.mdDoc "Listen address. Must be a path when using a unix socket."; + }; + + HTTP_PORT = mkOption { + type = types.port; + default = 3000; + description = lib.mdDoc "Listen port. Ignored when using a unix socket."; + }; + + DOMAIN = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Domain name of your server."; + }; + + ROOT_URL = mkOption { + type = types.str; + default = "http://${cfg.settings.server.DOMAIN}:${toString cfg.settings.server.HTTP_PORT}/"; + defaultText = literalExpression ''"http://''${config.services.gitea.settings.server.DOMAIN}:''${toString config.services.gitea.settings.server.HTTP_PORT}/"''; + description = lib.mdDoc "Full public URL of gitea server."; + }; + + STATIC_ROOT_PATH = mkOption { + type = types.either types.str types.path; + default = cfg.package.data; + defaultText = literalExpression "config.${opt.package}.data"; + example = "/var/lib/gitea/data"; + description = lib.mdDoc "Upper level of template and static files path."; + }; + DISABLE_SSH = mkOption { type = types.bool; default = false; @@ -359,7 +383,7 @@ in config = mkIf cfg.enable { assertions = [ - { assertion = cfg.database.createDatabase -> cfg.database.user == cfg.user; + { assertion = cfg.database.createDatabase -> useSqlite || cfg.database.user == cfg.user; message = "services.gitea.database.user must match services.gitea.user if the database is to be automatically provisioned"; } ]; @@ -389,26 +413,10 @@ in ROOT = cfg.repositoryRoot; }; - server = mkMerge [ - { - DOMAIN = cfg.domain; - STATIC_ROOT_PATH = toString cfg.staticRootPath; - LFS_JWT_SECRET = "#lfsjwtsecret#"; - ROOT_URL = cfg.rootUrl; - } - (mkIf cfg.enableUnixSocket { - PROTOCOL = "http+unix"; - HTTP_ADDR = "/run/gitea/gitea.sock"; - }) - (mkIf (!cfg.enableUnixSocket) { - HTTP_ADDR = cfg.httpAddress; - HTTP_PORT = cfg.httpPort; - }) - (mkIf cfg.lfs.enable { - LFS_START_SERVER = true; - }) - - ]; + server = mkIf cfg.lfs.enable { + LFS_START_SERVER = true; + LFS_JWT_SECRET = "#lfsjwtsecret#"; + }; session = { COOKIE_NAME = lib.mkDefault "session"; @@ -428,7 +436,7 @@ in JWT_SECRET = "#oauth2jwtsecret#"; }; - lfs = mkIf (cfg.lfs.enable) { + lfs = mkIf cfg.lfs.enable { PATH = cfg.lfs.contentDir; }; }; @@ -457,33 +465,35 @@ in }; systemd.tmpfiles.rules = [ - "d '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -" - "z '${cfg.dump.backupDir}' 0750 ${cfg.user} gitea - -" - "Z '${cfg.dump.backupDir}' - ${cfg.user} gitea - -" - "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -" - "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} gitea - -" - "Z '${cfg.lfs.contentDir}' - ${cfg.user} gitea - -" - "d '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -" - "z '${cfg.repositoryRoot}' 0750 ${cfg.user} gitea - -" - "Z '${cfg.repositoryRoot}' - ${cfg.user} gitea - -" - "d '${cfg.stateDir}' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/data' 0750 ${cfg.user} gitea - -" - "d '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/conf' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/custom' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/custom/conf' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/data' 0750 ${cfg.user} gitea - -" - "z '${cfg.stateDir}/log' 0750 ${cfg.user} gitea - -" - "Z '${cfg.stateDir}' - ${cfg.user} gitea - -" + "d '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.dump.backupDir}' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.dump.backupDir}' - ${cfg.user} ${cfg.group} - -" + "d '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.repositoryRoot}' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.repositoryRoot}' - ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" + "d '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/.ssh' 0700 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.customDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.customDir}/conf' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/data' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.stateDir}/log' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.stateDir}' - ${cfg.user} ${cfg.group} - -" # If we have a folder or symlink with gitea locales, remove it # And symlink the current gitea locales in place "L+ '${cfg.stateDir}/conf/locale' - - - - ${cfg.package.out}/locale" + + ] ++ lib.optionals cfg.lfs.enable [ + "d '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" + "z '${cfg.lfs.contentDir}' 0750 ${cfg.user} ${cfg.group} - -" + "Z '${cfg.lfs.contentDir}' - ${cfg.user} ${cfg.group} - -" ]; systemd.services.gitea = { @@ -500,47 +510,52 @@ in # lfs_jwt_secret. # We have to consider this to stay compatible with older installations. preStart = let - runConfig = "${cfg.stateDir}/custom/conf/app.ini"; - secretKey = "${cfg.stateDir}/custom/conf/secret_key"; - oauth2JwtSecret = "${cfg.stateDir}/custom/conf/oauth2_jwt_secret"; - oldLfsJwtSecret = "${cfg.stateDir}/custom/conf/jwt_secret"; # old file for LFS_JWT_SECRET - lfsJwtSecret = "${cfg.stateDir}/custom/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET - internalToken = "${cfg.stateDir}/custom/conf/internal_token"; + runConfig = "${cfg.customDir}/conf/app.ini"; + secretKey = "${cfg.customDir}/conf/secret_key"; + oauth2JwtSecret = "${cfg.customDir}/conf/oauth2_jwt_secret"; + oldLfsJwtSecret = "${cfg.customDir}/conf/jwt_secret"; # old file for LFS_JWT_SECRET + lfsJwtSecret = "${cfg.customDir}/conf/lfs_jwt_secret"; # new file for LFS_JWT_SECRET + internalToken = "${cfg.customDir}/conf/internal_token"; replaceSecretBin = "${pkgs.replace-secret}/bin/replace-secret"; in '' - # copy custom configuration and generate a random secret key if needed + # copy custom configuration and generate random secrets if needed ${optionalString (!cfg.useWizard) '' function gitea_setup { - cp -f ${configFile} ${runConfig} + cp -f '${configFile}' '${runConfig}' - if [ ! -s ${secretKey} ]; then - ${exe} generate secret SECRET_KEY > ${secretKey} + if [ ! -s '${secretKey}' ]; then + ${exe} generate secret SECRET_KEY > '${secretKey}' fi # Migrate LFS_JWT_SECRET filename - if [[ -s ${oldLfsJwtSecret} && ! -s ${lfsJwtSecret} ]]; then - mv ${oldLfsJwtSecret} ${lfsJwtSecret} + if [[ -s '${oldLfsJwtSecret}' && ! -s '${lfsJwtSecret}' ]]; then + mv '${oldLfsJwtSecret}' '${lfsJwtSecret}' fi - if [ ! -s ${oauth2JwtSecret} ]; then - ${exe} generate secret JWT_SECRET > ${oauth2JwtSecret} + if [ ! -s '${oauth2JwtSecret}' ]; then + ${exe} generate secret JWT_SECRET > '${oauth2JwtSecret}' fi - if [ ! -s ${lfsJwtSecret} ]; then - ${exe} generate secret LFS_JWT_SECRET > ${lfsJwtSecret} + ${lib.optionalString cfg.lfs.enable '' + if [ ! -s '${lfsJwtSecret}' ]; then + ${exe} generate secret LFS_JWT_SECRET > '${lfsJwtSecret}' fi + ''} - if [ ! -s ${internalToken} ]; then - ${exe} generate secret INTERNAL_TOKEN > ${internalToken} + if [ ! -s '${internalToken}' ]; then + ${exe} generate secret INTERNAL_TOKEN > '${internalToken}' fi chmod u+w '${runConfig}' ${replaceSecretBin} '#secretkey#' '${secretKey}' '${runConfig}' ${replaceSecretBin} '#dbpass#' '${cfg.database.passwordFile}' '${runConfig}' ${replaceSecretBin} '#oauth2jwtsecret#' '${oauth2JwtSecret}' '${runConfig}' - ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}' ${replaceSecretBin} '#internaltoken#' '${internalToken}' '${runConfig}' + ${lib.optionalString cfg.lfs.enable '' + ${replaceSecretBin} '#lfsjwtsecret#' '${lfsJwtSecret}' '${runConfig}'" + ''} + ${lib.optionalString (cfg.mailerPasswordFile != null) '' ${replaceSecretBin} '#mailerpass#' '${cfg.mailerPasswordFile}' '${runConfig}' ''} @@ -565,7 +580,7 @@ in serviceConfig = { Type = "simple"; User = cfg.user; - Group = "gitea"; + Group = cfg.group; WorkingDirectory = cfg.stateDir; ExecStart = "${exe} web --pid /run/gitea/gitea.pid"; Restart = "always"; @@ -573,7 +588,7 @@ in RuntimeDirectory = "gitea"; RuntimeDirectoryMode = "0755"; # Access write directories - ReadWritePaths = [ cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; + ReadWritePaths = [ cfg.customDir cfg.dump.backupDir cfg.repositoryRoot cfg.stateDir cfg.lfs.contentDir ]; UMask = "0027"; # Capabilities CapabilityBoundingSet = ""; @@ -606,6 +621,7 @@ in USER = cfg.user; HOME = cfg.stateDir; GITEA_WORK_DIR = cfg.stateDir; + GITEA_CUSTOM = cfg.customDir; }; }; @@ -614,12 +630,14 @@ in description = "Gitea Service"; home = cfg.stateDir; useDefaultShell = true; - group = "gitea"; + group = cfg.group; isSystemUser = true; }; }; - users.groups.gitea = {}; + users.groups = mkIf (cfg.group == "gitea") { + gitea = {}; + }; warnings = optional (cfg.database.password != "") "config.services.gitea.database.password will be stored as plaintext in the Nix store. Use database.passwordFile instead." ++ diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix index d278b571a6410..12c67c5f5a1e7 100644 --- a/nixos/modules/services/misc/gitlab.nix +++ b/nixos/modules/services/misc/gitlab.nix @@ -1215,7 +1215,7 @@ in { enableDelete = true; # This must be true, otherwise GitLab won't manage it correctly extraConfig = { auth.token = { - realm = "http${if cfg.https == true then "s" else ""}://${cfg.host}/jwt/auth"; + realm = "http${optionalString (cfg.https == true) "s"}://${cfg.host}/jwt/auth"; service = cfg.registry.serviceName; issuer = cfg.registry.issuer; rootcertbundle = cfg.registry.certFile; diff --git a/nixos/modules/services/misc/mbpfan.nix b/nixos/modules/services/misc/mbpfan.nix index 1a6b54854d1cd..e75c352541438 100644 --- a/nixos/modules/services/misc/mbpfan.nix +++ b/nixos/modules/services/misc/mbpfan.nix @@ -3,7 +3,7 @@ with lib; let cfg = config.services.mbpfan; - verbose = if cfg.verbose then "v" else ""; + verbose = optionalString cfg.verbose "v"; settingsFormat = pkgs.formats.ini {}; settingsFile = settingsFormat.generate "mbpfan.ini" cfg.settings; diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix new file mode 100644 index 0000000000000..78ec356469076 --- /dev/null +++ b/nixos/modules/services/misc/pufferpanel.nix @@ -0,0 +1,176 @@ +{ config, pkgs, lib, ... }: +let + cfg = config.services.pufferpanel; +in +{ + options.services.pufferpanel = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable PufferPanel game management server. + + Note that [PufferPanel templates] and binaries downloaded by PufferPanel + expect [FHS environment]. It is possible to set {option}`package` option + to use PufferPanel wrapper with FHS environment. For example, to use + `Download Game from Steam` and `Download Java` template operations: + ```Nix + { lib, pkgs, ... }: { + services.pufferpanel = { + enable = true; + extraPackages = with pkgs; [ bash curl gawk gnutar gzip ]; + package = pkgs.buildFHSUserEnv { + name = "pufferpanel-fhs"; + runScript = lib.getExe pkgs.pufferpanel; + targetPkgs = pkgs': with pkgs'; [ icu openssl zlib ]; + }; + }; + } + ``` + + [PufferPanel templates]: https://github.com/PufferPanel/templates + [FHS environment]: https://wikipedia.org/wiki/Filesystem_Hierarchy_Standard + ''; + }; + + package = lib.mkPackageOptionMD pkgs "pufferpanel" { }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "podman" ]; + description = lib.mdDoc '' + Additional groups for the systemd service. + ''; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + example = lib.literalExpression "[ pkgs.jre ]"; + description = lib.mdDoc '' + Packages to add to the PATH environment variable. Both the {file}`bin` + and {file}`sbin` subdirectories of each package are added. + ''; + }; + + environment = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = lib.literalExpression '' + { + PUFFER_WEB_HOST = ":8080"; + PUFFER_DAEMON_SFTP_HOST = ":5657"; + PUFFER_DAEMON_CONSOLE_BUFFER = "1000"; + PUFFER_DAEMON_CONSOLE_FORWARD = "true"; + PUFFER_PANEL_REGISTRATIONENABLED = "false"; + } + ''; + description = lib.mdDoc '' + Environment variables to set for the service. Secrets should be + specified using {option}`environmentFile`. + + Refer to the [PufferPanel source code][] for the list of available + configuration options. Variable name is an upper-cased configuration + entry name with underscores instead of dots, prefixed with `PUFFER_`. + For example, `panel.settings.companyName` entry can be set using + {env}`PUFFER_PANEL_SETTINGS_COMPANYNAME`. + + When running with panel enabled (configured with `PUFFER_PANEL_ENABLE` + environment variable), it is recommended disable registration using + `PUFFER_PANEL_REGISTRATIONENABLED` environment variable (registration is + enabled by default). To create the initial administrator user, run + {command}`pufferpanel --workDir /var/lib/pufferpanel user add --admin`. + + Some options override corresponding settings set via web interface (e.g. + `PUFFER_PANEL_REGISTRATIONENABLED`). Those options can be temporarily + toggled or set in settings but do not persist between restarts. + + [PufferPanel source code]: https://github.com/PufferPanel/PufferPanel/blob/master/config/entries.go + ''; + }; + + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = lib.mdDoc '' + File to load environment variables from. Loaded variables override + values set in {option}`environment`. + ''; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.pufferpanel = { + description = "PufferPanel game management server"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + path = cfg.extraPackages; + environment = cfg.environment; + + # Note that we export environment variables for service directories if the + # value is not set. An empty environment variable is considered to be set. + # E.g. + # export PUFFER_LOGS=${PUFFER_LOGS-$LOGS_DIRECTORY} + # would set PUFFER_LOGS to $LOGS_DIRECTORY if PUFFER_LOGS environment + # variable is not defined. + script = '' + ${lib.concatLines (lib.mapAttrsToList (name: value: '' + export ${name}="''${${name}-${value}}" + '') { + PUFFER_LOGS = "$LOGS_DIRECTORY"; + PUFFER_DAEMON_DATA_CACHE = "$CACHE_DIRECTORY"; + PUFFER_DAEMON_DATA_SERVERS = "$STATE_DIRECTORY/servers"; + PUFFER_DAEMON_DATA_BINARIES = "$STATE_DIRECTORY/binaries"; + })} + exec ${lib.getExe cfg.package} run --workDir "$STATE_DIRECTORY" + ''; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + + UMask = "0077"; + + SupplementaryGroups = cfg.extraGroups; + + StateDirectory = "pufferpanel"; + StateDirectoryMode = "0700"; + CacheDirectory = "pufferpanel"; + CacheDirectoryMode = "0700"; + LogsDirectory = "pufferpanel"; + LogsDirectoryMode = "0700"; + + EnvironmentFile = cfg.environmentFile; + + # Command "pufferpanel shutdown --pid $MAINPID" sends SIGTERM (code 15) + # to the main process and waits for termination. This is essentially + # KillMode=mixed we are using here. See + # https://freedesktop.org/software/systemd/man/systemd.kill.html#KillMode= + KillMode = "mixed"; + + DynamicUser = true; + ProtectHome = true; + ProtectProc = "invisible"; + ProtectClock = true; + ProtectHostname = true; + ProtectControlGroups = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + PrivateUsers = true; + PrivateDevices = true; + RestrictRealtime = true; + RestrictNamespaces = [ "user" "mnt" ]; # allow buildFHSUserEnv + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + LockPersonality = true; + DeviceAllow = [ "" ]; + DevicePolicy = "closed"; + CapabilityBoundingSet = [ "" ]; + }; + }; + }; + + meta.maintainers = [ lib.maintainers.tie ]; +} diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix index 58a595b5c76f5..d881ea913695c 100644 --- a/nixos/modules/services/misc/redmine.nix +++ b/nixos/modules/services/misc/redmine.nix @@ -283,13 +283,13 @@ in services.redmine.settings = { production = { - scm_subversion_command = if cfg.components.subversion then "${pkgs.subversion}/bin/svn" else ""; - scm_mercurial_command = if cfg.components.mercurial then "${pkgs.mercurial}/bin/hg" else ""; - scm_git_command = if cfg.components.git then "${pkgs.git}/bin/git" else ""; - scm_cvs_command = if cfg.components.cvs then "${pkgs.cvs}/bin/cvs" else ""; - scm_bazaar_command = if cfg.components.breezy then "${pkgs.breezy}/bin/bzr" else ""; - imagemagick_convert_command = if cfg.components.imagemagick then "${pkgs.imagemagick}/bin/convert" else ""; - gs_command = if cfg.components.ghostscript then "${pkgs.ghostscript}/bin/gs" else ""; + scm_subversion_command = optionalString cfg.components.subversion "${pkgs.subversion}/bin/svn"; + scm_mercurial_command = optionalString cfg.components.mercurial "${pkgs.mercurial}/bin/hg"; + scm_git_command = optionalString cfg.components.git "${pkgs.git}/bin/git"; + scm_cvs_command = optionalString cfg.components.cvs "${pkgs.cvs}/bin/cvs"; + scm_bazaar_command = optionalString cfg.components.breezy "${pkgs.breezy}/bin/bzr"; + imagemagick_convert_command = optionalString cfg.components.imagemagick "${pkgs.imagemagick}/bin/convert"; + gs_command = optionalString cfg.components.ghostscript "${pkgs.ghostscript}/bin/gs"; minimagick_font_path = "${cfg.components.minimagick_font_path}"; }; }; diff --git a/nixos/modules/services/misc/siproxd.nix b/nixos/modules/services/misc/siproxd.nix index f1a1ed4d29b38..99b25bdb8e9ed 100644 --- a/nixos/modules/services/misc/siproxd.nix +++ b/nixos/modules/services/misc/siproxd.nix @@ -20,7 +20,7 @@ let ${optionalString (cfg.hostsAllowReg != []) "hosts_allow_reg = ${concatStringsSep "," cfg.hostsAllowReg}"} ${optionalString (cfg.hostsAllowSip != []) "hosts_allow_sip = ${concatStringsSep "," cfg.hostsAllowSip}"} ${optionalString (cfg.hostsDenySip != []) "hosts_deny_sip = ${concatStringsSep "," cfg.hostsDenySip}"} - ${if (cfg.passwordFile != "") then "proxy_auth_pwfile = ${cfg.passwordFile}" else ""} + ${optionalString (cfg.passwordFile != "") "proxy_auth_pwfile = ${cfg.passwordFile}"} ${cfg.extraConfig} ''; diff --git a/nixos/modules/services/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix index 270d888afb781..b7761c34fe51a 100644 --- a/nixos/modules/services/monitoring/grafana-agent.nix +++ b/nixos/modules/services/monitoring/grafana-agent.nix @@ -140,7 +140,7 @@ in # We can't use Environment=HOSTNAME=%H, as it doesn't include the domain part. export HOSTNAME=$(< /proc/sys/kernel/hostname) - exec ${cfg.package}/bin/agent -config.expand-env -config.file ${configFile} + exec ${lib.getExe cfg.package} -config.expand-env -config.file ${configFile} ''; serviceConfig = { Restart = "always"; diff --git a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix index 0c2de683ecf72..f67596f05a3a1 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/collectd.nix @@ -58,10 +58,10 @@ in }; }; serviceOpts = let - collectSettingsArgs = if (cfg.collectdBinary.enable) then '' + collectSettingsArgs = optionalString (cfg.collectdBinary.enable) '' --collectd.listen-address ${cfg.collectdBinary.listenAddress}:${toString cfg.collectdBinary.port} \ --collectd.security-level ${cfg.collectdBinary.securityLevel} \ - '' else ""; + ''; in { serviceConfig = { ExecStart = '' diff --git a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix index f80aeae9c6b7d..50e1321a1e9ce 100644 --- a/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix +++ b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix @@ -4,12 +4,12 @@ with lib; let cfg = config.services.prometheus.exporters.smartctl; - args = concatStrings [ - "--web.listen-address=\"${cfg.listenAddress}:${toString cfg.port}\" " - "--smartctl.path=\"${pkgs.smartmontools}/bin/smartctl\" " - "--smartctl.interval=\"${cfg.maxInterval}\" " - "${concatMapStringsSep " " (device: "--smartctl.device=${device}") cfg.devices}" - ] ++ cfg.extraFlags; + args = lib.escapeShellArgs ([ + "--web.listen-address=${cfg.listenAddress}:${toString cfg.port}" + "--smartctl.path=${pkgs.smartmontools}/bin/smartctl" + "--smartctl.interval=${cfg.maxInterval}" + ] ++ map (device: "--smartctl.device=${device}") cfg.devices + ++ cfg.extraFlags); in { port = 9633; diff --git a/nixos/modules/services/network-filesystems/kubo.nix b/nixos/modules/services/network-filesystems/kubo.nix index 0cb0e126d4c50..2537bb1b8d80f 100644 --- a/nixos/modules/services/network-filesystems/kubo.nix +++ b/nixos/modules/services/network-filesystems/kubo.nix @@ -22,6 +22,18 @@ let configFile = settingsFormat.generate "kubo-config.json" customizedConfig; + # Create a fake repo containing only the file "api". + # $IPFS_PATH will point to this directory instead of the real one. + # For some reason the Kubo CLI tools insist on reading the + # config file when it exists. But the Kubo daemon sets the file + # permissions such that only the ipfs user is allowed to read + # this file. This prevents normal users from talking to the daemon. + # To work around this terrible design, create a fake repo with no + # config file, only an api file and everything should work as expected. + fakeKuboRepo = pkgs.writeTextDir "api" '' + /unix/run/ipfs.sock + ''; + kuboFlags = utils.escapeSystemdExecArgs ( optional cfg.autoMount "--mount" ++ optional cfg.enableGC "--enable-gc" ++ @@ -38,6 +50,22 @@ let splitMulitaddr = addrRaw: lib.tail (lib.splitString "/" addrRaw); + multiaddrsToListenStreams = addrIn: + let + addrs = if builtins.typeOf addrIn == "list" + then addrIn else [ addrIn ]; + unfilteredResult = map multiaddrToListenStream addrs; + in + builtins.filter (addr: addr != null) unfilteredResult; + + multiaddrsToListenDatagrams = addrIn: + let + addrs = if builtins.typeOf addrIn == "list" + then addrIn else [ addrIn ]; + unfilteredResult = map multiaddrToListenDatagram addrs; + in + builtins.filter (addr: addr != null) unfilteredResult; + multiaddrToListenStream = addrRaw: let addr = splitMulitaddr addrRaw; @@ -154,13 +182,18 @@ in options = { Addresses.API = mkOption { - type = types.str; - default = "/ip4/127.0.0.1/tcp/5001"; - description = lib.mdDoc "Where Kubo exposes its API to"; + type = types.oneOf [ types.str (types.listOf types.str) ]; + default = [ ]; + description = lib.mdDoc '' + Multiaddr or array of multiaddrs describing the address to serve the local HTTP API on. + In addition to the multiaddrs listed here, the daemon will also listen on a Unix domain socket. + To allow the ipfs CLI tools to communicate with the daemon over that socket, + add your user to the correct group, e.g. `users.users.alice.extraGroups = [ config.services.kubo.group ];` + ''; }; Addresses.Gateway = mkOption { - type = types.str; + type = types.oneOf [ types.str (types.listOf types.str) ]; default = "/ip4/127.0.0.1/tcp/8080"; description = lib.mdDoc "Where the IPFS Gateway can be reached"; }; @@ -248,7 +281,7 @@ in ]; environment.systemPackages = [ cfg.package ]; - environment.variables.IPFS_PATH = cfg.dataDir; + environment.variables.IPFS_PATH = fakeKuboRepo; # https://github.com/lucas-clemente/quic-go/wiki/UDP-Receive-Buffer-Size boot.kernel.sysctl."net.core.rmem_max" = mkDefault 2500000; @@ -319,6 +352,10 @@ in # change when the changes are applied. Whyyyyyy..... ipfs --offline config replace - ''; + postStop = mkIf cfg.autoMount '' + # After an unclean shutdown the fuse mounts at cfg.ipnsMountDir and cfg.ipfsMountDir are locked + umount --quiet '${cfg.ipnsMountDir}' '${cfg.ipfsMountDir}' || true + ''; serviceConfig = { ExecStart = [ "" "${cfg.package}/bin/ipfs daemon ${kuboFlags}" ]; User = cfg.user; @@ -334,27 +371,23 @@ in wantedBy = [ "sockets.target" ]; socketConfig = { ListenStream = - let - fromCfg = multiaddrToListenStream cfg.settings.Addresses.Gateway; - in - [ "" ] ++ lib.optional (fromCfg != null) fromCfg; + [ "" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.Gateway); ListenDatagram = - let - fromCfg = multiaddrToListenDatagram cfg.settings.Addresses.Gateway; - in - [ "" ] ++ lib.optional (fromCfg != null) fromCfg; + [ "" ] ++ (multiaddrsToListenDatagrams cfg.settings.Addresses.Gateway); }; }; systemd.sockets.ipfs-api = { wantedBy = [ "sockets.target" ]; - # We also include "%t/ipfs.sock" because there is no way to put the "%t" - # in the multiaddr. - socketConfig.ListenStream = - let - fromCfg = multiaddrToListenStream cfg.settings.Addresses.API; - in - [ "" "%t/ipfs.sock" ] ++ lib.optional (fromCfg != null) fromCfg; + socketConfig = { + # We also include "%t/ipfs.sock" because there is no way to put the "%t" + # in the multiaddr. + ListenStream = + [ "" "%t/ipfs.sock" ] ++ (multiaddrsToListenStreams cfg.settings.Addresses.API); + SocketMode = "0660"; + SocketUser = cfg.user; + SocketGroup = cfg.group; + }; }; }; diff --git a/nixos/modules/services/network-filesystems/openafs/lib.nix b/nixos/modules/services/network-filesystems/openafs/lib.nix index 80628f4dfaf29..e5e147a8dc338 100644 --- a/nixos/modules/services/network-filesystems/openafs/lib.nix +++ b/nixos/modules/services/network-filesystems/openafs/lib.nix @@ -1,13 +1,13 @@ { config, lib, ...}: let - inherit (lib) concatStringsSep mkOption types; + inherit (lib) concatStringsSep mkOption types optionalString; in { mkCellServDB = cellName: db: '' >${cellName} - '' + (concatStringsSep "\n" (map (dbm: if (dbm.ip != "" && dbm.dnsname != "") then dbm.ip + " #" + dbm.dnsname else "") + '' + (concatStringsSep "\n" (map (dbm: optionalString (dbm.ip != "" && dbm.dnsname != "") "${dbm.ip} #${dbm.dnsname}") db)) + "\n"; diff --git a/nixos/modules/services/networking/iscsi/root-initiator.nix b/nixos/modules/services/networking/iscsi/root-initiator.nix index 4434fedce1eb8..895467cc674ab 100644 --- a/nixos/modules/services/networking/iscsi/root-initiator.nix +++ b/nixos/modules/services/networking/iscsi/root-initiator.nix @@ -185,6 +185,10 @@ in assertion = cfg.loginAll -> cfg.target == null; message = "iSCSI target name is set while login on all portals is enabled."; } + { + assertion = !config.boot.initrd.systemd.enable; + message = "systemd stage 1 does not support iscsi yet."; + } ]; }; } diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix new file mode 100644 index 0000000000000..6df630c1f1947 --- /dev/null +++ b/nixos/modules/services/networking/ivpn.nix @@ -0,0 +1,51 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.ivpn; +in +with lib; +{ + options.services.ivpn = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + This option enables iVPN daemon. + This sets {option}`networking.firewall.checkReversePath` to "loose", which might be undesirable for security. + ''; + }; + }; + + config = mkIf cfg.enable { + boot.kernelModules = [ "tun" ]; + + environment.systemPackages = with pkgs; [ ivpn ivpn-service ]; + + # iVPN writes to /etc/iproute2/rt_tables + networking.iproute2.enable = true; + networking.firewall.checkReversePath = "loose"; + + systemd.services.ivpn-service = { + description = "iVPN daemon"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network.target" ]; + after = [ + "network-online.target" + "NetworkManager.service" + "systemd-resolved.service" + ]; + path = [ + # Needed for mount + "/run/wrappers" + ]; + startLimitBurst = 5; + startLimitIntervalSec = 20; + serviceConfig = { + ExecStart = "${pkgs.ivpn-service}/bin/ivpn-service --logging"; + Restart = "always"; + RestartSec = 1; + }; + }; + }; + + meta.maintainers = with maintainers; [ ataraxiasjel ]; +} diff --git a/nixos/modules/services/networking/ndppd.nix b/nixos/modules/services/networking/ndppd.nix index 98c58d2d5db1b..d221c95ae6200 100644 --- a/nixos/modules/services/networking/ndppd.nix +++ b/nixos/modules/services/networking/ndppd.nix @@ -17,7 +17,7 @@ let ttl ${toString proxy.ttl} ${render proxy.rules (ruleNetworkName: rule: '' rule ${prefer rule.network ruleNetworkName} { - ${rule.method}${if rule.method == "iface" then " ${rule.interface}" else ""} + ${rule.method}${optionalString (rule.method == "iface") " ${rule.interface}"} }'')} }'')} ''); diff --git a/nixos/modules/services/networking/netbird.nix b/nixos/modules/services/networking/netbird.nix index 5bd9e9ca61696..647c0ce3e6d1f 100644 --- a/nixos/modules/services/networking/netbird.nix +++ b/nixos/modules/services/networking/netbird.nix @@ -41,9 +41,10 @@ in { documentation = [ "https://netbird.io/docs/" ]; after = [ "network.target" ]; wantedBy = [ "multi-user.target" ]; + path = with pkgs; [ + openresolv + ]; serviceConfig = { - AmbientCapabilities = [ "CAP_NET_ADMIN" ]; - DynamicUser = true; Environment = [ "NB_CONFIG=/var/lib/netbird/config.json" "NB_LOG_FILE=console" diff --git a/nixos/modules/services/networking/ntopng.nix b/nixos/modules/services/networking/ntopng.nix index e6344d7ff3b34..bf7ec19f02a68 100644 --- a/nixos/modules/services/networking/ntopng.nix +++ b/nixos/modules/services/networking/ntopng.nix @@ -86,7 +86,7 @@ in redis.createInstance = mkOption { type = types.nullOr types.str; - default = if versionAtLeast config.system.stateVersion "22.05" then "ntopng" else ""; + default = optionalString (versionAtLeast config.system.stateVersion "22.05") "ntopng"; description = lib.mdDoc '' Local Redis instance name. Set to `null` to disable local Redis instance. Defaults to `""` for diff --git a/nixos/modules/services/networking/peroxide.nix b/nixos/modules/services/networking/peroxide.nix index 6cac4bf2f89a1..885ee1d96cd05 100644 --- a/nixos/modules/services/networking/peroxide.nix +++ b/nixos/modules/services/networking/peroxide.nix @@ -9,7 +9,7 @@ let in { options.services.peroxide = { - enable = mkEnableOption (lib.mdDoc "enable"); + enable = mkEnableOption (lib.mdDoc "peroxide"); package = mkPackageOptionMD pkgs "peroxide" { default = [ "peroxide" ]; diff --git a/nixos/modules/services/networking/smokeping.nix b/nixos/modules/services/networking/smokeping.nix index c2c2a370cb004..19ab3f1aa48c0 100644 --- a/nixos/modules/services/networking/smokeping.nix +++ b/nixos/modules/services/networking/smokeping.nix @@ -339,14 +339,9 @@ in }; preStart = '' mkdir -m 0755 -p ${smokepingHome}/cache ${smokepingHome}/data - rm -f ${smokepingHome}/cropper - ln -s ${cfg.package}/htdocs/cropper ${smokepingHome}/cropper - rm -f ${smokepingHome}/css - ln -s ${cfg.package}/htdocs/css ${smokepingHome}/css - rm -f ${smokepingHome}/js - ln -s ${cfg.package}/htdocs/js ${smokepingHome}/js - rm -f ${smokepingHome}/smokeping.fcgi - ln -s ${cgiHome} ${smokepingHome}/smokeping.fcgi + ln -sf ${cfg.package}/htdocs/css ${smokepingHome}/css + ln -sf ${cfg.package}/htdocs/js ${smokepingHome}/js + ln -sf ${cgiHome} ${smokepingHome}/smokeping.fcgi ${cfg.package}/bin/smokeping --check --config=${configPath} ${cfg.package}/bin/smokeping --static --config=${configPath} ''; diff --git a/nixos/modules/services/networking/ssh/lshd.nix b/nixos/modules/services/networking/ssh/lshd.nix index 7932bac9ca3a1..af64969c2fcd4 100644 --- a/nixos/modules/services/networking/ssh/lshd.nix +++ b/nixos/modules/services/networking/ssh/lshd.nix @@ -169,11 +169,11 @@ in else (concatStrings (map (i: "--interface=\"${i}\"") interfaces))} \ -h "${hostKey}" \ - ${if !syslog then "--no-syslog" else ""} \ + ${optionalString (!syslog) "--no-syslog" } \ ${if passwordAuthentication then "--password" else "--no-password" } \ ${if publicKeyAuthentication then "--publickey" else "--no-publickey" } \ ${if rootLogin then "--root-login" else "--no-root-login" } \ - ${if loginShell != null then "--login-shell=\"${loginShell}\"" else "" } \ + ${optionalString (loginShell != null) "--login-shell=\"${loginShell}\"" } \ ${if srpKeyExchange then "--srp-keyexchange" else "--no-srp-keyexchange" } \ ${if !tcpForwarding then "--no-tcpip-forward" else "--tcpip-forward"} \ ${if x11Forwarding then "--x11-forward" else "--no-x11-forward" } \ diff --git a/nixos/modules/services/networking/ssh/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix index 5f225682b7779..89ddf82152993 100644 --- a/nixos/modules/services/networking/ssh/sshd.nix +++ b/nixos/modules/services/networking/ssh/sshd.nix @@ -474,10 +474,10 @@ in mkdir -m 0755 -p "$(dirname '${k.path}')" ssh-keygen \ -t "${k.type}" \ - ${if k ? bits then "-b ${toString k.bits}" else ""} \ - ${if k ? rounds then "-a ${toString k.rounds}" else ""} \ - ${if k ? comment then "-C '${k.comment}'" else ""} \ - ${if k ? openSSHFormat && k.openSSHFormat then "-o" else ""} \ + ${optionalString (k ? bits) "-b ${toString k.bits}"} \ + ${optionalString (k ? rounds) "-a ${toString k.rounds}"} \ + ${optionalString (k ? comment) "-C '${k.comment}'"} \ + ${optionalString (k ? openSSHFormat && k.openSSHFormat) "-o"} \ -f "${k.path}" \ -N "" fi @@ -536,7 +536,7 @@ in # https://github.com/NixOS/nixpkgs/pull/10155 # https://github.com/NixOS/nixpkgs/pull/41745 services.openssh.authorizedKeysFiles = - [ "%h/.ssh/authorized_keys" "%h/.ssh/authorized_keys2" "/etc/ssh/authorized_keys.d/%u" ]; + [ "%h/.ssh/authorized_keys" "/etc/ssh/authorized_keys.d/%u" ]; services.openssh.extraConfig = mkOrder 0 '' @@ -550,7 +550,7 @@ in '') cfg.ports} ${concatMapStrings ({ port, addr, ... }: '' - ListenAddress ${addr}${if port != null then ":" + toString port else ""} + ListenAddress ${addr}${optionalString (port != null) (":" + toString port)} '') cfg.listenAddresses} ${optionalString cfgc.setXAuthLocation '' diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix index 8b1398bfd47d4..e58526814d1ad 100644 --- a/nixos/modules/services/networking/strongswan.nix +++ b/nixos/modules/services/networking/strongswan.nix @@ -4,7 +4,7 @@ let inherit (builtins) toFile; inherit (lib) concatMapStringsSep concatStringsSep mapAttrsToList - mkIf mkEnableOption mkOption types literalExpression; + mkIf mkEnableOption mkOption types literalExpression optionalString; cfg = config.services.strongswan; @@ -34,8 +34,8 @@ let strongswanConf = {setup, connections, ca, secretsFile, managePlugins, enabledPlugins}: toFile "strongswan.conf" '' charon { - ${if managePlugins then "load_modular = no" else ""} - ${if managePlugins then ("load = " + (concatStringsSep " " enabledPlugins)) else ""} + ${optionalString managePlugins "load_modular = no"} + ${optionalString managePlugins ("load = " + (concatStringsSep " " enabledPlugins))} plugins { stroke { secrets_file = ${secretsFile} diff --git a/nixos/modules/services/networking/stunnel.nix b/nixos/modules/services/networking/stunnel.nix index 4f592fb312d33..996e9b2253921 100644 --- a/nixos/modules/services/networking/stunnel.nix +++ b/nixos/modules/services/networking/stunnel.nix @@ -154,8 +154,8 @@ in environment.systemPackages = [ pkgs.stunnel ]; environment.etc."stunnel.cfg".text = '' - ${ if cfg.user != null then "setuid = ${cfg.user}" else "" } - ${ if cfg.group != null then "setgid = ${cfg.group}" else "" } + ${ optionalString (cfg.user != null) "setuid = ${cfg.user}" } + ${ optionalString (cfg.group != null) "setgid = ${cfg.group}" } debug = ${cfg.logLevel} diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix new file mode 100644 index 0000000000000..93227a9b625d0 --- /dev/null +++ b/nixos/modules/services/networking/wgautomesh.nix @@ -0,0 +1,161 @@ +{ lib, config, pkgs, ... }: +with lib; +let + cfg = config.services.wgautomesh; + settingsFormat = pkgs.formats.toml { }; + configFile = + # Have to remove nulls manually as TOML generator will not just skip key + # if value is null + settingsFormat.generate "wgautomesh-config.toml" + (filterAttrs (k: v: v != null) + (mapAttrs + (k: v: + if k == "peers" + then map (e: filterAttrs (k: v: v != null) e) v + else v) + cfg.settings)); + runtimeConfigFile = + if cfg.enableGossipEncryption + then "/run/wgautomesh/wgautomesh.toml" + else configFile; +in +{ + options.services.wgautomesh = { + enable = mkEnableOption (mdDoc "the wgautomesh daemon"); + logLevel = mkOption { + type = types.enum [ "trace" "debug" "info" "warn" "error" ]; + default = "info"; + description = mdDoc "wgautomesh log level."; + }; + enableGossipEncryption = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable encryption of gossip traffic."; + }; + gossipSecretFile = mkOption { + type = types.path; + description = mdDoc '' + File containing the shared secret key to use for gossip encryption. + Required if `enableGossipEncryption` is set. + ''; + }; + enablePersistence = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable persistence of Wireguard peer info between restarts."; + }; + openFirewall = mkOption { + type = types.bool; + default = true; + description = mdDoc "Automatically open gossip port in firewall (recommended)."; + }; + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = { + + interface = mkOption { + type = types.str; + description = mdDoc '' + Wireguard interface to manage (it is NOT created by wgautomesh, you + should use another NixOS option to create it such as + `networking.wireguard.interfaces.wg0 = {...};`). + ''; + example = "wg0"; + }; + gossip_port = mkOption { + type = types.port; + description = mdDoc '' + wgautomesh gossip port, this MUST be the same number on all nodes in + the wgautomesh network. + ''; + default = 1666; + }; + lan_discovery = mkOption { + type = types.bool; + default = true; + description = mdDoc "Enable discovery of peers on the same LAN using UDP broadcast."; + }; + upnp_forward_external_port = mkOption { + type = types.nullOr types.port; + default = null; + description = mdDoc '' + Public port number to try to redirect to this machine's Wireguard + daemon using UPnP IGD. + ''; + }; + peers = mkOption { + type = types.listOf (types.submodule { + options = { + pubkey = mkOption { + type = types.str; + description = mdDoc "Wireguard public key of this peer."; + }; + address = mkOption { + type = types.str; + description = mdDoc '' + Wireguard address of this peer (a single IP address, multliple + addresses or address ranges are not supported). + ''; + example = "10.0.0.42"; + }; + endpoint = mkOption { + type = types.nullOr types.str; + description = mdDoc '' + Bootstrap endpoint for connecting to this Wireguard peer if no + other address is known or none are working. + ''; + default = null; + example = "wgnode.mydomain.example:51820"; + }; + }; + }); + default = [ ]; + description = mdDoc "wgautomesh peer list."; + }; + }; + + }; + default = { }; + description = mdDoc "Configuration for wgautomesh."; + }; + }; + + config = mkIf cfg.enable { + services.wgautomesh.settings = { + gossip_secret_file = mkIf cfg.enableGossipEncryption "$CREDENTIALS_DIRECTORY/gossip_secret"; + persist_file = mkIf cfg.enablePersistence "/var/lib/wgautomesh/state"; + }; + + systemd.services.wgautomesh = { + path = [ pkgs.wireguard-tools ]; + environment = { RUST_LOG = "wgautomesh=${cfg.logLevel}"; }; + description = "wgautomesh"; + serviceConfig = { + Type = "simple"; + + ExecStart = "${getExe pkgs.wgautomesh} ${runtimeConfigFile}"; + Restart = "always"; + RestartSec = "30"; + LoadCredential = mkIf cfg.enableGossipEncryption [ "gossip_secret:${cfg.gossipSecretFile}" ]; + + ExecStartPre = mkIf cfg.enableGossipEncryption [ + ''${pkgs.envsubst}/bin/envsubst \ + -i ${configFile} \ + -o ${runtimeConfigFile}'' + ]; + + DynamicUser = true; + StateDirectory = "wgautomesh"; + StateDirectoryMode = "0700"; + RuntimeDirectory = "wgautomesh"; + AmbientCapabilities = "CAP_NET_ADMIN"; + CapabilityBoundingSet = "CAP_NET_ADMIN"; + }; + wantedBy = [ "multi-user.target" ]; + }; + networking.firewall.allowedUDPPorts = + mkIf cfg.openFirewall [ cfg.settings.gossip_port ]; + }; +} + diff --git a/nixos/modules/services/networking/wstunnel.nix b/nixos/modules/services/networking/wstunnel.nix index 440b617f60a39..067d5df487255 100644 --- a/nixos/modules/services/networking/wstunnel.nix +++ b/nixos/modules/services/networking/wstunnel.nix @@ -294,7 +294,7 @@ let DynamicUser = true; SupplementaryGroups = optional (serverCfg.useACMEHost != null) certConfig.group; PrivateTmp = true; - AmbientCapabilities = optional (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + AmbientCapabilities = optionals (serverCfg.listen.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; @@ -340,7 +340,7 @@ let EnvironmentFile = optional (clientCfg.environmentFile != null) clientCfg.environmentFile; DynamicUser = true; PrivateTmp = true; - AmbientCapabilities = (optional (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optional ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]); + AmbientCapabilities = (optionals (clientCfg.soMark != null) [ "CAP_NET_ADMIN" ]) ++ (optionals ((clientCfg.dynamicToRemote.port or 1024) < 1024 || (any (x: x.local.port < 1024) clientCfg.localToRemote)) [ "CAP_NET_BIND_SERVICE" ]); NoNewPrivileges = true; RestrictNamespaces = "uts ipc pid user cgroup"; ProtectSystem = "strict"; diff --git a/nixos/modules/services/networking/xinetd.nix b/nixos/modules/services/networking/xinetd.nix index b9120f37ba247..fb3de7077e31e 100644 --- a/nixos/modules/services/networking/xinetd.nix +++ b/nixos/modules/services/networking/xinetd.nix @@ -27,7 +27,7 @@ let ${optionalString srv.unlisted "type = UNLISTED"} ${optionalString (srv.flags != "") "flags = ${srv.flags}"} socket_type = ${if srv.protocol == "udp" then "dgram" else "stream"} - ${if srv.port != 0 then "port = ${toString srv.port}" else ""} + ${optionalString (srv.port != 0) "port = ${toString srv.port}"} wait = ${if srv.protocol == "udp" then "yes" else "no"} user = ${srv.user} server = ${srv.server} diff --git a/nixos/modules/services/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix index 9ac89e057620b..f6a23fb900f08 100644 --- a/nixos/modules/services/printing/cupsd.nix +++ b/nixos/modules/services/printing/cupsd.nix @@ -317,6 +317,7 @@ in environment.etc.cups.source = "/var/lib/cups"; services.dbus.packages = [ cups.out ] ++ optional polkitEnabled cups-pk-helper; + services.udev.packages = cfg.drivers; # Allow asswordless printer admin for members of wheel group security.polkit.extraConfig = mkIf polkitEnabled '' diff --git a/nixos/modules/services/search/qdrant.nix b/nixos/modules/services/search/qdrant.nix index a843c44dbb5f9..e1f7365d951a0 100644 --- a/nixos/modules/services/search/qdrant.nix +++ b/nixos/modules/services/search/qdrant.nix @@ -100,6 +100,7 @@ in { after = [ "network.target" ]; serviceConfig = { + LimitNOFILE=65536; ExecStart = "${pkgs.qdrant}/bin/qdrant --config-path ${configFile}"; DynamicUser = true; Restart = "on-failure"; diff --git a/nixos/modules/services/security/authelia.nix b/nixos/modules/services/security/authelia.nix index 143c441c7e153..28c5fd0a1df59 100644 --- a/nixos/modules/services/security/authelia.nix +++ b/nixos/modules/services/security/authelia.nix @@ -336,7 +336,7 @@ in ProtectProc = "noaccess"; ProtectSystem = "strict"; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; RestrictNamespaces = true; RestrictRealtime = true; RestrictSUIDSGID = true; diff --git a/nixos/modules/services/security/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix index ead24d1470717..93962d40ce4b4 100644 --- a/nixos/modules/services/security/fail2ban.nix +++ b/nixos/modules/services/security/fail2ban.nix @@ -78,6 +78,13 @@ in ''; }; + bantime = mkOption { + default = null; + type = types.nullOr types.str; + example = "10m"; + description = lib.mdDoc "Number of seconds that a host is banned."; + }; + maxretry = mkOption { default = 3; type = types.ints.unsigned; @@ -202,6 +209,20 @@ in ''; }; + extraSettings = mkOption { + type = with types; attrsOf (oneOf [ bool ints.positive str ]); + default = {}; + description = lib.mdDoc '' + Extra default configuration for all jails (i.e. `[DEFAULT]`). See + <https://github.com/fail2ban/fail2ban/blob/master/config/jail.conf> for an overview. + ''; + example = literalExpression '' + { + findtime = "15m"; + } + ''; + }; + jails = mkOption { default = { }; example = literalExpression '' @@ -320,11 +341,18 @@ in ''} # Miscellaneous options ignoreip = 127.0.0.1/8 ${optionalString config.networking.enableIPv6 "::1"} ${concatStringsSep " " cfg.ignoreIP} + ${optionalString (cfg.bantime != null) '' + bantime = ${cfg.bantime} + ''} maxretry = ${toString cfg.maxretry} backend = systemd # Actions banaction = ${cfg.banaction} banaction_allports = ${cfg.banaction-allports} + ${optionalString (cfg.extraSettings != {}) '' + # Extra settings + ${generators.toKeyValue {} cfg.extraSettings} + ''} ''; # Block SSH if there are too many failing connection attempts. # Benefits from verbose sshd logging to observe failed login attempts, diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix index 5583c39368f77..2f19decb5cb17 100644 --- a/nixos/modules/services/security/kanidm.nix +++ b/nixos/modules/services/security/kanidm.nix @@ -7,6 +7,18 @@ let serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); + certPaths = builtins.map builtins.dirOf [ cfg.serverSettings.tls_chain cfg.serverSettings.tls_key ]; + + # Merge bind mount paths and remove paths where a prefix is already mounted. + # This makes sure that if e.g. the tls_chain is in the nix store and /nix/store is alread in the mount + # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. + hasPrefixInList = list: newPath: lib.any (path: lib.hasPrefix (builtins.toString path) (builtins.toString newPath)) list; + mergePaths = lib.foldl' (merged: newPath: let + # If the new path is a prefix to some existing path, we need to filter it out + filteredPaths = lib.filter (p: !lib.hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + # If a prefix of the new path is already in the list, do not add it + filteredNew = if hasPrefixInList filteredPaths newPath then [] else [ newPath ]; + in filteredPaths ++ filteredNew) []; defaultServiceConfig = { BindReadOnlyPaths = [ @@ -16,7 +28,7 @@ let "-/etc/hosts" "-/etc/localtime" ]; - CapabilityBoundingSet = ""; + CapabilityBoundingSet = []; # ProtectClock= adds DeviceAllow=char-rtc r DeviceAllow = ""; # Implies ProtectSystem=strict, which re-mounts all paths @@ -216,22 +228,28 @@ in description = "kanidm identity management daemon"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; - serviceConfig = defaultServiceConfig // { - StateDirectory = "kanidm"; - StateDirectoryMode = "0700"; - ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; - User = "kanidm"; - Group = "kanidm"; + serviceConfig = lib.mkMerge [ + # Merge paths and ignore existing prefixes needs to sidestep mkMerge + (defaultServiceConfig // { + BindReadOnlyPaths = mergePaths (defaultServiceConfig.BindReadOnlyPaths ++ certPaths); + }) + { + StateDirectory = "kanidm"; + StateDirectoryMode = "0700"; + ExecStart = "${pkgs.kanidm}/bin/kanidmd server -c ${serverConfigFile}"; + User = "kanidm"; + Group = "kanidm"; - AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; - CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; - # This would otherwise override the CAP_NET_BIND_SERVICE capability. - PrivateUsers = false; - # Port needs to be exposed to the host network - PrivateNetwork = false; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; - TemporaryFileSystem = "/:ro"; - }; + AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ]; + CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ]; + # This would otherwise override the CAP_NET_BIND_SERVICE capability. + PrivateUsers = lib.mkForce false; + # Port needs to be exposed to the host network + PrivateNetwork = lib.mkForce false; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + TemporaryFileSystem = "/:ro"; + } + ]; environment.RUST_LOG = "info"; }; @@ -240,34 +258,32 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network.target" ]; restartTriggers = [ unixConfigFile clientConfigFile ]; - serviceConfig = defaultServiceConfig // { - CacheDirectory = "kanidm-unixd"; - CacheDirectoryMode = "0700"; - RuntimeDirectory = "kanidm-unixd"; - ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; - User = "kanidm-unixd"; - Group = "kanidm-unixd"; + serviceConfig = lib.mkMerge [ + defaultServiceConfig + { + CacheDirectory = "kanidm-unixd"; + CacheDirectoryMode = "0700"; + RuntimeDirectory = "kanidm-unixd"; + ExecStart = "${pkgs.kanidm}/bin/kanidm_unixd"; + User = "kanidm-unixd"; + Group = "kanidm-unixd"; - BindReadOnlyPaths = [ - "/nix/store" - "-/etc/resolv.conf" - "-/etc/nsswitch.conf" - "-/etc/hosts" - "-/etc/localtime" - "-/etc/kanidm" - "-/etc/static/kanidm" - "-/etc/ssl" - "-/etc/static/ssl" - ]; - BindPaths = [ - # To create the socket - "/run/kanidm-unixd:/var/run/kanidm-unixd" - ]; - # Needs to connect to kanidmd - PrivateNetwork = false; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; - TemporaryFileSystem = "/:ro"; - }; + BindReadOnlyPaths = [ + "-/etc/kanidm" + "-/etc/static/kanidm" + "-/etc/ssl" + "-/etc/static/ssl" + ]; + BindPaths = [ + # To create the socket + "/run/kanidm-unixd:/var/run/kanidm-unixd" + ]; + # Needs to connect to kanidmd + PrivateNetwork = lib.mkForce false; + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ]; + TemporaryFileSystem = "/:ro"; + } + ]; environment.RUST_LOG = "info"; }; diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix index e3f8e75ca2476..12547acabfe05 100644 --- a/nixos/modules/services/security/oauth2_proxy.nix +++ b/nixos/modules/services/security/oauth2_proxy.nix @@ -72,15 +72,14 @@ let } // (getProviderOptions cfg cfg.provider) // cfg.extraConfig; mapConfig = key: attr: - if attr != null && attr != [] then ( + optionalString (attr != null && attr != []) ( if isDerivation attr then mapConfig key (toString attr) else if (builtins.typeOf attr) == "set" then concatStringsSep " " (mapAttrsToList (name: value: mapConfig (key + "-" + name) value) attr) else if (builtins.typeOf attr) == "list" then concatMapStringsSep " " (mapConfig key) attr else if (builtins.typeOf attr) == "bool" then "--${key}=${boolToString attr}" else if (builtins.typeOf attr) == "string" then "--${key}='${attr}'" else - "--${key}=${toString attr}") - else ""; + "--${key}=${toString attr}"); configString = concatStringsSep " " (mapAttrsToList mapConfig allConfig); in diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix index 11769d4e3095f..06494ddb631af 100644 --- a/nixos/modules/services/system/cachix-agent/default.nix +++ b/nixos/modules/services/system/cachix-agent/default.nix @@ -72,7 +72,7 @@ in { EnvironmentFile = cfg.credentialsFile; ExecStart = '' ${cfg.package}/bin/cachix ${lib.optionalString cfg.verbose "--verbose"} ${lib.optionalString (cfg.host != null) "--host ${cfg.host}"} \ - deploy agent ${cfg.name} ${if cfg.profile != null then cfg.profile else ""} + deploy agent ${cfg.name} ${optionalString (cfg.profile != null) cfg.profile} ''; }; }; diff --git a/nixos/modules/services/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix index 85e9509bcc82d..89157b460b9a4 100644 --- a/nixos/modules/services/system/cachix-watch-store.nix +++ b/nixos/modules/services/system/cachix-watch-store.nix @@ -62,7 +62,13 @@ in after = [ "network-online.target" ]; path = [ config.nix.package ]; wantedBy = [ "multi-user.target" ]; + unitConfig = { + # allow to restart indefinitely + StartLimitIntervalSec = 0; + }; serviceConfig = { + # don't put too much stress on the machine when restarting + RestartSec = 1; # we don't want to kill children processes as those are deployments KillMode = "process"; Restart = "on-failure"; diff --git a/nixos/modules/services/system/dbus.nix b/nixos/modules/services/system/dbus.nix index c677088101f0c..9d8a62ec78c53 100644 --- a/nixos/modules/services/system/dbus.nix +++ b/nixos/modules/services/system/dbus.nix @@ -14,13 +14,17 @@ let serviceDirectories = cfg.packages; }; - inherit (lib) mkOption mkIf mkMerge types; + inherit (lib) mkOption mkEnableOption mkIf mkMerge types; in { options = { + boot.initrd.systemd.dbus = { + enable = mkEnableOption (lib.mdDoc "dbus in stage 1") // { visible = false; }; + }; + services.dbus = { enable = mkOption { @@ -111,6 +115,21 @@ in ]; } + (mkIf config.boot.initrd.systemd.dbus.enable { + boot.initrd.systemd = { + users.messagebus = { }; + groups.messagebus = { }; + contents."/etc/dbus-1".source = pkgs.makeDBusConf { + inherit (cfg) apparmor; + suidHelper = "/bin/false"; + serviceDirectories = [ pkgs.dbus ]; + }; + packages = [ pkgs.dbus ]; + storePaths = [ "${pkgs.dbus}/bin/dbus-daemon" ]; + targets.sockets.wants = [ "dbus.socket" ]; + }; + }) + (mkIf (cfg.implementation == "dbus") { environment.systemPackages = [ pkgs.dbus diff --git a/nixos/modules/services/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix index 2dd62edab7871..18a9e3d5fe305 100644 --- a/nixos/modules/services/video/rtsp-simple-server.nix +++ b/nixos/modules/services/video/mediamtx.nix @@ -3,19 +3,19 @@ with lib; let - cfg = config.services.rtsp-simple-server; - package = pkgs.rtsp-simple-server; + cfg = config.services.mediamtx; + package = pkgs.mediamtx; format = pkgs.formats.yaml {}; in { options = { - services.rtsp-simple-server = { - enable = mkEnableOption (lib.mdDoc "RTSP Simple Server"); + services.mediamtx = { + enable = mkEnableOption (lib.mdDoc "MediaMTX"); settings = mkOption { description = lib.mdDoc '' - Settings for rtsp-simple-server. - Read more at <https://github.com/aler9/rtsp-simple-server/blob/main/rtsp-simple-server.yml> + Settings for MediaMTX. + Read more at <https://github.com/aler9/mediamtx/blob/main/mediamtx.yml> ''; type = format.type; @@ -25,7 +25,7 @@ in "stdout" ]; # we set this so when the user uses it, it just works (see LogsDirectory below). but it's not used by default. - logFile = "/var/log/rtsp-simple-server/rtsp-simple-server.log"; + logFile = "/var/log/mediamtx/mediamtx.log"; }; example = { @@ -40,20 +40,20 @@ in env = mkOption { type = with types; attrsOf anything; - description = lib.mdDoc "Extra environment variables for RTSP Simple Server"; + description = lib.mdDoc "Extra environment variables for MediaMTX"; default = {}; example = { - RTSP_CONFKEY = "mykey"; + MTX_CONFKEY = "mykey"; }; }; }; }; config = mkIf (cfg.enable) { - # NOTE: rtsp-simple-server watches this file and automatically reloads if it changes - environment.etc."rtsp-simple-server.yaml".source = format.generate "rtsp-simple-server.yaml" cfg.settings; + # NOTE: mediamtx watches this file and automatically reloads if it changes + environment.etc."mediamtx.yaml".source = format.generate "mediamtx.yaml" cfg.settings; - systemd.services.rtsp-simple-server = { + systemd.services.mediamtx = { environment = cfg.env; after = [ "network.target" ]; @@ -65,15 +65,15 @@ in serviceConfig = { DynamicUser = true; - User = "rtsp-simple-server"; - Group = "rtsp-simple-server"; + User = "mediamtx"; + Group = "mediamtx"; - LogsDirectory = "rtsp-simple-server"; + LogsDirectory = "mediamtx"; # user likely may want to stream cameras, can't hurt to add video group SupplementaryGroups = "video"; - ExecStart = "${package}/bin/rtsp-simple-server /etc/rtsp-simple-server.yaml"; + ExecStart = "${package}/bin/mediamtx /etc/mediamtx.yaml"; }; }; }; diff --git a/nixos/modules/services/web-apps/discourse.nix b/nixos/modules/services/web-apps/discourse.nix index 151fb812ddea6..5b2bd5aeeb09c 100644 --- a/nixos/modules/services/web-apps/discourse.nix +++ b/nixos/modules/services/web-apps/discourse.nix @@ -1025,8 +1025,8 @@ in services.postfix = lib.mkIf cfg.mail.incoming.enable { enable = true; - sslCert = if cfg.sslCertificate != null then cfg.sslCertificate else ""; - sslKey = if cfg.sslCertificateKey != null then cfg.sslCertificateKey else ""; + sslCert = lib.optionalString (cfg.sslCertificate != null) cfg.sslCertificate; + sslKey = lib.optionalString (cfg.sslCertificateKey != null) cfg.sslCertificateKey; origin = cfg.hostname; relayDomains = [ cfg.hostname ]; diff --git a/nixos/modules/services/web-apps/ihatemoney/default.nix b/nixos/modules/services/web-apps/ihatemoney/default.nix deleted file mode 100644 index a61aa445f82c5..0000000000000 --- a/nixos/modules/services/web-apps/ihatemoney/default.nix +++ /dev/null @@ -1,153 +0,0 @@ -{ config, pkgs, lib, ... }: -with lib; -let - cfg = config.services.ihatemoney; - user = "ihatemoney"; - group = "ihatemoney"; - db = "ihatemoney"; - python3 = config.services.uwsgi.package.python3; - pkg = python3.pkgs.ihatemoney; - toBool = x: if x then "True" else "False"; - configFile = pkgs.writeText "ihatemoney.cfg" '' - from secrets import token_hex - # load a persistent secret key - SECRET_KEY_FILE = "/var/lib/ihatemoney/secret_key" - SECRET_KEY = "" - try: - with open(SECRET_KEY_FILE) as f: - SECRET_KEY = f.read() - except FileNotFoundError: - pass - if not SECRET_KEY: - print("ihatemoney: generating a new secret key") - SECRET_KEY = token_hex(50) - with open(SECRET_KEY_FILE, "w") as f: - f.write(SECRET_KEY) - del token_hex - del SECRET_KEY_FILE - - # "normal" configuration - DEBUG = False - SQLALCHEMY_DATABASE_URI = '${ - if cfg.backend == "sqlite" - then "sqlite:////var/lib/ihatemoney/ihatemoney.sqlite" - else "postgresql:///${db}"}' - SQLALCHEMY_TRACK_MODIFICATIONS = False - MAIL_DEFAULT_SENDER = (r"${cfg.defaultSender.name}", r"${cfg.defaultSender.email}") - ACTIVATE_DEMO_PROJECT = ${toBool cfg.enableDemoProject} - ADMIN_PASSWORD = r"${toString cfg.adminHashedPassword /*toString null == ""*/}" - ALLOW_PUBLIC_PROJECT_CREATION = ${toBool cfg.enablePublicProjectCreation} - ACTIVATE_ADMIN_DASHBOARD = ${toBool cfg.enableAdminDashboard} - SESSION_COOKIE_SECURE = ${toBool cfg.secureCookie} - ENABLE_CAPTCHA = ${toBool cfg.enableCaptcha} - LEGAL_LINK = r"${toString cfg.legalLink}" - - ${cfg.extraConfig} - ''; -in - { - options.services.ihatemoney = { - enable = mkEnableOption (lib.mdDoc "ihatemoney webapp. Note that this will set uwsgi to emperor mode"); - backend = mkOption { - type = types.enum [ "sqlite" "postgresql" ]; - default = "sqlite"; - description = lib.mdDoc '' - The database engine to use for ihatemoney. - If `postgresql` is selected, then a database called - `${db}` will be created. If you disable this option, - it will however not be removed. - ''; - }; - adminHashedPassword = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc "The hashed password of the administrator. To obtain it, run `ihatemoney generate_password_hash`"; - }; - uwsgiConfig = mkOption { - type = types.attrs; - example = { - http = ":8000"; - }; - description = lib.mdDoc "Additional configuration of the UWSGI vassal running ihatemoney. It should notably specify on which interfaces and ports the vassal should listen."; - }; - defaultSender = { - name = mkOption { - type = types.str; - default = "Budget manager"; - description = lib.mdDoc "The display name of the sender of ihatemoney emails"; - }; - email = mkOption { - type = types.str; - default = "ihatemoney@${config.networking.hostName}"; - defaultText = literalExpression ''"ihatemoney@''${config.networking.hostName}"''; - description = lib.mdDoc "The email of the sender of ihatemoney emails"; - }; - }; - secureCookie = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc "Use secure cookies. Disable this when ihatemoney is served via http instead of https"; - }; - enableDemoProject = mkEnableOption (lib.mdDoc "access to the demo project in ihatemoney"); - enablePublicProjectCreation = mkEnableOption (lib.mdDoc "permission to create projects in ihatemoney by anyone"); - enableAdminDashboard = mkEnableOption (lib.mdDoc "ihatemoney admin dashboard"); - enableCaptcha = mkEnableOption (lib.mdDoc "a simplistic captcha for some forms"); - legalLink = mkOption { - type = types.nullOr types.str; - default = null; - description = lib.mdDoc "The URL to a page explaining legal statements about your service, eg. GDPR-related information."; - }; - extraConfig = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc "Extra configuration appended to ihatemoney's configuration file. It is a python file, so pay attention to indentation."; - }; - }; - config = mkIf cfg.enable { - services.postgresql = mkIf (cfg.backend == "postgresql") { - enable = true; - ensureDatabases = [ db ]; - ensureUsers = [ { - name = user; - ensurePermissions = { - "DATABASE ${db}" = "ALL PRIVILEGES"; - }; - } ]; - }; - systemd.services.postgresql = mkIf (cfg.backend == "postgresql") { - wantedBy = [ "uwsgi.service" ]; - before = [ "uwsgi.service" ]; - }; - systemd.tmpfiles.rules = [ - "d /var/lib/ihatemoney 770 ${user} ${group}" - ]; - users = { - users.${user} = { - isSystemUser = true; - inherit group; - }; - groups.${group} = {}; - }; - services.uwsgi = { - enable = true; - plugins = [ "python3" ]; - instance = { - type = "emperor"; - vassals.ihatemoney = { - type = "normal"; - strict = true; - immediate-uid = user; - immediate-gid = group; - # apparently flask uses threads: https://github.com/spiral-project/ihatemoney/commit/c7815e48781b6d3a457eaff1808d179402558f8c - enable-threads = true; - module = "wsgi:application"; - chdir = "${pkg}/${pkg.pythonModule.sitePackages}/ihatemoney"; - env = [ "IHATEMONEY_SETTINGS_FILE_PATH=${configFile}" ]; - pythonPackages = self: [ self.ihatemoney ]; - } // cfg.uwsgiConfig; - }; - }; - }; - } - - diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix new file mode 100644 index 0000000000000..442044fedb14e --- /dev/null +++ b/nixos/modules/services/web-apps/monica.nix @@ -0,0 +1,468 @@ +{ + config, + lib, + pkgs, + ... +}: +with lib; let + cfg = config.services.monica; + monica = pkgs.monica.override { + dataDir = cfg.dataDir; + }; + db = cfg.database; + mail = cfg.mail; + + user = cfg.user; + group = cfg.group; + + # shell script for local administration + artisan = pkgs.writeScriptBin "monica" '' + #! ${pkgs.runtimeShell} + cd ${monica} + sudo() { + if [[ "$USER" != ${user} ]]; then + exec /run/wrappers/bin/sudo -u ${user} "$@" + else + exec "$@" + fi + } + sudo ${pkgs.php}/bin/php artisan "$@" + ''; + + tlsEnabled = cfg.nginx.addSSL || cfg.nginx.forceSSL || cfg.nginx.onlySSL || cfg.nginx.enableACME; +in { + options.services.monica = { + enable = mkEnableOption (lib.mdDoc "monica"); + + user = mkOption { + default = "monica"; + description = lib.mdDoc "User monica runs as."; + type = types.str; + }; + + group = mkOption { + default = "monica"; + description = lib.mdDoc "Group monica runs as."; + type = types.str; + }; + + appKeyFile = mkOption { + description = lib.mdDoc '' + A file containing the Laravel APP_KEY - a 32 character long, + base64 encoded key used for encryption where needed. Can be + generated with <code>head -c 32 /dev/urandom | base64</code>. + ''; + example = "/run/keys/monica-appkey"; + type = types.path; + }; + + hostname = lib.mkOption { + type = lib.types.str; + default = + if config.networking.domain != null + then config.networking.fqdn + else config.networking.hostName; + defaultText = lib.literalExpression "config.networking.fqdn"; + example = "monica.example.com"; + description = lib.mdDoc '' + The hostname to serve monica on. + ''; + }; + + appURL = mkOption { + description = lib.mdDoc '' + The root URL that you want to host monica on. All URLs in monica will be generated using this value. + If you change this in the future you may need to run a command to update stored URLs in the database. + Command example: <code>php artisan monica:update-url https://old.example.com https://new.example.com</code> + ''; + default = "http${lib.optionalString tlsEnabled "s"}://${cfg.hostname}"; + defaultText = ''http''${lib.optionalString tlsEnabled "s"}://''${cfg.hostname}''; + example = "https://example.com"; + type = types.str; + }; + + dataDir = mkOption { + description = lib.mdDoc "monica data directory"; + default = "/var/lib/monica"; + type = types.path; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Database host address."; + }; + port = mkOption { + type = types.port; + default = 3306; + description = lib.mdDoc "Database host port."; + }; + name = mkOption { + type = types.str; + default = "monica"; + description = lib.mdDoc "Database name."; + }; + user = mkOption { + type = types.str; + default = user; + defaultText = lib.literalExpression "user"; + description = lib.mdDoc "Database username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/monica-dbpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + <option>database.user</option>. + ''; + }; + createLocally = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc "Create the database and database user locally."; + }; + }; + + mail = { + driver = mkOption { + type = types.enum ["smtp" "sendmail"]; + default = "smtp"; + description = lib.mdDoc "Mail driver to use."; + }; + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "Mail host address."; + }; + port = mkOption { + type = types.port; + default = 1025; + description = lib.mdDoc "Mail host port."; + }; + fromName = mkOption { + type = types.str; + default = "monica"; + description = lib.mdDoc "Mail \"from\" name."; + }; + from = mkOption { + type = types.str; + default = "mail@monica.com"; + description = lib.mdDoc "Mail \"from\" email."; + }; + user = mkOption { + type = with types; nullOr str; + default = null; + example = "monica"; + description = lib.mdDoc "Mail username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/monica-mailpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + <option>mail.user</option>. + ''; + }; + encryption = mkOption { + type = with types; nullOr (enum ["tls"]); + default = null; + description = lib.mdDoc "SMTP encryption mechanism to use."; + }; + }; + + maxUploadSize = mkOption { + type = types.str; + default = "18M"; + example = "1G"; + description = lib.mdDoc "The maximum size for uploads (e.g. images)."; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [str int bool]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = lib.mdDoc '' + Options for the monica PHP pool. See the documentation on <literal>php-fpm.conf</literal> + for details on configuration directives. + ''; + }; + + nginx = mkOption { + type = types.submodule ( + recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix {inherit config lib;}) {} + ); + default = {}; + example = '' + { + serverAliases = [ + "monica.''${config.networking.domain}" + ]; + # To enable encryption and let let's encrypt take care of certificate + forceSSL = true; + enableACME = true; + } + ''; + description = lib.mdDoc '' + With this option, you can customize the nginx virtualHost settings. + ''; + }; + + config = mkOption { + type = with types; + attrsOf + (nullOr + (either + (oneOf [ + bool + int + port + path + str + ]) + (submodule { + options = { + _secret = mkOption { + type = nullOr str; + description = lib.mdDoc '' + The path to a file containing the value the + option should be set to in the final + configuration file. + ''; + }; + }; + }))); + default = {}; + example = '' + { + ALLOWED_IFRAME_HOSTS = "https://example.com"; + WKHTMLTOPDF = "/home/user/bins/wkhtmltopdf"; + AUTH_METHOD = "oidc"; + OIDC_NAME = "MyLogin"; + OIDC_DISPLAY_NAME_CLAIMS = "name"; + OIDC_CLIENT_ID = "monica"; + OIDC_CLIENT_SECRET = {_secret = "/run/keys/oidc_secret"}; + OIDC_ISSUER = "https://keycloak.example.com/auth/realms/My%20Realm"; + OIDC_ISSUER_DISCOVER = true; + } + ''; + description = lib.mdDoc '' + monica configuration options to set in the + <filename>.env</filename> file. + + Refer to <link xlink:href="https://github.com/monicahq/monica"/> + for details on supported values. + + Settings containing secret data should be set to an attribute + set containing the attribute <literal>_secret</literal> - a + string pointing to a file containing the value the option + should be set to. See the example to get a better picture of + this: in the resulting <filename>.env</filename> file, the + <literal>OIDC_CLIENT_SECRET</literal> key will be set to the + contents of the <filename>/run/keys/oidc_secret</filename> + file. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = db.createLocally -> db.user == user; + message = "services.monica.database.user must be set to ${user} if services.monica.database.createLocally is set true."; + } + { + assertion = db.createLocally -> db.passwordFile == null; + message = "services.monica.database.passwordFile cannot be specified if services.monica.database.createLocally is set to true."; + } + ]; + + services.monica.config = { + APP_ENV = "production"; + APP_KEY._secret = cfg.appKeyFile; + APP_URL = cfg.appURL; + DB_HOST = db.host; + DB_PORT = db.port; + DB_DATABASE = db.name; + DB_USERNAME = db.user; + MAIL_DRIVER = mail.driver; + MAIL_FROM_NAME = mail.fromName; + MAIL_FROM = mail.from; + MAIL_HOST = mail.host; + MAIL_PORT = mail.port; + MAIL_USERNAME = mail.user; + MAIL_ENCRYPTION = mail.encryption; + DB_PASSWORD._secret = db.passwordFile; + MAIL_PASSWORD._secret = mail.passwordFile; + APP_SERVICES_CACHE = "/run/monica/cache/services.php"; + APP_PACKAGES_CACHE = "/run/monica/cache/packages.php"; + APP_CONFIG_CACHE = "/run/monica/cache/config.php"; + APP_ROUTES_CACHE = "/run/monica/cache/routes-v7.php"; + APP_EVENTS_CACHE = "/run/monica/cache/events.php"; + SESSION_SECURE_COOKIE = tlsEnabled; + }; + + environment.systemPackages = [artisan]; + + services.mysql = mkIf db.createLocally { + enable = true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [db.name]; + ensureUsers = [ + { + name = db.user; + ensurePermissions = {"${db.name}.*" = "ALL PRIVILEGES";}; + } + ]; + }; + + services.phpfpm.pools.monica = { + inherit user group; + phpOptions = '' + log_errors = on + post_max_size = ${cfg.maxUploadSize} + upload_max_filesize = ${cfg.maxUploadSize} + ''; + settings = { + "listen.mode" = "0660"; + "listen.owner" = user; + "listen.group" = group; + } // cfg.poolConfig; + }; + + services.nginx = { + enable = mkDefault true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + recommendedBrotliSettings = true; + recommendedProxySettings = true; + virtualHosts.${cfg.hostname} = mkMerge [ + cfg.nginx + { + root = mkForce "${monica}/public"; + locations = { + "/" = { + index = "index.php"; + tryFiles = "$uri $uri/ /index.php?$query_string"; + }; + "~ \.php$".extraConfig = '' + fastcgi_pass unix:${config.services.phpfpm.pools."monica".socket}; + ''; + "~ \.(js|css|gif|png|ico|jpg|jpeg)$" = { + extraConfig = "expires 365d;"; + }; + }; + } + ]; + }; + + systemd.services.monica-setup = { + description = "Preperation tasks for monica"; + before = ["phpfpm-monica.service"]; + after = optional db.createLocally "mysql.service"; + wantedBy = ["multi-user.target"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = user; + UMask = 077; + WorkingDirectory = "${monica}"; + RuntimeDirectory = "monica/cache"; + RuntimeDirectoryMode = 0700; + }; + path = [pkgs.replace-secret]; + script = let + isSecret = v: isAttrs v && v ? _secret && isString v._secret; + monicaEnvVars = lib.generators.toKeyValue { + mkKeyValue = lib.flip lib.generators.mkKeyValueDefault "=" { + mkValueString = v: + with builtins; + if isInt v + then toString v + else if isString v + then v + else if true == v + then "true" + else if false == v + then "false" + else if isSecret v + then hashString "sha256" v._secret + else throw "unsupported type ${typeOf v}: ${(lib.generators.toPretty {}) v}"; + }; + }; + secretPaths = lib.mapAttrsToList (_: v: v._secret) (lib.filterAttrs (_: isSecret) cfg.config); + mkSecretReplacement = file: '' + replace-secret ${escapeShellArgs [(builtins.hashString "sha256" file) file "${cfg.dataDir}/.env"]} + ''; + secretReplacements = lib.concatMapStrings mkSecretReplacement secretPaths; + filteredConfig = lib.converge (lib.filterAttrsRecursive (_: v: ! elem v [{} null])) cfg.config; + monicaEnv = pkgs.writeText "monica.env" (monicaEnvVars filteredConfig); + in '' + # error handling + set -euo pipefail + + # create .env file + install -T -m 0600 -o ${user} ${monicaEnv} "${cfg.dataDir}/.env" + ${secretReplacements} + if ! grep 'APP_KEY=base64:' "${cfg.dataDir}/.env" >/dev/null; then + sed -i 's/APP_KEY=/APP_KEY=base64:/' "${cfg.dataDir}/.env" + fi + + # migrate & seed db + ${pkgs.php}/bin/php artisan key:generate --force + ${pkgs.php}/bin/php artisan setup:production -v --force + ''; + }; + + systemd.services.monica-scheduler = { + description = "Background tasks for monica"; + startAt = "minutely"; + after = ["monica-setup.service"]; + serviceConfig = { + Type = "oneshot"; + User = user; + WorkingDirectory = "${monica}"; + ExecStart = "${pkgs.php}/bin/php ${monica}/artisan schedule:run -v"; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0710 ${user} ${group} - -" + "d ${cfg.dataDir}/public 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/public/uploads 0750 ${user} ${group} - -" + "d ${cfg.dataDir}/storage 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/app 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/fonts 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/cache 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/sessions 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/framework/views 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/logs 0700 ${user} ${group} - -" + "d ${cfg.dataDir}/storage/uploads 0700 ${user} ${group} - -" + ]; + + users = { + users = mkIf (user == "monica") { + monica = { + inherit group; + isSystemUser = true; + }; + "${config.services.nginx.user}".extraGroups = [group]; + }; + groups = mkIf (group == "monica") { + monica = {}; + }; + }; + }; +} + diff --git a/nixos/modules/services/web-apps/moodle.nix b/nixos/modules/services/web-apps/moodle.nix index 5f8d9c5b15f4c..b617e9a593795 100644 --- a/nixos/modules/services/web-apps/moodle.nix +++ b/nixos/modules/services/web-apps/moodle.nix @@ -56,7 +56,7 @@ let mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql"; pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql"; - phpExt = pkgs.php80.buildEnv { + phpExt = pkgs.php81.buildEnv { extensions = { all, ... }: with all; [ iconv mbstring curl openssl tokenizer soap ctype zip gd simplexml dom intl sqlite3 pgsql pdo_sqlite pdo_pgsql pdo_odbc pdo_mysql pdo mysqli session zlib xmlreader fileinfo filter opcache exif sodium ]; extraConfig = "max_input_vars = 5000"; }; diff --git a/nixos/modules/services/web-apps/nextcloud.md b/nixos/modules/services/web-apps/nextcloud.md index 7ef3cca281f9e..15c1f2da2724b 100644 --- a/nixos/modules/services/web-apps/nextcloud.md +++ b/nixos/modules/services/web-apps/nextcloud.md @@ -132,7 +132,9 @@ Auto updates for Nextcloud apps can be enabled using Nextcloud supports [server-side encryption (SSE)](https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html). This is not an end-to-end encryption, but can be used to encrypt files that will be persisted to external storage such as S3. Please note that this won't work anymore when using OpenSSL 3 - for PHP's openssl extension because this is implemented using the legacy cipher RC4. + for PHP's openssl extension and **Nextcloud 25 or older** because this is implemented using the + legacy cipher RC4. For Nextcloud26 this isn't relevant anymore, because Nextcloud has an RC4 implementation + written in native PHP and thus doesn't need `ext-openssl` for that anymore. If [](#opt-system.stateVersion) is *above* `22.05`, this is disabled by default. To turn it on again and for further information please refer to [](#opt-services.nextcloud.enableBrokenCiphersForSSE). diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix index 76a0172747ffd..2824b7ee24562 100644 --- a/nixos/modules/services/web-apps/nextcloud.nix +++ b/nixos/modules/services/web-apps/nextcloud.nix @@ -204,7 +204,7 @@ in { package = mkOption { type = types.package; description = lib.mdDoc "Which package to use for the Nextcloud instance."; - relatedPackages = [ "nextcloud24" "nextcloud25" "nextcloud26" ]; + relatedPackages = [ "nextcloud25" "nextcloud26" ]; }; phpPackage = mkOption { type = types.package; @@ -712,6 +712,10 @@ in { See <https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/encryption_configuration.html#disabling-encryption> on how to achieve this. For more context, here is the implementing pull request: https://github.com/NixOS/nixpkgs/pull/198470 + '') + ++ (optional (cfg.enableBrokenCiphersForSSE && versionAtLeast cfg.package.version "26") '' + Nextcloud26 supports RC4 without requiring legacy OpenSSL, so + `services.nextcloud.enableBrokenCiphersForSSE` can be set to `false`. ''); services.nextcloud.package = with pkgs; diff --git a/nixos/modules/services/web-apps/plausible.nix b/nixos/modules/services/web-apps/plausible.nix index f64254d62524e..893dfa10acbc0 100644 --- a/nixos/modules/services/web-apps/plausible.nix +++ b/nixos/modules/services/web-apps/plausible.nix @@ -9,6 +9,8 @@ in { options.services.plausible = { enable = mkEnableOption (lib.mdDoc "plausible"); + package = mkPackageOptionMD pkgs "plausible" { }; + releaseCookiePath = mkOption { type = with types; either str path; description = lib.mdDoc '' @@ -180,12 +182,12 @@ in { services.epmd.enable = true; - environment.systemPackages = [ pkgs.plausible ]; + environment.systemPackages = [ cfg.package ]; systemd.services = mkMerge [ { plausible = { - inherit (pkgs.plausible.meta) description; + inherit (cfg.package.meta) description; documentation = [ "https://plausible.io/docs/self-hosting" ]; wantedBy = [ "multi-user.target" ]; after = optional cfg.database.clickhouse.setup "clickhouse.service" @@ -233,7 +235,7 @@ in { SMTP_USER_NAME = cfg.mail.smtp.user; }); - path = [ pkgs.plausible ] + path = [ cfg.package ] ++ optional cfg.database.postgres.setup config.services.postgresql.package; script = '' export CONFIG_DIR=$CREDENTIALS_DIRECTORY @@ -241,10 +243,10 @@ in { export RELEASE_COOKIE="$(< $CREDENTIALS_DIRECTORY/RELEASE_COOKIE )" # setup - ${pkgs.plausible}/createdb.sh - ${pkgs.plausible}/migrate.sh + ${cfg.package}/createdb.sh + ${cfg.package}/migrate.sh ${optionalString cfg.adminUser.activate '' - if ! ${pkgs.plausible}/init-admin.sh | grep 'already exists'; then + if ! ${cfg.package}/init-admin.sh | grep 'already exists'; then psql -d plausible <<< "UPDATE users SET email_verified=true;" fi ''} diff --git a/nixos/modules/services/web-servers/fcgiwrap.nix b/nixos/modules/services/web-servers/fcgiwrap.nix index f9c91fb35db23..3a57ef383065b 100644 --- a/nixos/modules/services/web-servers/fcgiwrap.nix +++ b/nixos/modules/services/web-servers/fcgiwrap.nix @@ -54,7 +54,7 @@ in { serviceConfig = { ExecStart = "${pkgs.fcgiwrap}/sbin/fcgiwrap -c ${builtins.toString cfg.preforkProcesses} ${ - if (cfg.socketType != "unix") then "-s ${cfg.socketType}:${cfg.socketAddress}" else "" + optionalString (cfg.socketType != "unix") "-s ${cfg.socketType}:${cfg.socketAddress}" }"; } // (if cfg.user != null && cfg.group != null then { User = cfg.user; diff --git a/nixos/modules/services/web-servers/lighttpd/default.nix b/nixos/modules/services/web-servers/lighttpd/default.nix index 811afe8e0af66..0438e12e7da82 100644 --- a/nixos/modules/services/web-servers/lighttpd/default.nix +++ b/nixos/modules/services/web-servers/lighttpd/default.nix @@ -64,7 +64,7 @@ let ]; maybeModuleString = moduleName: - if elem moduleName cfg.enableModules then ''"${moduleName}"'' else ""; + optionalString (elem moduleName cfg.enableModules) ''"${moduleName}"''; modulesIncludeString = concatStringsSep ",\n" (filter (x: x != "") (map maybeModuleString allKnownModules)); @@ -106,15 +106,15 @@ let static-file.exclude-extensions = ( ".fcgi", ".php", ".rb", "~", ".inc" ) index-file.names = ( "index.html" ) - ${if cfg.mod_userdir then '' + ${optionalString cfg.mod_userdir '' userdir.path = "public_html" - '' else ""} + ''} - ${if cfg.mod_status then '' + ${optionalString cfg.mod_status '' status.status-url = "/server-status" status.statistics-url = "/server-statistics" status.config-url = "/server-config" - '' else ""} + ''} ${cfg.extraConfig} ''; diff --git a/nixos/modules/services/web-servers/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 3d19186e1a9d3..1e6cb0d374053 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -318,7 +318,7 @@ let listenString = { addr, port, ssl, extraParameters ? [], ... }: # UDP listener for QUIC transport protocol. - (if ssl && vhost.quic then " + (optionalString (ssl && vhost.quic) (" listen ${addr}:${toString port} quic " + optionalString vhost.default "default_server " + optionalString vhost.reuseport "reuseport " @@ -326,7 +326,7 @@ let let inCompatibleParameters = [ "ssl" "proxy_protocol" "http2" ]; isCompatibleParameter = param: !(any (p: p == param) inCompatibleParameters); in filter isCompatibleParameter extraParameters)) - + ";" else "") + + ";")) + " listen ${addr}:${toString port} " diff --git a/nixos/modules/services/web-servers/tomcat.nix b/nixos/modules/services/web-servers/tomcat.nix index d8bfee547c79a..4d2c36287be69 100644 --- a/nixos/modules/services/web-servers/tomcat.nix +++ b/nixos/modules/services/web-servers/tomcat.nix @@ -234,11 +234,11 @@ in ln -sfn ${tomcat}/conf/$i ${cfg.baseDir}/conf/`basename $i` done - ${if cfg.extraConfigFiles != [] then '' + ${optionalString (cfg.extraConfigFiles != []) '' for i in ${toString cfg.extraConfigFiles}; do ln -sfn $i ${cfg.baseDir}/conf/`basename $i` done - '' else ""} + ''} # Create a modified catalina.properties file # Change all references from CATALINA_HOME to CATALINA_BASE and add support for shared libraries @@ -345,7 +345,7 @@ in # Symlink all the given web applications files or paths into the webapps/ directory # of this virtual host - for i in "${if virtualHost ? webapps then toString virtualHost.webapps else ""}"; do + for i in "${optionalString (virtualHost ? webapps) (toString virtualHost.webapps)}"; do if [ -f $i ]; then # If the given web application is a file, symlink it into the webapps/ directory ln -sfn $i ${cfg.baseDir}/virtualhosts/${virtualHost.name}/webapps/`basename $i` diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix index 73a864bb95fe8..38f932ffb4206 100644 --- a/nixos/modules/services/x11/desktop-managers/plasma5.nix +++ b/nixos/modules/services/x11/desktop-managers/plasma5.nix @@ -429,7 +429,8 @@ in dolphin-plugins ffmpegthumbs kdegraphics-thumbnailers - pkgs.kio-admin + kde-inotify-survey + kio-admin kio-extras ]; optionalPackages = [ |