diff options
Diffstat (limited to 'nixos/modules/services')
42 files changed, 1560 insertions, 1047 deletions
diff --git a/nixos/modules/services/backup/borgbackup.nix b/nixos/modules/services/backup/borgbackup.nix index bc2d79ac10ac..08a2967e9c7f 100644 --- a/nixos/modules/services/backup/borgbackup.nix +++ b/nixos/modules/services/backup/borgbackup.nix @@ -66,6 +66,7 @@ let ${mkKeepArgs cfg} \ ${optionalString (cfg.prune.prefix != null) "--glob-archives ${escapeShellArg "${cfg.prune.prefix}*"}"} \ $extraPruneArgs + borg compact $extraArgs $extraCompactArgs ${cfg.postPrune} ''); @@ -638,6 +639,15 @@ in { example = "--save-space"; }; + extraCompactArgs = mkOption { + type = types.str; + description = lib.mdDoc '' + Additional arguments for {command}`borg compact`. + Can also be set at runtime using `$extraCompactArgs`. + ''; + default = ""; + example = "--cleanup-commits"; + }; }; } )); diff --git a/nixos/modules/services/backup/borgmatic.nix b/nixos/modules/services/backup/borgmatic.nix index e7cd6ae4bb57..5ee036e68c7b 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/cluster/kubernetes/kubelet.nix b/nixos/modules/services/cluster/kubernetes/kubelet.nix index 8e935d621be4..eebacb3f3ef3 100644 --- a/nixos/modules/services/cluster/kubernetes/kubelet.nix +++ b/nixos/modules/services/cluster/kubernetes/kubelet.nix @@ -63,6 +63,7 @@ in (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "cadvisorPort" ] "") (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "allowPrivileged" ] "") (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "networkPlugin" ] "") + (mkRemovedOptionModule [ "services" "kubernetes" "kubelet" "containerRuntime" ] "") ]; ###### interface @@ -134,12 +135,6 @@ in }; }; - containerRuntime = mkOption { - description = lib.mdDoc "Which container runtime type to use"; - type = enum ["docker" "remote"]; - default = "remote"; - }; - containerRuntimeEndpoint = mkOption { description = lib.mdDoc "Endpoint at which to find the container runtime api interface/socket"; type = str; @@ -331,7 +326,6 @@ in ${optionalString (cfg.tlsKeyFile != null) "--tls-private-key-file=${cfg.tlsKeyFile}"} \ ${optionalString (cfg.verbosity != null) "--v=${toString cfg.verbosity}"} \ - --container-runtime=${cfg.containerRuntime} \ --container-runtime-endpoint=${cfg.containerRuntimeEndpoint} \ --cgroup-driver=systemd \ ${cfg.extraOpts} diff --git a/nixos/modules/services/computing/boinc/client.nix b/nixos/modules/services/computing/boinc/client.nix index 5fb715f4d779..1879fef9666f 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/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix index 5666199c4845..595374ea1e5b 100644 --- a/nixos/modules/services/continuous-integration/buildbot/master.nix +++ b/nixos/modules/services/continuous-integration/buildbot/master.nix @@ -8,7 +8,8 @@ let cfg = config.services.buildbot-master; opt = options.services.buildbot-master; - python = cfg.package.pythonModule; + package = pkgs.python3.pkgs.toPythonModule cfg.package; + python = package.pythonModule; escapeStr = escape [ "'" ]; @@ -212,10 +213,10 @@ in { package = mkOption { type = types.package; - default = pkgs.python3Packages.buildbot-full; - defaultText = literalExpression "pkgs.python3Packages.buildbot-full"; + default = pkgs.buildbot-full; + defaultText = literalExpression "pkgs.buildbot-full"; description = lib.mdDoc "Package to use for buildbot."; - example = literalExpression "pkgs.python3Packages.buildbot"; + example = literalExpression "pkgs.buildbot"; }; packages = mkOption { @@ -255,7 +256,7 @@ in { after = [ "network-online.target" ]; wantedBy = [ "multi-user.target" ]; path = cfg.packages ++ cfg.pythonPackages python.pkgs; - environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ cfg.package ])}/${python.sitePackages}"; + environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}"; preStart = '' mkdir -vp "${cfg.buildbotDir}" diff --git a/nixos/modules/services/continuous-integration/buildbot/worker.nix b/nixos/modules/services/continuous-integration/buildbot/worker.nix index 52c41c4a7584..7e78b8935f81 100644 --- a/nixos/modules/services/continuous-integration/buildbot/worker.nix +++ b/nixos/modules/services/continuous-integration/buildbot/worker.nix @@ -8,7 +8,8 @@ let cfg = config.services.buildbot-worker; opt = options.services.buildbot-worker; - python = cfg.package.pythonModule; + package = pkgs.python3.pkgs.toPythonModule cfg.package; + python = package.pythonModule; tacFile = pkgs.writeText "aur-buildbot-worker.tac" '' import os @@ -129,7 +130,7 @@ in { package = mkOption { type = types.package; - default = pkgs.python3Packages.buildbot-worker; + default = pkgs.buildbot-worker; defaultText = literalExpression "pkgs.python3Packages.buildbot-worker"; description = lib.mdDoc "Package to use for buildbot worker."; example = literalExpression "pkgs.python2Packages.buildbot-worker"; @@ -168,7 +169,7 @@ in { after = [ "network.target" "buildbot-master.service" ]; wantedBy = [ "multi-user.target" ]; path = cfg.packages; - environment.PYTHONPATH = "${python.withPackages (p: [ cfg.package ])}/${python.sitePackages}"; + environment.PYTHONPATH = "${python.withPackages (p: [ package ])}/${python.sitePackages}"; preStart = '' mkdir -vp "${cfg.buildbotDir}/info" diff --git a/nixos/modules/services/development/lorri.nix b/nixos/modules/services/development/lorri.nix index 8c64e3d9a560..74f56f5890fc 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/hardware/auto-cpufreq.nix b/nixos/modules/services/hardware/auto-cpufreq.nix index 9698e72eb31e..df7c01ae54ef 100644 --- a/nixos/modules/services/hardware/auto-cpufreq.nix +++ b/nixos/modules/services/hardware/auto-cpufreq.nix @@ -2,10 +2,26 @@ with lib; let cfg = config.services.auto-cpufreq; + cfgFilename = "auto-cpufreq.conf"; + cfgFile = format.generate cfgFilename cfg.settings; + + format = pkgs.formats.ini {}; in { options = { services.auto-cpufreq = { enable = mkEnableOption (lib.mdDoc "auto-cpufreq daemon"); + + settings = mkOption { + description = lib.mdDoc '' + Configuration for `auto-cpufreq`. + + See its [example configuration file] for supported settings. + [example configuration file]: https://github.com/AdnanHodzic/auto-cpufreq/blob/master/auto-cpufreq.conf-example + ''; + + default = {}; + type = types.submodule { freeformType = format.type; }; + }; }; }; @@ -18,6 +34,11 @@ in { # Workaround for https://github.com/NixOS/nixpkgs/issues/81138 wantedBy = [ "multi-user.target" ]; path = with pkgs; [ bash coreutils ]; + + serviceConfig.ExecStart = [ + "" + "${lib.getExe pkgs.auto-cpufreq} --config ${cfgFile}" + ]; }; }; }; diff --git a/nixos/modules/services/home-automation/esphome.nix b/nixos/modules/services/home-automation/esphome.nix new file mode 100644 index 000000000000..d7dbb6f0b90e --- /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/mail/maddy.nix b/nixos/modules/services/mail/maddy.nix index 5f3a9b56292d..d0b525bcb002 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/roundcube.nix b/nixos/modules/services/mail/roundcube.nix index 7b6d82219298..3aaec145930d 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/gitit.nix b/nixos/modules/services/misc/gitit.nix deleted file mode 100644 index 0fafa76b5487..000000000000 --- a/nixos/modules/services/misc/gitit.nix +++ /dev/null @@ -1,725 +0,0 @@ -{ config, lib, pkgs, ... }: - -with lib; - -let - - cfg = config.services.gitit; - - homeDir = "/var/lib/gitit"; - - toYesNo = b: if b then "yes" else "no"; - - gititShared = with cfg.haskellPackages; gitit + "/share/" + ghc.targetPrefix + ghc.haskellCompilerName + "/" + gitit.pname + "-" + gitit.version; - - gititWithPkgs = hsPkgs: extras: hsPkgs.ghcWithPackages (self: with self; [ gitit ] ++ (extras self)); - - gititSh = hsPkgs: extras: with pkgs; let - env = gititWithPkgs hsPkgs extras; - in writeScript "gitit" '' - #!${runtimeShell} - cd $HOME - export NIX_GHC="${env}/bin/ghc" - export NIX_GHCPKG="${env}/bin/ghc-pkg" - export NIX_GHC_DOCDIR="${env}/share/doc/ghc/html" - export NIX_GHC_LIBDIR=$( $NIX_GHC --print-libdir ) - ${env}/bin/gitit -f ${configFile} - ''; - - gititOptions = { - - enable = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc "Enable the gitit service."; - }; - - haskellPackages = mkOption { - default = pkgs.haskellPackages; - defaultText = literalExpression "pkgs.haskellPackages"; - example = literalExpression "pkgs.haskell.packages.ghc784"; - description = lib.mdDoc "haskellPackages used to build gitit and plugins."; - }; - - extraPackages = mkOption { - type = types.functionTo (types.listOf types.package); - default = self: []; - example = literalExpression '' - haskellPackages: [ - haskellPackages.wreq - ] - ''; - description = lib.mdDoc '' - Extra packages available to ghc when running gitit. The - value must be a function which receives the attrset defined - in {var}`haskellPackages` as the sole argument. - ''; - }; - - address = mkOption { - type = types.str; - default = "0.0.0.0"; - description = lib.mdDoc "IP address on which the web server will listen."; - }; - - port = mkOption { - type = types.int; - default = 5001; - description = lib.mdDoc "Port on which the web server will run."; - }; - - wikiTitle = mkOption { - type = types.str; - default = "Gitit!"; - description = lib.mdDoc "The wiki title."; - }; - - repositoryType = mkOption { - type = types.enum ["git" "darcs" "mercurial"]; - default = "git"; - description = lib.mdDoc "Specifies the type of repository used for wiki content."; - }; - - repositoryPath = mkOption { - type = types.path; - default = homeDir + "/wiki"; - description = lib.mdDoc '' - Specifies the path of the repository directory. If it does not - exist, gitit will create it on startup. - ''; - }; - - requireAuthentication = mkOption { - type = types.enum [ "none" "modify" "read" ]; - default = "modify"; - description = lib.mdDoc '' - If 'none', login is never required, and pages can be edited - anonymously. If 'modify', login is required to modify the wiki - (edit, add, delete pages, upload files). If 'read', login is - required to see any wiki pages. - ''; - }; - - authenticationMethod = mkOption { - type = types.enum [ "form" "http" "generic" "github" ]; - default = "form"; - description = lib.mdDoc '' - 'form' means that users will be logged in and registered using forms - in the gitit web interface. 'http' means that gitit will assume that - HTTP authentication is in place and take the logged in username from - the "Authorization" field of the HTTP request header (in addition, - the login/logout and registration links will be suppressed). - 'generic' means that gitit will assume that some form of - authentication is in place that directly sets REMOTE_USER to the name - of the authenticated user (e.g. mod_auth_cas on apache). 'rpx' means - that gitit will attempt to log in through https://rpxnow.com. This - requires that 'rpx-domain', 'rpx-key', and 'base-url' be set below, - and that 'curl' be in the system path. - ''; - }; - - userFile = mkOption { - type = types.path; - default = homeDir + "/gitit-users"; - description = lib.mdDoc '' - Specifies the path of the file containing user login information. If - it does not exist, gitit will create it (with an empty user list). - This file is not used if 'http' is selected for - authentication-method. - ''; - }; - - sessionTimeout = mkOption { - type = types.int; - default = 60; - description = lib.mdDoc '' - Number of minutes of inactivity before a session expires. - ''; - }; - - staticDir = mkOption { - type = types.path; - default = gititShared + "/data/static"; - description = lib.mdDoc '' - Specifies the path of the static directory (containing javascript, - css, and images). If it does not exist, gitit will create it and - populate it with required scripts, stylesheets, and images. - ''; - }; - - defaultPageType = mkOption { - type = types.enum [ "markdown" "rst" "latex" "html" "markdown+lhs" "rst+lhs" "latex+lhs" ]; - default = "markdown"; - description = lib.mdDoc '' - Specifies the type of markup used to interpret pages in the wiki. - Possible values are markdown, rst, latex, html, markdown+lhs, - rst+lhs, and latex+lhs. (the +lhs variants treat the input as - literate Haskell. See pandoc's documentation for more details.) If - Markdown is selected, pandoc's syntax extensions (for footnotes, - delimited code blocks, etc.) will be enabled. Note that pandoc's - restructuredtext parser is not complete, so some pages may not be - rendered correctly if rst is selected. The same goes for latex and - html. - ''; - }; - - math = mkOption { - type = types.enum [ "mathml" "raw" "mathjax" "jsmath" "google" ]; - default = "mathml"; - description = lib.mdDoc '' - Specifies how LaTeX math is to be displayed. Possible values are - mathml, raw, mathjax, jsmath, and google. If mathml is selected, - gitit will convert LaTeX math to MathML and link in a script, - MathMLinHTML.js, that allows the MathML to be seen in Gecko browsers, - IE + mathplayer, and Opera. In other browsers you may get a jumble of - characters. If raw is selected, the LaTeX math will be displayed as - raw LaTeX math. If mathjax is selected, gitit will link to the - remote mathjax script. If jsMath is selected, gitit will link to the - script /js/jsMath/easy/load.js, and will assume that jsMath has been - installed into the js/jsMath directory. This is the most portable - solution. If google is selected, the google chart API is called to - render the formula as an image. This requires a connection to google, - and might raise a technical or a privacy problem. - ''; - }; - - mathJaxScript = mkOption { - type = types.str; - default = "https://d3eoax9i5htok0.cloudfront.net/mathjax/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML"; - description = lib.mdDoc '' - Specifies the path to MathJax rendering script. You might want to - use your own MathJax script to render formulas without Internet - connection or if you want to use some special LaTeX packages. Note: - path specified there cannot be an absolute path to a script on your - hdd, instead you should run your (local if you wish) HTTP server - which will serve the MathJax.js script. You can easily (in four lines - of code) serve MathJax.js using - http://happstack.com/docs/crashcourse/FileServing.html Do not forget - the "http://" prefix (e.g. http://localhost:1234/MathJax.js). - ''; - }; - - showLhsBirdTracks = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Specifies whether to show Haskell code blocks in "bird style", with - "> " at the beginning of each line. - ''; - }; - - templatesDir = mkOption { - type = types.path; - default = gititShared + "/data/templates"; - description = lib.mdDoc '' - Specifies the path of the directory containing page templates. If it - does not exist, gitit will create it with default templates. Users - may wish to edit the templates to customize the appearance of their - wiki. The template files are HStringTemplate templates. Variables to - be interpolated appear between $\'s. Literal $\'s must be - backslash-escaped. - ''; - }; - - logFile = mkOption { - type = types.path; - default = homeDir + "/gitit.log"; - description = lib.mdDoc '' - Specifies the path of gitit's log file. If it does not exist, gitit - will create it. The log is in Apache combined log format. - ''; - }; - - logLevel = mkOption { - type = types.enum [ "DEBUG" "INFO" "NOTICE" "WARNING" "ERROR" "CRITICAL" "ALERT" "EMERGENCY" ]; - default = "ERROR"; - description = lib.mdDoc '' - Determines how much information is logged. Possible values (from - most to least verbose) are DEBUG, INFO, NOTICE, WARNING, ERROR, - CRITICAL, ALERT, EMERGENCY. - ''; - }; - - frontPage = mkOption { - type = types.str; - default = "Front Page"; - description = lib.mdDoc '' - Specifies which wiki page is to be used as the wiki's front page. - Gitit creates a default front page on startup, if one does not exist - already. - ''; - }; - - noDelete = mkOption { - type = types.str; - default = "Front Page, Help"; - description = lib.mdDoc '' - Specifies pages that cannot be deleted through the web interface. - (They can still be deleted directly using git or darcs.) A - comma-separated list of page names. Leave blank to allow every page - to be deleted. - ''; - }; - - noEdit = mkOption { - type = types.str; - default = "Help"; - description = lib.mdDoc '' - Specifies pages that cannot be edited through the web interface. - Leave blank to allow every page to be edited. - ''; - }; - - defaultSummary = mkOption { - type = types.str; - default = ""; - description = lib.mdDoc '' - Specifies text to be used in the change description if the author - leaves the "description" field blank. If default-summary is blank - (the default), the author will be required to fill in the description - field. - ''; - }; - - tableOfContents = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - Specifies whether to print a tables of contents (with links to - sections) on each wiki page. - ''; - }; - - plugins = mkOption { - type = with types; listOf str; - default = [ (gititShared + "/plugins/Dot.hs") ]; - description = lib.mdDoc '' - Specifies a list of plugins to load. Plugins may be specified either - by their path or by their module name. If the plugin name starts - with Gitit.Plugin., gitit will assume that the plugin is an installed - module and will not try to find a source file. - ''; - }; - - useCache = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Specifies whether to cache rendered pages. Note that if use-feed is - selected, feeds will be cached regardless of the value of use-cache. - ''; - }; - - cacheDir = mkOption { - type = types.path; - default = homeDir + "/cache"; - description = lib.mdDoc "Path where rendered pages will be cached."; - }; - - maxUploadSize = mkOption { - type = types.str; - default = "1000K"; - description = lib.mdDoc '' - Specifies an upper limit on the size (in bytes) of files uploaded - through the wiki's web interface. To disable uploads, set this to - 0K. This will result in the uploads link disappearing and the - _upload url becoming inactive. - ''; - }; - - maxPageSize = mkOption { - type = types.str; - default = "1000K"; - description = lib.mdDoc "Specifies an upper limit on the size (in bytes) of pages."; - }; - - debugMode = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc "Causes debug information to be logged while gitit is running."; - }; - - compressResponses = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc "Specifies whether HTTP responses should be compressed."; - }; - - mimeTypesFile = mkOption { - type = types.path; - default = "/etc/mime/types.info"; - description = lib.mdDoc '' - Specifies the path of a file containing mime type mappings. Each - line of the file should contain two fields, separated by whitespace. - The first field is the mime type, the second is a file extension. - For example: - ``` - video/x-ms-wmx wmx - ``` - If the file is not found, some simple defaults will be used. - ''; - }; - - useReCaptcha = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - If true, causes gitit to use the reCAPTCHA service - (http://recaptcha.net) to prevent bots from creating accounts. - ''; - }; - - reCaptchaPrivateKey = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specifies the private key for the reCAPTCHA service. To get - these, you need to create an account at http://recaptcha.net. - ''; - }; - - reCaptchaPublicKey = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specifies the public key for the reCAPTCHA service. To get - these, you need to create an account at http://recaptcha.net. - ''; - }; - - accessQuestion = mkOption { - type = types.str; - default = "What is the code given to you by Ms. X?"; - description = lib.mdDoc '' - Specifies a question that users must answer when they attempt to - create an account - ''; - }; - - accessQuestionAnswers = mkOption { - type = types.str; - default = "RED DOG, red dog"; - description = lib.mdDoc '' - Specifies a question that users must answer when they attempt to - create an account, along with a comma-separated list of acceptable - answers. This can be used to institute a rudimentary password for - signing up as a user on the wiki, or as an alternative to reCAPTCHA. - Example: - access-question: What is the code given to you by Ms. X? - access-question-answers: RED DOG, red dog - ''; - }; - - rpxDomain = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - Specifies the domain and key of your RPX account. The domain is just - the prefix of the complete RPX domain, so if your full domain is - 'https://foo.rpxnow.com/', use 'foo' as the value of rpx-domain. - ''; - }; - - rpxKey = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "RPX account access key."; - }; - - mailCommand = mkOption { - type = types.str; - default = "sendmail %s"; - description = lib.mdDoc '' - Specifies the command to use to send notification emails. '%s' will - be replaced by the destination email address. The body of the - message will be read from stdin. If this field is left blank, - password reset will not be offered. - ''; - }; - - resetPasswordMessage = mkOption { - type = types.lines; - default = '' - > From: gitit@$hostname$ - > To: $useremail$ - > Subject: Wiki password reset - > - > Hello $username$, - > - > To reset your password, please follow the link below: - > http://$hostname$:$port$$resetlink$ - > - > Regards - ''; - description = lib.mdDoc '' - Gives the text of the message that will be sent to the user should - she want to reset her password, or change other registration info. - The lines must be indented, and must begin with '>'. The initial - spaces and '> ' will be stripped off. $username$ will be replaced by - the user's username, $useremail$ by her email address, $hostname$ by - the hostname on which the wiki is running (as returned by the - hostname system call), $port$ by the port on which the wiki is - running, and $resetlink$ by the relative path of a reset link derived - from the user's existing hashed password. If your gitit wiki is being - proxied to a location other than the root path of $port$, you should - change the link to reflect this: for example, to - http://$hostname$/path/to/wiki$resetlink$ or - http://gitit.$hostname$$resetlink$ - ''; - }; - - useFeed = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Specifies whether an ATOM feed should be enabled (for the site and - for individual pages). - ''; - }; - - baseUrl = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc '' - The base URL of the wiki, to be used in constructing feed IDs and RPX - token_urls. Set this if useFeed is false or authentication-method - is 'rpx'. - ''; - }; - - absoluteUrls = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - Make wikilinks absolute with respect to the base-url. So, for - example, in a wiki served at the base URL '/wiki', on a page - Sub/Page, the wikilink `[Cactus]()` will produce a link to - '/wiki/Cactus' if absoluteUrls is true, and a relative link to - 'Cactus' (referring to '/wiki/Sub/Cactus') if absolute-urls is 'no'. - ''; - }; - - feedDays = mkOption { - type = types.int; - default = 14; - description = lib.mdDoc "Number of days to be included in feeds."; - }; - - feedRefreshTime = mkOption { - type = types.int; - default = 60; - description = lib.mdDoc "Number of minutes to cache feeds before refreshing."; - }; - - pdfExport = mkOption { - type = types.bool; - default = false; - description = lib.mdDoc '' - If true, PDF will appear in export options. PDF will be created using - pdflatex, which must be installed and in the path. Note that PDF - exports create significant additional server load. - ''; - }; - - pandocUserData = mkOption { - type = with types; nullOr path; - default = null; - description = lib.mdDoc '' - If a directory is specified, this will be searched for pandoc - customizations. These can include a templates/ directory for custom - templates for various export formats, an S5 directory for custom S5 - styles, and a reference.odt for ODT exports. If no directory is - specified, $HOME/.pandoc will be searched. See pandoc's README for - more information. - ''; - }; - - xssSanitize = mkOption { - type = types.bool; - default = true; - description = lib.mdDoc '' - If true, all HTML (including that produced by pandoc) is filtered - through xss-sanitize. Set to no only if you trust all of your users. - ''; - }; - - oauthClientId = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "OAuth client ID"; - }; - - oauthClientSecret = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "OAuth client secret"; - }; - - oauthCallback = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "OAuth callback URL"; - }; - - oauthAuthorizeEndpoint = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "OAuth authorize endpoint"; - }; - - oauthAccessTokenEndpoint = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "OAuth access token endpoint"; - }; - - githubOrg = mkOption { - type = with types; nullOr str; - default = null; - description = lib.mdDoc "Github organization"; - }; - }; - - configFile = pkgs.writeText "gitit.conf" '' - address: ${cfg.address} - port: ${toString cfg.port} - wiki-title: ${cfg.wikiTitle} - repository-type: ${cfg.repositoryType} - repository-path: ${cfg.repositoryPath} - require-authentication: ${cfg.requireAuthentication} - authentication-method: ${cfg.authenticationMethod} - user-file: ${cfg.userFile} - session-timeout: ${toString cfg.sessionTimeout} - static-dir: ${cfg.staticDir} - default-page-type: ${cfg.defaultPageType} - math: ${cfg.math} - mathjax-script: ${cfg.mathJaxScript} - show-lhs-bird-tracks: ${toYesNo cfg.showLhsBirdTracks} - templates-dir: ${cfg.templatesDir} - log-file: ${cfg.logFile} - log-level: ${cfg.logLevel} - front-page: ${cfg.frontPage} - no-delete: ${cfg.noDelete} - no-edit: ${cfg.noEdit} - default-summary: ${cfg.defaultSummary} - table-of-contents: ${toYesNo cfg.tableOfContents} - plugins: ${concatStringsSep "," cfg.plugins} - use-cache: ${toYesNo cfg.useCache} - cache-dir: ${cfg.cacheDir} - max-upload-size: ${cfg.maxUploadSize} - max-page-size: ${cfg.maxPageSize} - debug-mode: ${toYesNo cfg.debugMode} - compress-responses: ${toYesNo cfg.compressResponses} - mime-types-file: ${cfg.mimeTypesFile} - use-recaptcha: ${toYesNo cfg.useReCaptcha} - recaptcha-private-key: ${toString cfg.reCaptchaPrivateKey} - recaptcha-public-key: ${toString cfg.reCaptchaPublicKey} - access-question: ${cfg.accessQuestion} - access-question-answers: ${cfg.accessQuestionAnswers} - rpx-domain: ${toString cfg.rpxDomain} - rpx-key: ${toString cfg.rpxKey} - mail-command: ${cfg.mailCommand} - reset-password-message: ${cfg.resetPasswordMessage} - use-feed: ${toYesNo cfg.useFeed} - base-url: ${toString cfg.baseUrl} - absolute-urls: ${toYesNo cfg.absoluteUrls} - feed-days: ${toString cfg.feedDays} - feed-refresh-time: ${toString cfg.feedRefreshTime} - pdf-export: ${toYesNo cfg.pdfExport} - pandoc-user-data: ${toString cfg.pandocUserData} - xss-sanitize: ${toYesNo cfg.xssSanitize} - - [Github] - oauthclientid: ${toString cfg.oauthClientId} - oauthclientsecret: ${toString cfg.oauthClientSecret} - oauthcallback: ${toString cfg.oauthCallback} - oauthauthorizeendpoint: ${toString cfg.oauthAuthorizeEndpoint} - oauthaccesstokenendpoint: ${toString cfg.oauthAccessTokenEndpoint} - github-org: ${toString cfg.githubOrg} - ''; - -in - -{ - - options.services.gitit = gititOptions; - - config = mkIf cfg.enable { - - users.users.gitit = { - group = config.users.groups.gitit.name; - description = "Gitit user"; - home = homeDir; - createHome = true; - uid = config.ids.uids.gitit; - }; - - users.groups.gitit.gid = config.ids.gids.gitit; - - systemd.services.gitit = let - uid = toString config.ids.uids.gitit; - gid = toString config.ids.gids.gitit; - in { - description = "Git and Pandoc Powered Wiki"; - after = [ "network.target" ]; - wantedBy = [ "multi-user.target" ]; - path = with pkgs; [ curl ] - ++ optional cfg.pdfExport texlive.combined.scheme-basic - ++ optional (cfg.repositoryType == "darcs") darcs - ++ optional (cfg.repositoryType == "mercurial") mercurial - ++ optional (cfg.repositoryType == "git") git; - - preStart = let - gm = "gitit@${config.networking.hostName}"; - in - with cfg; '' - chown ${uid}:${gid} -R ${homeDir} - for dir in ${repositoryPath} ${staticDir} ${templatesDir} ${cacheDir} - do - if [ ! -d $dir ] - then - mkdir -p $dir - find $dir -type d -exec chmod 0750 {} + - find $dir -type f -exec chmod 0640 {} + - fi - done - cd ${repositoryPath} - ${ - if repositoryType == "darcs" then - '' - if [ ! -d _darcs ] - then - darcs initialize - echo "${gm}" > _darcs/prefs/email - '' - else if repositoryType == "mercurial" then - '' - if [ ! -d .hg ] - then - hg init - cat >> .hg/hgrc <<NAMED -[ui] -username = gitit ${gm} -NAMED - '' - else - '' - if [ ! -d .git ] - then - git init - git config user.email "${gm}" - git config user.name "gitit" - ''} - chown ${uid}:${gid} -R ${repositoryPath} - fi - cd - - ''; - - serviceConfig = { - User = config.users.users.gitit.name; - Group = config.users.groups.gitit.name; - ExecStart = with cfg; gititSh haskellPackages extraPackages; - }; - }; - }; -} diff --git a/nixos/modules/services/misc/gpsd.nix b/nixos/modules/services/misc/gpsd.nix index 9b03b6f9662e..ce0f9bb3ba28 100644 --- a/nixos/modules/services/misc/gpsd.nix +++ b/nixos/modules/services/misc/gpsd.nix @@ -1,4 +1,4 @@ -{ config, lib, pkgs, ... }: +{ config, lib, pkgs, utils, ... }: with lib; @@ -8,12 +8,15 @@ let gid = config.ids.gids.gpsd; cfg = config.services.gpsd; -in - -{ +in { ###### interface + imports = [ + (lib.mkRemovedOptionModule [ "services" "gpsd" "device" ] + "Use `services.gpsd.devices` instead.") + ]; + options = { services.gpsd = { @@ -26,13 +29,17 @@ in ''; }; - device = mkOption { - type = types.str; - default = "/dev/ttyUSB0"; + devices = mkOption { + type = types.listOf types.str; + default = [ "/dev/ttyUSB0" ]; description = lib.mdDoc '' - A device may be a local serial device for GPS input, or a URL of the form: - `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]` - in which case it specifies an input source for DGPS or ntrip data. + List of devices that `gpsd` should subscribe to. + + A device may be a local serial device for GPS input, or a + URL of the form: + `[{dgpsip|ntrip}://][user:passwd@]host[:port][/stream]` in + which case it specifies an input source for DGPS or ntrip + data. ''; }; @@ -89,17 +96,16 @@ in }; - ###### implementation config = mkIf cfg.enable { - users.users.gpsd = - { inherit uid; - group = "gpsd"; - description = "gpsd daemon user"; - home = "/var/empty"; - }; + users.users.gpsd = { + inherit uid; + group = "gpsd"; + description = "gpsd daemon user"; + home = "/var/empty"; + }; users.groups.gpsd = { inherit gid; }; @@ -109,13 +115,15 @@ in after = [ "network.target" ]; serviceConfig = { Type = "forking"; - ExecStart = '' + ExecStart = let + devices = utils.escapeSystemdExecArgs cfg.devices; + in '' ${pkgs.gpsd}/sbin/gpsd -D "${toString cfg.debugLevel}" \ -S "${toString cfg.port}" \ ${optionalString cfg.readonly "-b"} \ ${optionalString cfg.nowait "-n"} \ ${optionalString cfg.listenany "-G"} \ - "${cfg.device}" + ${devices} ''; }; }; diff --git a/nixos/modules/services/misc/pufferpanel.nix b/nixos/modules/services/misc/pufferpanel.nix new file mode 100644 index 000000000000..78ec35646907 --- /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/monitoring/grafana-agent.nix b/nixos/modules/services/monitoring/grafana-agent.nix index 270d888afb78..b7761c34fe51 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/smartctl.nix b/nixos/modules/services/monitoring/prometheus/exporters/smartctl.nix index 0c5648c14149..50e1321a1e9c 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}" - ]; + 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 0cb0e126d4c5..2537bb1b8d80 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/server.nix b/nixos/modules/services/network-filesystems/openafs/server.nix index 1c615d3bfb64..ad0fd7835670 100644 --- a/nixos/modules/services/network-filesystems/openafs/server.nix +++ b/nixos/modules/services/network-filesystems/openafs/server.nix @@ -4,7 +4,8 @@ with import ./lib.nix { inherit config lib pkgs; }; let - inherit (lib) concatStringsSep literalExpression mkIf mkOption optionalString types; + inherit (lib) concatStringsSep literalExpression mkIf mkOption mkEnableOption + optionalString types; bosConfig = pkgs.writeText "BosConfig" ('' restrictmode 1 @@ -24,9 +25,15 @@ let parm ${openafsSrv}/libexec/openafs/salvageserver ${cfg.roles.fileserver.salvageserverArgs} parm ${openafsSrv}/libexec/openafs/dasalvager ${cfg.roles.fileserver.salvagerArgs} end - '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable) '' + '') + (optionalString (cfg.roles.database.enable && cfg.roles.backup.enable && (!cfg.roles.backup.enableFabs)) '' bnode simple buserver 1 - parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString (cfg.roles.backup.cellServDB != []) "-cellservdb /etc/openafs/backup/"} + parm ${openafsSrv}/libexec/openafs/buserver ${cfg.roles.backup.buserverArgs} ${optionalString useBuCellServDB "-cellservdb /etc/openafs/backup/"} + end + '') + (optionalString (cfg.roles.database.enable && + cfg.roles.backup.enable && + cfg.roles.backup.enableFabs) '' + bnode simple buserver 1 + parm ${lib.getBin pkgs.fabs}/bin/fabsys server --config ${fabsConfFile} ${cfg.roles.backup.fabsArgs} end '')); @@ -34,12 +41,27 @@ let pkgs.writeText "NetInfo" ((concatStringsSep "\nf " cfg.advertisedAddresses) + "\n") else null; - buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB); + buCellServDB = pkgs.writeText "backup-cellServDB-${cfg.cellName}" + (mkCellServDB cfg.cellName cfg.roles.backup.cellServDB); + + useBuCellServDB = (cfg.roles.backup.cellServDB != []) && (!cfg.roles.backup.enableFabs); cfg = config.services.openafsServer; udpSizeStr = toString cfg.udpPacketSize; + fabsConfFile = pkgs.writeText "fabs.yaml" (builtins.toJSON ({ + afs = { + aklog = cfg.package + "/bin/aklog"; + cell = cfg.cellName; + dumpscan = cfg.package + "/bin/afsdump_scan"; + fs = cfg.package + "/bin/fs"; + pts = cfg.package + "/bin/pts"; + vos = cfg.package + "/bin/vos"; + }; + k5start.command = (lib.getBin pkgs.kstart) + "/bin/k5start"; + } // cfg.roles.backup.fabsExtraConfig)); + in { options = { @@ -80,8 +102,8 @@ in { }; package = mkOption { - default = pkgs.openafs.server or pkgs.openafs; - defaultText = literalExpression "pkgs.openafs.server or pkgs.openafs"; + default = pkgs.openafs; + defaultText = literalExpression "pkgs.openafs"; type = types.package; description = lib.mdDoc "OpenAFS package for the server binaries"; }; @@ -154,16 +176,20 @@ in { }; backup = { - enable = mkOption { - default = false; - type = types.bool; - description = lib.mdDoc '' - Backup server role. Use in conjunction with the - `database` role to maintain the Backup - Database. Normally only used in conjunction with tape storage - or IBM's Tivoli Storage Manager. - ''; - }; + enable = mkEnableOption (lib.mdDoc '' + Backup server role. When using OpenAFS built-in buserver, use in conjunction with the + `database` role to maintain the Backup + Database. Normally only used in conjunction with tape storage + or IBM's Tivoli Storage Manager. + + For a modern backup server, enable this role and see + {option}`enableFabs`. + ''); + + enableFabs = mkEnableOption (lib.mdDoc '' + FABS, the flexible AFS backup system. It stores volumes as dump files, relying on other + pre-existing backup solutions for handling them. + ''); buserverArgs = mkOption { default = ""; @@ -181,6 +207,30 @@ in { other database server machines. ''; }; + + fabsArgs = mkOption { + default = ""; + type = types.str; + description = lib.mdDoc '' + Arguments to the fabsys process. See + {manpage}`fabsys_server(1)` and + {manpage}`fabsys_config(1)`. + ''; + }; + + fabsExtraConfig = mkOption { + default = {}; + type = types.attrs; + description = lib.mdDoc '' + Additional configuration parameters for the FABS backup server. + ''; + example = literalExpression '' + { + afs.localauth = true; + afs.keytab = config.sops.secrets.fabsKeytab.path; + } + ''; + }; }; }; @@ -239,7 +289,7 @@ in { mode = "0644"; }; buCellServDB = { - enable = (cfg.roles.backup.cellServDB != []); + enable = useBuCellServDB; text = mkCellServDB cfg.cellName cfg.roles.backup.cellServDB; target = "openafs/backup/CellServDB"; }; @@ -257,7 +307,7 @@ in { preStart = '' mkdir -m 0755 -p /var/openafs ${optionalString (netInfo != null) "cp ${netInfo} /var/openafs/netInfo"} - ${optionalString (cfg.roles.backup.cellServDB != []) "cp ${buCellServDB}"} + ${optionalString useBuCellServDB "cp ${buCellServDB}"} ''; serviceConfig = { ExecStart = "${openafsBin}/bin/bosserver -nofork"; diff --git a/nixos/modules/services/networking/ddclient.nix b/nixos/modules/services/networking/ddclient.nix index 5e6f5217c0ce..7caee8a8eb3d 100644 --- a/nixos/modules/services/networking/ddclient.nix +++ b/nixos/modules/services/networking/ddclient.nix @@ -29,9 +29,9 @@ let configFile = if (cfg.configFile != null) then cfg.configFile else configFile'; preStart = '' - install ${configFile} /run/${RuntimeDirectory}/ddclient.conf + install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then '' - install ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key + install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key '' else if (cfg.passwordFile != null) then '' "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf" '' else '' diff --git a/nixos/modules/services/networking/dhcpd.nix b/nixos/modules/services/networking/dhcpd.nix index 0bd5e4ef5535..a981a255c3ee 100644 --- a/nixos/modules/services/networking/dhcpd.nix +++ b/nixos/modules/services/networking/dhcpd.nix @@ -218,6 +218,13 @@ in systemd.services = dhcpdService "4" cfg4 // dhcpdService "6" cfg6; + warnings = [ + '' + The dhcpd4 and dhcpd6 modules will be removed from NixOS 23.11, because ISC DHCP reached its end of life. + See https://www.isc.org/blogs/isc-dhcp-eol/ for details. + Please switch to a different implementation like kea, systemd-networkd or dnsmasq. + '' + ]; }; } diff --git a/nixos/modules/services/networking/go-neb.nix b/nixos/modules/services/networking/go-neb.nix index 8c04542c47cc..b65bb5f548ee 100644 --- a/nixos/modules/services/networking/go-neb.nix +++ b/nixos/modules/services/networking/go-neb.nix @@ -60,13 +60,12 @@ in { serviceConfig = { ExecStartPre = lib.optional (cfg.secretFile != null) - (pkgs.writeShellScript "pre-start" '' + ("+" + pkgs.writeShellScript "pre-start" '' umask 077 export $(xargs < ${cfg.secretFile}) ${pkgs.envsubst}/bin/envsubst -i "${configFile}" > ${finalConfigFile} chown go-neb ${finalConfigFile} ''); - PermissionsStartOnly = true; RuntimeDirectory = "go-neb"; ExecStart = "${pkgs.go-neb}/bin/go-neb"; User = "go-neb"; diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix new file mode 100644 index 000000000000..6df630c1f194 --- /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/peroxide.nix b/nixos/modules/services/networking/peroxide.nix index 6cac4bf2f89a..885ee1d96cd0 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 c2c2a370cb00..19ab3f1aa48c 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/sshd.nix b/nixos/modules/services/networking/ssh/sshd.nix index 9982da304a48..89ddf8215299 100644 --- a/nixos/modules/services/networking/ssh/sshd.nix +++ b/nixos/modules/services/networking/ssh/sshd.nix @@ -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 '' diff --git a/nixos/modules/services/networking/wgautomesh.nix b/nixos/modules/services/networking/wgautomesh.nix new file mode 100644 index 000000000000..93227a9b625d --- /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 440b617f60a3..067d5df48725 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/printing/cupsd.nix b/nixos/modules/services/printing/cupsd.nix index 9ac89e057620..f6a23fb900f0 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 a843c44dbb5f..e1f7365d951a 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/fail2ban.nix b/nixos/modules/services/security/fail2ban.nix index ead24d147071..1962d3f59c9f 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; @@ -320,6 +327,9 @@ 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 diff --git a/nixos/modules/services/security/kanidm.nix b/nixos/modules/services/security/kanidm.nix index 5583c39368f7..2f19decb5cb1 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/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix index 85e9509bcc82..89157b460b9a 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/video/rtsp-simple-server.nix b/nixos/modules/services/video/mediamtx.nix index 2dd62edab787..18a9e3d5fe30 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/video/v4l2-relayd.nix b/nixos/modules/services/video/v4l2-relayd.nix new file mode 100644 index 000000000000..2a9dbe00158f --- /dev/null +++ b/nixos/modules/services/video/v4l2-relayd.nix @@ -0,0 +1,199 @@ +{ config, lib, pkgs, utils, ... }: +let + + inherit (lib) attrValues concatStringsSep filterAttrs length listToAttrs literalExpression + makeSearchPathOutput mkEnableOption mkIf mkOption nameValuePair optionals types; + inherit (utils) escapeSystemdPath; + + cfg = config.services.v4l2-relayd; + + kernelPackages = config.boot.kernelPackages; + + gst = (with pkgs.gst_all_1; [ + gst-plugins-bad + gst-plugins-base + gst-plugins-good + gstreamer.out + ]); + + instanceOpts = { name, ... }: { + options = { + enable = mkEnableOption (lib.mdDoc "this v4l2-relayd instance"); + + name = mkOption { + type = types.str; + default = name; + description = lib.mdDoc '' + The name of the instance. + ''; + }; + + cardLabel = mkOption { + type = types.str; + description = lib.mdDoc '' + The name the camera will show up as. + ''; + }; + + extraPackages = mkOption { + type = with types; listOf package; + default = [ ]; + description = lib.mdDoc '' + Extra packages to add to {env}`GST_PLUGIN_PATH` for the instance. + ''; + }; + + input = { + pipeline = mkOption { + type = types.str; + description = lib.mdDoc '' + The gstreamer-pipeline to use for the input-stream. + ''; + }; + + format = mkOption { + type = types.str; + default = "YUY2"; + description = lib.mdDoc '' + The video-format to read from input-stream. + ''; + }; + + width = mkOption { + type = types.ints.positive; + default = 1280; + description = lib.mdDoc '' + The width to read from input-stream. + ''; + }; + + height = mkOption { + type = types.ints.positive; + default = 720; + description = lib.mdDoc '' + The height to read from input-stream. + ''; + }; + + framerate = mkOption { + type = types.ints.positive; + default = 30; + description = lib.mdDoc '' + The framerate to read from input-stream. + ''; + }; + }; + + output = { + format = mkOption { + type = types.str; + default = "YUY2"; + description = lib.mdDoc '' + The video-format to write to output-stream. + ''; + }; + }; + + }; + }; + +in +{ + + options.services.v4l2-relayd = { + + instances = mkOption { + type = with types; attrsOf (submodule instanceOpts); + default = { }; + example = literalExpression '' + { + example = { + cardLabel = "Example card"; + input.pipeline = "videotestsrc"; + }; + } + ''; + description = lib.mdDoc '' + v4l2-relayd instances to be created. + ''; + }; + + }; + + config = + let + + mkInstanceService = instance: { + description = "Streaming relay for v4l2loopback using GStreamer"; + + after = [ "modprobe@v4l2loopback.service" "systemd-logind.service" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + Restart = "always"; + PrivateNetwork = true; + PrivateTmp = true; + LimitNPROC = 1; + }; + + environment = { + GST_PLUGIN_PATH = makeSearchPathOutput "lib" "lib/gstreamer-1.0" (gst ++ instance.extraPackages); + V4L2_DEVICE_FILE = "/run/v4l2-relayd-${instance.name}/device"; + }; + + script = + let + appsrcOptions = concatStringsSep "," [ + "caps=video/x-raw" + "format=${instance.input.format}" + "width=${toString instance.input.width}" + "height=${toString instance.input.height}" + "framerate=${toString instance.input.framerate}/1" + ]; + + outputPipeline = [ + "appsrc name=appsrc ${appsrcOptions}" + "videoconvert" + ] ++ optionals (instance.input.format != instance.output.format) [ + "video/x-raw,format=${instance.output.format}" + "queue" + ] ++ [ "v4l2sink name=v4l2sink device=$(cat $V4L2_DEVICE_FILE)" ]; + in + '' + exec ${pkgs.v4l2-relayd}/bin/v4l2-relayd -i "${instance.input.pipeline}" -o "${concatStringsSep " ! " outputPipeline}" + ''; + + preStart = '' + mkdir -p $(dirname $V4L2_DEVICE_FILE) + ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl add -x 1 -n "${instance.cardLabel}" > $V4L2_DEVICE_FILE + ''; + + postStop = '' + ${kernelPackages.v4l2loopback.bin}/bin/v4l2loopback-ctl delete $(cat $V4L2_DEVICE_FILE) + rm -rf $(dirname $V4L2_DEVICE_FILE) + ''; + }; + + mkInstanceServices = instances: listToAttrs (map + (instance: + nameValuePair "v4l2-relayd-${escapeSystemdPath instance.name}" (mkInstanceService instance) + ) + instances); + + enabledInstances = attrValues (filterAttrs (n: v: v.enable) cfg.instances); + + in + { + + boot = mkIf ((length enabledInstances) > 0) { + extraModulePackages = [ kernelPackages.v4l2loopback ]; + kernelModules = [ "v4l2loopback" ]; + }; + + systemd.services = mkInstanceServices enabledInstances; + + }; + + meta.maintainers = with lib.maintainers; [ betaboon ]; +} 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 a61aa445f82c..000000000000 --- 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/mastodon.nix b/nixos/modules/services/web-apps/mastodon.nix index 3e1286dc475d..cf5329be489c 100644 --- a/nixos/modules/services/web-apps/mastodon.nix +++ b/nixos/modules/services/web-apps/mastodon.nix @@ -587,6 +587,13 @@ in { <option>services.mastodon.smtp.authenticate</option> is enabled. ''; } + { + assertion = 1 == builtins.length + (lib.mapAttrsToList + (_: v: builtins.elem "scheduler" v.jobClasses || v.jobClasses == [ ]) + cfg.sidekiqProcesses); + message = "There must be one and only one Sidekiq queue in services.mastodon.sidekiqProcesses with jobClass \"scheduler\"."; + } ]; environment.systemPackages = [ mastodonTootctl ]; diff --git a/nixos/modules/services/web-apps/monica.nix b/nixos/modules/services/web-apps/monica.nix new file mode 100644 index 000000000000..442044fedb14 --- /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 5f8d9c5b15f4..b617e9a59379 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/plausible.nix b/nixos/modules/services/web-apps/plausible.nix index f64254d62524..893dfa10acbc 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/nginx/default.nix b/nixos/modules/services/web-servers/nginx/default.nix index 7811876369b3..4f97fe752ef7 100644 --- a/nixos/modules/services/web-servers/nginx/default.nix +++ b/nixos/modules/services/web-servers/nginx/default.nix @@ -31,6 +31,7 @@ let # Mime.types values are taken from brotli sample configuration - https://github.com/google/ngx_brotli # and Nginx Server Configs - https://github.com/h5bp/server-configs-nginx + # "text/html" is implicitly included in {brotli,gzip,zstd}_types compressMimeTypes = [ "application/atom+xml" "application/geo+json" @@ -55,7 +56,6 @@ let "text/calendar" "text/css" "text/csv" - "text/html" "text/javascript" "text/markdown" "text/plain" diff --git a/nixos/modules/services/x11/desktop-managers/pantheon.nix b/nixos/modules/services/x11/desktop-managers/pantheon.nix index 4d0296c8254c..ab4d75b10134 100644 --- a/nixos/modules/services/x11/desktop-managers/pantheon.nix +++ b/nixos/modules/services/x11/desktop-managers/pantheon.nix @@ -288,7 +288,7 @@ in elementary-music elementary-photos elementary-screenshot - # elementary-tasks + elementary-tasks elementary-terminal elementary-videos epiphany diff --git a/nixos/modules/services/x11/desktop-managers/plasma5.nix b/nixos/modules/services/x11/desktop-managers/plasma5.nix index 73a864bb95fe..38f932ffb420 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 = [ |