about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2311.section.md8
-rw-r--r--nixos/doc/manual/release-notes/rl-2405.section.md9
-rw-r--r--nixos/modules/i18n/input-method/ibus.nix2
-rw-r--r--nixos/modules/installer/tools/tools.nix9
-rw-r--r--nixos/modules/module-list.nix10
-rw-r--r--nixos/modules/profiles/macos-builder.nix13
-rw-r--r--nixos/modules/programs/environment.nix2
-rw-r--r--nixos/modules/programs/mininet.nix33
-rw-r--r--nixos/modules/services/computing/foldingathome/client.nix4
-rw-r--r--nixos/modules/services/databases/redis.nix4
-rw-r--r--nixos/modules/services/development/athens.md52
-rw-r--r--nixos/modules/services/development/athens.nix936
-rw-r--r--nixos/modules/services/matrix/maubot.md103
-rw-r--r--nixos/modules/services/matrix/maubot.nix459
-rw-r--r--nixos/modules/services/misc/forgejo.md2
-rw-r--r--nixos/modules/services/misc/sourcehut/default.nix5
-rw-r--r--nixos/modules/services/networking/x2goserver.nix3
-rw-r--r--nixos/modules/services/security/clamav.nix3
-rw-r--r--nixos/modules/system/boot/unl0kr.nix89
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix12
-rw-r--r--nixos/tests/nixops/default.nix6
-rw-r--r--nixos/tests/systemd-initrd-luks-unl0kr.nix75
-rw-r--r--nixos/tests/systemd-timesyncd.nix15
24 files changed, 1813 insertions, 42 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2311.section.md b/nixos/doc/manual/release-notes/rl-2311.section.md
index 7be39a69f5b0..5df8473babae 100644
--- a/nixos/doc/manual/release-notes/rl-2311.section.md
+++ b/nixos/doc/manual/release-notes/rl-2311.section.md
@@ -79,6 +79,8 @@
 
 * [NS-USBLoader](https://github.com/developersu/ns-usbloader/), an all-in-one tool for managing Nintendo Switch homebrew. Available as [programs.ns-usbloader](#opt-programs.ns-usbloader.enable).
 
+- [athens](https://github.com/gomods/athens), a Go module datastore and proxy. Available as [services.athens](#opt-services.athens.enable).
+
 - [Mobilizon](https://joinmobilizon.org/), a Fediverse platform for publishing events.
 
 - [Anuko Time Tracker](https://github.com/anuko/timetracker), a simple, easy to use, open source time tracking system. Available as [services.anuko-time-tracker](#opt-services.anuko-time-tracker.enable).
@@ -621,6 +623,9 @@ The module update takes care of the new config syntax and the data itself (user
 
 - `python3.pkgs.flitBuildHook` has been removed. Use `flit-core` and `format = "pyproject"` instead.
 
+-  Now `magma` defaults to `magma-hip` instead of `magma-cuda`. It also
+   respects the `config.cudaSupport` and `config.rocmSupport` options.
+
 - The `extend` function of `llvmPackages` has been removed due it coming from the `tools` attrset thus only extending the `tool` attrset. A possible replacement is to construct the set from `libraries` and `tools`, or patch nixpkgs.
 
 - The `qemu-vm.nix` module now supports disabling overriding `fileSystems` with
@@ -660,7 +665,8 @@ The module update takes care of the new config syntax and the data itself (user
   designed to be easy and safe to use.
 
   This aims to be a replacement for `lib.sources`-based filtering.
-  To learn more about it, see [the tutorial](https://nix.dev/tutorials/file-sets).
+  To learn more about it, see [the blog post](https://www.tweag.io/blog/2023-11-28-file-sets/)
+  or [the tutorial](https://nix.dev/tutorials/file-sets).
 
 - [`lib.gvariant`](https://nixos.org/manual/nixpkgs/unstable#sec-functions-library-gvariant):
   A partial and basic implementation of GVariant formatted strings.
diff --git a/nixos/doc/manual/release-notes/rl-2405.section.md b/nixos/doc/manual/release-notes/rl-2405.section.md
index e2f99c20cfc8..b6b343145d78 100644
--- a/nixos/doc/manual/release-notes/rl-2405.section.md
+++ b/nixos/doc/manual/release-notes/rl-2405.section.md
@@ -14,16 +14,19 @@ In addition to numerous new and upgraded packages, this release has the followin
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
-- Create the first release note entry in this section!
+- [maubot](https://github.com/maubot/maubot), a plugin-based Matrix bot framework. Available as [services.maubot](#opt-services.maubot.enable).
 
 ## Backward Incompatibilities {#sec-release-24.05-incompatibilities}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
-- Create the first release note entry in this section!
+- `mkosi` was updated to v19. Parts of the user interface have changed. Consult the
+  [release notes](https://github.com/systemd/mkosi/releases/tag/v19) for a list of changes.
 
 ## Other Notable Changes {#sec-release-24.05-notable-changes}
 
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
-- Create the first release note entry in this section!
+- Programs written in [Nim](https://nim-lang.org/) are built with libraries selected by lockfiles.
+  The `nimPackages` and `nim2Packages` sets have been removed.
+  See https://nixos.org/manual/nixpkgs/unstable#nim for more information.
diff --git a/nixos/modules/i18n/input-method/ibus.nix b/nixos/modules/i18n/input-method/ibus.nix
index 2a35afad2ac7..a81ce828b13d 100644
--- a/nixos/modules/i18n/input-method/ibus.nix
+++ b/nixos/modules/i18n/input-method/ibus.nix
@@ -47,7 +47,7 @@ in
       panel = mkOption {
         type = with types; nullOr path;
         default = null;
-        example = literalExpression ''"''${pkgs.plasma5Packages.plasma-desktop}/lib/libexec/kimpanel-ibus-panel"'';
+        example = literalExpression ''"''${pkgs.plasma5Packages.plasma-desktop}/libexec/kimpanel-ibus-panel"'';
         description = lib.mdDoc "Replace the IBus panel with another panel.";
       };
     };
diff --git a/nixos/modules/installer/tools/tools.nix b/nixos/modules/installer/tools/tools.nix
index 15e10128ac9a..9ccc76a82c95 100644
--- a/nixos/modules/installer/tools/tools.nix
+++ b/nixos/modules/installer/tools/tools.nix
@@ -130,7 +130,7 @@ in
     '';
   };
 
-  config = lib.mkIf (config.nix.enable && !config.system.disableInstallerTools) {
+  config = lib.mkMerge [ (lib.mkIf (config.nix.enable && !config.system.disableInstallerTools) {
 
     system.nixos-generate-config.configuration = mkDefault ''
       # Edit this configuration file to define what should be installed on
@@ -257,10 +257,13 @@ in
 
     documentation.man.man-db.skipPackages = [ nixos-version ];
 
+  })
+
+  # These may be used in auxiliary scripts (ie not part of toplevel), so they are defined unconditionally.
+  ({
     system.build = {
       inherit nixos-install nixos-generate-config nixos-option nixos-rebuild nixos-enter;
     };
-
-  };
+  })];
 
 }
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 2ef049beeca4..e40b7ed8015f 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -480,6 +480,7 @@
   ./services/desktops/telepathy.nix
   ./services/desktops/tumbler.nix
   ./services/desktops/zeitgeist.nix
+  ./services/development/athens.nix
   ./services/development/blackfire.nix
   ./services/development/bloop.nix
   ./services/development/distccd.nix
@@ -621,6 +622,7 @@
   ./services/matrix/appservice-irc.nix
   ./services/matrix/conduit.nix
   ./services/matrix/dendrite.nix
+  ./services/matrix/maubot.nix
   ./services/matrix/mautrix-facebook.nix
   ./services/matrix/mautrix-telegram.nix
   ./services/matrix/mautrix-whatsapp.nix
@@ -1440,6 +1442,7 @@
   ./system/boot/stratisroot.nix
   ./system/boot/modprobe.nix
   ./system/boot/networkd.nix
+  ./system/boot/unl0kr.nix
   ./system/boot/plymouth.nix
   ./system/boot/resolved.nix
   ./system/boot/shutdown.nix
@@ -1537,9 +1540,10 @@
   ./virtualisation/waydroid.nix
   ./virtualisation/xe-guest-utilities.nix
   ./virtualisation/xen-dom0.nix
-  { documentation.nixos.extraModules = [
-    ./virtualisation/qemu-vm.nix
-    ./image/repart.nix
+  {
+    documentation.nixos.extraModules = [
+      ./virtualisation/qemu-vm.nix
+      ./image/repart.nix
     ];
   }
 ]
diff --git a/nixos/modules/profiles/macos-builder.nix b/nixos/modules/profiles/macos-builder.nix
index d48afed18f7e..6c2602881d6b 100644
--- a/nixos/modules/profiles/macos-builder.nix
+++ b/nixos/modules/profiles/macos-builder.nix
@@ -103,6 +103,19 @@ in
     # server that QEMU provides (normally 10.0.2.3)
     networking.nameservers = [ "8.8.8.8" ];
 
+    # The linux builder is a lightweight VM for remote building; not evaluation.
+    nix.channel.enable = false;
+    # remote builder uses `nix-daemon` (ssh-ng:) or `nix-store --serve` (ssh:)
+    # --force: do not complain when missing
+    # TODO: install a store-only nix
+    #       https://github.com/NixOS/rfcs/blob/master/rfcs/0134-nix-store-layer.md#detailed-design
+    environment.extraSetup = ''
+      rm --force $out/bin/{nix-instantiate,nix-build,nix-shell,nix-prefetch*,nix}
+    '';
+    # Deployment is by image.
+    # TODO system.switch.enable = false;?
+    system.disableInstallerTools = true;
+
     nix.settings = {
       auto-optimise-store = true;
 
diff --git a/nixos/modules/programs/environment.nix b/nixos/modules/programs/environment.nix
index 6cf9257d035a..8ac723f42f61 100644
--- a/nixos/modules/programs/environment.nix
+++ b/nixos/modules/programs/environment.nix
@@ -45,7 +45,7 @@ in
         GTK_PATH = [ "/lib/gtk-2.0" "/lib/gtk-3.0" "/lib/gtk-4.0" ];
         XDG_CONFIG_DIRS = [ "/etc/xdg" ];
         XDG_DATA_DIRS = [ "/share" ];
-        LIBEXEC_PATH = [ "/lib/libexec" ];
+        LIBEXEC_PATH = [ "/libexec" ];
       };
 
     environment.pathsToLink = [ "/lib/gtk-2.0" "/lib/gtk-3.0" "/lib/gtk-4.0" ];
diff --git a/nixos/modules/programs/mininet.nix b/nixos/modules/programs/mininet.nix
index 02272729d233..01ffd811e70e 100644
--- a/nixos/modules/programs/mininet.nix
+++ b/nixos/modules/programs/mininet.nix
@@ -5,26 +5,39 @@
 with lib;
 
 let
-  cfg  = config.programs.mininet;
+  cfg = config.programs.mininet;
 
-  generatedPath = with pkgs; makeSearchPath "bin"  [
-    iperf ethtool iproute2 socat
+  telnet = pkgs.runCommand "inetutils-telnet"
+    { }
+    ''
+      mkdir -p $out/bin
+      ln -s ${pkgs.inetutils}/bin/telnet $out/bin
+    '';
+
+  generatedPath = with pkgs; makeSearchPath "bin" [
+    iperf
+    ethtool
+    iproute2
+    socat
+    # mn errors out without a telnet binary
+    # pkgs.inetutils brings an undesired ifconfig into PATH see #43105
+    nettools
+    telnet
   ];
 
-  pyEnv = pkgs.python.withPackages(ps: [ ps.mininet-python ]);
+  pyEnv = pkgs.python3.withPackages (ps: [ ps.mininet-python ]);
 
   mnexecWrapped = pkgs.runCommand "mnexec-wrapper"
-    { nativeBuildInputs = [ pkgs.makeWrapper pkgs.pythonPackages.wrapPython ]; }
+    { nativeBuildInputs = [ pkgs.makeWrapper pkgs.python3Packages.wrapPython ]; }
     ''
       makeWrapper ${pkgs.mininet}/bin/mnexec \
         $out/bin/mnexec \
         --prefix PATH : "${generatedPath}"
 
-      ln -s ${pyEnv}/bin/mn $out/bin/mn
-
-      # mn errors out without a telnet binary
-      # pkgs.inetutils brings an undesired ifconfig into PATH see #43105
-      ln -s ${pkgs.inetutils}/bin/telnet $out/bin/telnet
+      makeWrapper ${pyEnv}/bin/mn \
+        $out/bin/mn \
+        --prefix PYTHONPATH : "${pyEnv}/${pyEnv.sitePackages}" \
+        --prefix PATH : "${generatedPath}"
     '';
 in
 {
diff --git a/nixos/modules/services/computing/foldingathome/client.nix b/nixos/modules/services/computing/foldingathome/client.nix
index 1229e5ac987e..a4e79fd1c141 100644
--- a/nixos/modules/services/computing/foldingathome/client.nix
+++ b/nixos/modules/services/computing/foldingathome/client.nix
@@ -63,7 +63,7 @@ in
       default = [];
       description = lib.mdDoc ''
         Extra startup options for the FAHClient. Run
-        `FAHClient --help` to find all the available options.
+        `fah-client --help` to find all the available options.
       '';
     };
   };
@@ -74,7 +74,7 @@ in
       after = [ "network.target" ];
       wantedBy = [ "multi-user.target" ];
       script = ''
-        exec ${cfg.package}/bin/FAHClient ${lib.escapeShellArgs args}
+        exec ${lib.getExe cfg.package} ${lib.escapeShellArgs args}
       '';
       serviceConfig = {
         DynamicUser = true;
diff --git a/nixos/modules/services/databases/redis.nix b/nixos/modules/services/databases/redis.nix
index 315a0282cd73..4a5c19240ec6 100644
--- a/nixos/modules/services/databases/redis.nix
+++ b/nixos/modules/services/databases/redis.nix
@@ -393,9 +393,7 @@ in {
         ProtectKernelModules = true;
         ProtectKernelTunables = true;
         ProtectControlGroups = true;
-        RestrictAddressFamilies =
-          optionals (conf.port != 0) ["AF_INET" "AF_INET6"] ++
-          optional (conf.unixSocket != null) "AF_UNIX";
+        RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
         RestrictNamespaces = true;
         LockPersonality = true;
         MemoryDenyWriteExecute = true;
diff --git a/nixos/modules/services/development/athens.md b/nixos/modules/services/development/athens.md
new file mode 100644
index 000000000000..77663db509d5
--- /dev/null
+++ b/nixos/modules/services/development/athens.md
@@ -0,0 +1,52 @@
+# Athens {#module-athens}
+
+*Source:* {file}`modules/services/development/athens.nix`
+
+*Upstream documentation:* <https://docs.gomods.io/>
+
+[Athens](https://github.com/gomods/athens)
+is a Go module datastore and proxy
+
+The main goal of Athens is providing a Go proxy (`$GOPROXY`) in regions without access to `https://proxy.golang.org` or to
+improve the speed of Go module downloads for CI/CD systems.
+
+## Configuring {#module-services-development-athens-configuring}
+
+A complete list of options for the Athens module may be found
+[here](#opt-services.athens.enable).
+
+## Basic usage for a caching proxy configuration {#opt-services-development-athens-caching-proxy}
+
+A very basic configuration for Athens that acts as a caching and forwarding HTTP proxy is:
+```
+{
+    services.athens = {
+      enable = true;
+    };
+}
+```
+
+If you want to prevent Athens from writing to disk, you can instead configure it to cache modules only in memory:
+
+```
+{
+    services.athens = {
+      enable = true;
+      storageType = "memory";
+    };
+}
+```
+
+To use the local proxy in Go builds, you can set the proxy as environment variable:
+
+```
+{
+  environment.variables = {
+    GOPROXY = "http://localhost:3000"
+  };
+}
+```
+
+It is currently not possible to use the local proxy for builds done by the Nix daemon. This might be enabled
+by experimental features, specifically [`configurable-impure-env`](https://nixos.org/manual/nix/unstable/contributing/experimental-features#xp-feature-configurable-impure-env),
+in upcoming Nix versions.
diff --git a/nixos/modules/services/development/athens.nix b/nixos/modules/services/development/athens.nix
new file mode 100644
index 000000000000..34f8964a3bd5
--- /dev/null
+++ b/nixos/modules/services/development/athens.nix
@@ -0,0 +1,936 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.athens;
+
+  athensConfig = flip recursiveUpdate cfg.extraConfig (
+    {
+      GoBinary = "${cfg.goBinary}/bin/go";
+      GoEnv = cfg.goEnv;
+      GoBinaryEnvVars = lib.mapAttrsToList (k: v: "${k}=${v}") cfg.goBinaryEnvVars;
+      GoGetWorkers = cfg.goGetWorkers;
+      GoGetDir = cfg.goGetDir;
+      ProtocolWorkers = cfg.protocolWorkers;
+      LogLevel = cfg.logLevel;
+      CloudRuntime = cfg.cloudRuntime;
+      EnablePprof = cfg.enablePprof;
+      PprofPort = ":${toString cfg.pprofPort}";
+      FilterFile = cfg.filterFile;
+      RobotsFile = cfg.robotsFile;
+      Timeout = cfg.timeout;
+      StorageType = cfg.storageType;
+      TLSCertFile = cfg.tlsCertFile;
+      TLSKeyFile = cfg.tlsKeyFile;
+      Port = ":${toString cfg.port}";
+      UnixSocket = cfg.unixSocket;
+      GlobalEndpoint = cfg.globalEndpoint;
+      BasicAuthUser = cfg.basicAuthUser;
+      BasicAuthPass = cfg.basicAuthPass;
+      ForceSSL = cfg.forceSSL;
+      ValidatorHook = cfg.validatorHook;
+      PathPrefix = cfg.pathPrefix;
+      NETRCPath = cfg.netrcPath;
+      GithubToken = cfg.githubToken;
+      HGRCPath = cfg.hgrcPath;
+      TraceExporter = cfg.traceExporter;
+      StatsExporter = cfg.statsExporter;
+      SumDBs = cfg.sumDBs;
+      NoSumPatterns = cfg.noSumPatterns;
+      DownloadMode = cfg.downloadMode;
+      NetworkMode = cfg.networkMode;
+      DownloadURL = cfg.downloadURL;
+      SingleFlightType = cfg.singleFlightType;
+      IndexType = cfg.indexType;
+      ShutdownTimeout = cfg.shutdownTimeout;
+      SingleFlight = {
+        Etcd = {
+          Endpoints = builtins.concatStringsSep "," cfg.singleFlight.etcd.endpoints;
+        };
+        Redis = {
+          Endpoint = cfg.singleFlight.redis.endpoint;
+          Password = cfg.singleFlight.redis.password;
+          LockConfig = {
+            TTL = cfg.singleFlight.redis.lockConfig.ttl;
+            Timeout = cfg.singleFlight.redis.lockConfig.timeout;
+            MaxRetries = cfg.singleFlight.redis.lockConfig.maxRetries;
+          };
+        };
+        RedisSentinel = {
+          Endpoints = cfg.singleFlight.redisSentinel.endpoints;
+          MasterName = cfg.singleFlight.redisSentinel.masterName;
+          SentinelPassword = cfg.singleFlight.redisSentinel.sentinelPassword;
+          LockConfig = {
+            TTL = cfg.singleFlight.redisSentinel.lockConfig.ttl;
+            Timeout = cfg.singleFlight.redisSentinel.lockConfig.timeout;
+            MaxRetries = cfg.singleFlight.redisSentinel.lockConfig.maxRetries;
+          };
+        };
+      };
+      Storage = {
+        CDN = {
+          Endpoint = cfg.storage.cdn.endpoint;
+        };
+        Disk = {
+          RootPath = cfg.storage.disk.rootPath;
+        };
+        GCP = {
+          ProjectID = cfg.storage.gcp.projectID;
+          Bucket = cfg.storage.gcp.bucket;
+          JSONKey = cfg.storage.gcp.jsonKey;
+        };
+        Minio = {
+          Endpoint = cfg.storage.minio.endpoint;
+          Key = cfg.storage.minio.key;
+          Secret = cfg.storage.minio.secret;
+          EnableSSL = cfg.storage.minio.enableSSL;
+          Bucket = cfg.storage.minio.bucket;
+          region = cfg.storage.minio.region;
+        };
+        Mongo = {
+          URL = cfg.storage.mongo.url;
+          DefaultDBName = cfg.storage.mongo.defaultDBName;
+          CertPath = cfg.storage.mongo.certPath;
+          Insecure = cfg.storage.mongo.insecure;
+        };
+        S3 = {
+          Region = cfg.storage.s3.region;
+          Key = cfg.storage.s3.key;
+          Secret = cfg.storage.s3.secret;
+          Token = cfg.storage.s3.token;
+          Bucket = cfg.storage.s3.bucket;
+          ForcePathStyle = cfg.storage.s3.forcePathStyle;
+          UseDefaultConfiguration = cfg.storage.s3.useDefaultConfiguration;
+          CredentialsEndpoint = cfg.storage.s3.credentialsEndpoint;
+          AwsContainerCredentialsRelativeURI = cfg.storage.s3.awsContainerCredentialsRelativeURI;
+          Endpoint = cfg.storage.s3.endpoint;
+        };
+        AzureBlob = {
+          AccountName = cfg.storage.azureblob.accountName;
+          AccountKey = cfg.storage.azureblob.accountKey;
+          ContainerName = cfg.storage.azureblob.containerName;
+        };
+        External = {
+          URL = cfg.storage.external.url;
+        };
+      };
+      Index = {
+        MySQL = {
+          Protocol = cfg.index.mysql.protocol;
+          Host = cfg.index.mysql.host;
+          Port = cfg.index.mysql.port;
+          User = cfg.index.mysql.user;
+          Password = cfg.index.mysql.password;
+          Database = cfg.index.mysql.database;
+          Params = {
+            parseTime = cfg.index.mysql.params.parseTime;
+            timeout = cfg.index.mysql.params.timeout;
+          };
+        };
+        Postgres = {
+          Host = cfg.index.postgres.host;
+          Port = cfg.index.postgres.port;
+          User = cfg.index.postgres.user;
+          Password = cfg.index.postgres.password;
+          Database = cfg.index.postgres.database;
+          Params = {
+            connect_timeout = cfg.index.postgres.params.connect_timeout;
+            sslmode = cfg.index.postgres.params.sslmode;
+          };
+        };
+      };
+    }
+  );
+
+  configFile = pkgs.runCommandLocal "config.toml" { } ''
+    ${pkgs.buildPackages.jq}/bin/jq 'del(..|nulls)' \
+      < ${pkgs.writeText "config.json" (builtins.toJSON athensConfig)} | \
+    ${pkgs.buildPackages.remarshal}/bin/remarshal -if json -of toml \
+      > $out
+  '';
+in
+{
+  meta = {
+    maintainers = pkgs.athens.meta.maintainers;
+    doc = ./athens.md;
+  };
+
+  options.services.athens = {
+    enable = mkEnableOption (lib.mdDoc "Go module datastore and proxy");
+
+    package = mkOption {
+      default = pkgs.athens;
+      defaultText = literalExpression "pkgs.athens";
+      example = "pkgs.athens";
+      description = lib.mdDoc "Which athens derivation to use";
+      type = types.package;
+    };
+
+    goBinary = mkOption {
+      type = types.package;
+      default = pkgs.go;
+      defaultText = literalExpression "pkgs.go";
+      example = "pkgs.go_1_21";
+      description = lib.mdDoc ''
+        The Go package used by Athens at runtime.
+
+        Athens primarily runs two Go commands:
+        1. `go mod download -json <module>@<version>`
+        2. `go list -m -json <module>@latest`
+      '';
+    };
+
+    goEnv = mkOption {
+      type = types.enum [ "development" "production" ];
+      description = lib.mdDoc "Specifies the type of environment to run. One of 'development' or 'production'.";
+      default = "development";
+      example = "production";
+    };
+
+    goBinaryEnvVars = mkOption {
+      type = types.attrs;
+      description = lib.mdDoc "Environment variables to pass to the Go binary.";
+      example = ''
+        { "GOPROXY" = "direct", "GODEBUG" = "true" }
+      '';
+      default = { };
+    };
+
+    goGetWorkers = mkOption {
+      type = types.int;
+      description = lib.mdDoc "Number of workers concurrently downloading modules.";
+      default = 10;
+      example = 32;
+    };
+
+    goGetDir = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''
+        Temporary directory that Athens will use to
+        fetch modules from VCS prior to persisting
+        them to a storage backend.
+
+        If the value is empty, Athens will use the
+        default OS temp directory.
+      '';
+      default = null;
+      example = "/tmp/athens";
+    };
+
+    protocolWorkers = mkOption {
+      type = types.int;
+      description = lib.mdDoc "Number of workers concurrently serving protocol paths.";
+      default = 30;
+    };
+
+    logLevel = mkOption {
+      type = types.nullOr (types.enum [ "panic" "fatal" "error" "warning" "info" "debug" "trace" ]);
+      description = lib.mdDoc ''
+        Log level for Athens.
+        Supports all logrus log levels (https://github.com/Sirupsen/logrus#level-logging)".
+      '';
+      default = "warning";
+      example = "debug";
+    };
+
+    cloudRuntime = mkOption {
+      type = types.enum [ "GCP" "none" ];
+      description = lib.mdDoc ''
+        Specifies the Cloud Provider on which the Proxy/registry is running.
+      '';
+      default = "none";
+      example = "GCP";
+    };
+
+    enablePprof = mkOption {
+      type = types.bool;
+      description = lib.mdDoc "Enable pprof endpoints.";
+      default = false;
+    };
+
+    pprofPort = mkOption {
+      type = types.port;
+      description = lib.mdDoc "Port number for pprof endpoints.";
+      default = 3301;
+      example = 443;
+    };
+
+    filterFile = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''Filename for the include exclude filter.'';
+      default = null;
+      example = literalExpression ''
+        pkgs.writeText "filterFile" '''
+          - github.com/azure
+          + github.com/azure/azure-sdk-for-go
+          D golang.org/x/tools
+        '''
+      '';
+    };
+
+    robotsFile = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''Provides /robots.txt for net crawlers.'';
+      default = null;
+      example = literalExpression ''pkgs.writeText "robots.txt" "# my custom robots.txt ..."'';
+    };
+
+    timeout = mkOption {
+      type = types.int;
+      description = lib.mdDoc "Timeout for external network calls in seconds.";
+      default = 300;
+      example = 3;
+    };
+
+    storageType = mkOption {
+      type = types.enum [ "memory" "disk" "mongo" "gcp" "minio" "s3" "azureblob" "external" ];
+      description = lib.mdDoc "Specifies the type of storage backend to use.";
+      default = "disk";
+    };
+
+    tlsCertFile = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc "Path to the TLS certificate file.";
+      default = null;
+      example = "/etc/ssl/certs/athens.crt";
+    };
+
+    tlsKeyFile = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc "Path to the TLS key file.";
+      default = null;
+      example = "/etc/ssl/certs/athens.key";
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 3000;
+      description = lib.mdDoc ''
+        Port number Athens listens on.
+      '';
+      example = 443;
+    };
+
+    unixSocket = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''
+        Path to the unix socket file.
+        If set, Athens will listen on the unix socket instead of TCP socket.
+      '';
+      default = null;
+      example = "/run/athens.sock";
+    };
+
+    globalEndpoint = mkOption {
+      type = types.str;
+      description = lib.mdDoc ''
+        Endpoint for a package registry in case of a proxy cache miss.
+      '';
+      default = "";
+      example = "http://upstream-athens.example.com:3000";
+    };
+
+    basicAuthUser = mkOption {
+      type = types.nullOr types.str;
+      description = lib.mdDoc ''
+        Username for basic auth.
+      '';
+      default = null;
+      example = "user";
+    };
+
+    basicAuthPass = mkOption {
+      type = types.nullOr types.str;
+      description = lib.mdDoc ''
+        Password for basic auth. Warning: this is stored in plain text in the config file.
+      '';
+      default = null;
+      example = "swordfish";
+    };
+
+    forceSSL = mkOption {
+      type = types.bool;
+      description = lib.mdDoc ''
+        Force SSL redirects for incoming requests.
+      '';
+      default = false;
+    };
+
+    validatorHook = mkOption {
+      type = types.nullOr types.str;
+      description = lib.mdDoc ''
+        Endpoint to validate modules against.
+
+        Not used if empty.
+      '';
+      default = null;
+      example = "https://validation.example.com";
+    };
+
+    pathPrefix = mkOption {
+      type = types.nullOr types.str;
+      description = lib.mdDoc ''
+        Sets basepath for all routes.
+      '';
+      default = null;
+      example = "/athens";
+    };
+
+    netrcPath = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''
+        Path to the .netrc file.
+      '';
+      default = null;
+      example = "/home/user/.netrc";
+    };
+
+    githubToken = mkOption {
+      type = types.nullOr types.str;
+      description = lib.mdDoc ''
+        Creates .netrc file with the given token to be used for GitHub.
+        Warning: this is stored in plain text in the config file.
+      '';
+      default = null;
+      example = "ghp_1234567890";
+    };
+
+    hgrcPath = mkOption {
+      type = types.nullOr types.path;
+      description = lib.mdDoc ''
+        Path to the .hgrc file.
+      '';
+      default = null;
+      example = "/home/user/.hgrc";
+    };
+
+    traceExporter = mkOption {
+      type = types.nullOr (types.enum [ "jaeger" "datadog" ]);
+      description = lib.mdDoc ''
+        Trace exporter to use.
+      '';
+      default = null;
+    };
+
+    traceExporterURL = mkOption {
+      type = types.nullOr types.str;
+      description = lib.mdDoc ''
+        URL endpoint that traces will be sent to.
+      '';
+      default = null;
+      example = "http://localhost:14268";
+    };
+
+    statsExporter = mkOption {
+      type = types.nullOr (types.enum [ "prometheus" ]);
+      description = lib.mdDoc "Stats exporter to use.";
+      default = null;
+    };
+
+    sumDBs = mkOption {
+      type = types.listOf types.str;
+      description = lib.mdDoc ''
+        List of fully qualified URLs that Athens will proxy
+        that the go command can use a checksum verifier.
+      '';
+      default = [ "https://sum.golang.org" ];
+    };
+
+    noSumPatterns = mkOption {
+      type = types.listOf types.str;
+      description = lib.mdDoc ''
+        List of patterns that Athens sum db proxy will return a 403 for.
+      '';
+      default = [ ];
+      example = [ "github.com/mycompany/*" ];
+    };
+
+    downloadMode = mkOption {
+      type = types.oneOf [ (types.enum [ "sync" "async" "redirect" "async_redirect" "none" ]) (types.strMatching "^file:.*$|^custom:.*$") ];
+      description = lib.mdDoc ''
+        Defines how Athens behaves when a module@version
+        is not found in storage. There are 7 options:
+        1. "sync": download the module synchronously and
+        return the results to the client.
+        2. "async": return 404, but asynchronously store the module
+        in the storage backend.
+        3. "redirect": return a 301 redirect status to the client
+        with the base URL as the DownloadRedirectURL from below.
+        4. "async_redirect": same as option number 3 but it will
+        asynchronously store the module to the backend.
+        5. "none": return 404 if a module is not found and do nothing.
+        6. "file:<path>": will point to an HCL file that specifies
+        any of the 5 options above based on different import paths.
+        7. "custom:<base64-encoded-hcl>" is the same as option 6
+        but the file is fully encoded in the option. This is
+        useful for using an environment variable in serverless
+        deployments.
+      '';
+      default = "async_redirect";
+    };
+
+    networkMode = mkOption {
+      type = types.enum [ "strict" "offline" "fallback" ];
+      description = lib.mdDoc ''
+        Configures how Athens will return the results
+        of the /list endpoint as it can be assembled from both its own
+        storage and the upstream VCS.
+
+        Note, that for better error messaging, this would also affect how other
+        endpoints behave.
+
+        Modes:
+        1. strict: merge VCS versions with storage versions, but fail if either of them fails.
+        2. offline: only get storage versions, never reach out to VCS.
+        3. fallback: only return storage versions, if VCS fails. Note this means that you may
+        see inconsistent results since fallback mode does a best effort of giving you what's
+        available at the time of requesting versions.
+      '';
+      default = "strict";
+    };
+
+    downloadURL = mkOption {
+      type = types.str;
+      description = lib.mdDoc "URL used if DownloadMode is set to redirect.";
+      default = "https://proxy.golang.org";
+    };
+
+    singleFlightType = mkOption {
+      type = types.enum [ "memory" "etcd" "redis" "redis-sentinel" "gcp" "azureblob" ];
+      description = lib.mdDoc ''
+        Determines what mechanism Athens uses to manage concurrency flowing into the Athens backend.
+      '';
+      default = "memory";
+    };
+
+    indexType = mkOption {
+      type = types.enum [ "none" "memory" "mysql" "postgres" ];
+      description = lib.mdDoc ''
+        Type of index backend Athens will use.
+      '';
+      default = "none";
+    };
+
+    shutdownTimeout = mkOption {
+      type = types.int;
+      description = lib.mdDoc ''
+        Number of seconds to wait for the server to shutdown gracefully.
+      '';
+      default = 60;
+      example = 1;
+    };
+
+    singleFlight = {
+      etcd = {
+        endpoints = mkOption {
+          type = types.listOf types.str;
+          description = lib.mdDoc "URLs that determine all distributed etcd servers.";
+          default = [ ];
+          example = [ "localhost:2379" ];
+        };
+      };
+      redis = {
+        endpoint = mkOption {
+          type = types.str;
+          description = lib.mdDoc "URL of the redis server.";
+          default = "";
+          example = "localhost:6379";
+        };
+        password = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Password for the redis server. Warning: this is stored in plain text in the config file.";
+          default = "";
+          example = "swordfish";
+        };
+
+        lockConfig = {
+          ttl = mkOption {
+            type = types.int;
+            description = lib.mdDoc "TTL for the lock in seconds.";
+            default = 900;
+            example = 1;
+          };
+          timeout = mkOption {
+            type = types.int;
+            description = lib.mdDoc "Timeout for the lock in seconds.";
+            default = 15;
+            example = 1;
+          };
+          maxRetries = mkOption {
+            type = types.int;
+            description = lib.mdDoc "Maximum number of retries for the lock.";
+            default = 10;
+            example = 1;
+          };
+        };
+      };
+
+      redisSentinel = {
+        endpoints = mkOption {
+          type = types.listOf types.str;
+          description = lib.mdDoc "URLs that determine all distributed redis servers.";
+          default = [ ];
+          example = [ "localhost:26379" ];
+        };
+        masterName = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Name of the sentinel master server.";
+          default = "";
+          example = "redis-1";
+        };
+        sentinelPassword = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Password for the sentinel server. Warning: this is stored in plain text in the config file.";
+          default = "";
+          example = "swordfish";
+        };
+
+        lockConfig = {
+          ttl = mkOption {
+            type = types.int;
+            description = lib.mdDoc "TTL for the lock in seconds.";
+            default = 900;
+            example = 1;
+          };
+          timeout = mkOption {
+            type = types.int;
+            description = lib.mdDoc "Timeout for the lock in seconds.";
+            default = 15;
+            example = 1;
+          };
+          maxRetries = mkOption {
+            type = types.int;
+            description = lib.mdDoc "Maximum number of retries for the lock.";
+            default = 10;
+            example = 1;
+          };
+        };
+      };
+    };
+
+    storage = {
+      cdn = {
+        endpoint = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "hostname of the CDN server.";
+          example = "cdn.example.com";
+          default = null;
+        };
+      };
+
+      disk = {
+        rootPath = mkOption {
+          type = types.nullOr types.path;
+          description = lib.mdDoc "Athens disk root folder.";
+          default = "/var/lib/athens";
+        };
+      };
+
+      gcp = {
+        projectID = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "GCP project ID.";
+          example = "my-project";
+          default = null;
+        };
+        bucket = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "GCP backend storage bucket.";
+          example = "my-bucket";
+          default = null;
+        };
+        jsonKey = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Base64 encoded GCP service account key. Warning: this is stored in plain text in the config file.";
+          default = null;
+        };
+      };
+
+      minio = {
+        endpoint = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Endpoint of the minio storage backend.";
+          example = "minio.example.com:9001";
+          default = null;
+        };
+        key = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Access key id for the minio storage backend.";
+          example = "minio";
+          default = null;
+        };
+        secret = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Secret key for the minio storage backend. Warning: this is stored in plain text in the config file.";
+          example = "minio123";
+          default = null;
+        };
+        enableSSL = mkOption {
+          type = types.bool;
+          description = lib.mdDoc "Enable SSL for the minio storage backend.";
+          default = false;
+        };
+        bucket = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Bucket name for the minio storage backend.";
+          example = "gomods";
+          default = null;
+        };
+        region = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Region for the minio storage backend.";
+          example = "us-east-1";
+          default = null;
+        };
+      };
+
+      mongo = {
+        url = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "URL of the mongo database.";
+          example = "mongodb://localhost:27017";
+          default = null;
+        };
+        defaultDBName = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Name of the mongo database.";
+          example = "athens";
+          default = null;
+        };
+        certPath = mkOption {
+          type = types.nullOr types.path;
+          description = lib.mdDoc "Path to the certificate file for the mongo database.";
+          example = "/etc/ssl/mongo.pem";
+          default = null;
+        };
+        insecure = mkOption {
+          type = types.bool;
+          description = lib.mdDoc "Allow insecure connections to the mongo database.";
+          default = false;
+        };
+      };
+
+      s3 = {
+        region = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Region of the S3 storage backend.";
+          example = "eu-west-3";
+          default = null;
+        };
+        key = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Access key id for the S3 storage backend.";
+          example = "minio";
+          default = null;
+        };
+        secret = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Secret key for the S3 storage backend. Warning: this is stored in plain text in the config file.";
+          default = "";
+        };
+        token = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Token for the S3 storage backend. Warning: this is stored in plain text in the config file.";
+          default = null;
+        };
+        bucket = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Bucket name for the S3 storage backend.";
+          example = "gomods";
+          default = null;
+        };
+        forcePathStyle = mkOption {
+          type = types.bool;
+          description = lib.mdDoc "Force path style for the S3 storage backend.";
+          default = false;
+        };
+        useDefaultConfiguration = mkOption {
+          type = types.bool;
+          description = lib.mdDoc "Use default configuration for the S3 storage backend.";
+          default = false;
+        };
+        credentialsEndpoint = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Credentials endpoint for the S3 storage backend.";
+          default = "";
+        };
+        awsContainerCredentialsRelativeURI = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Container relative url (used by fargate).";
+          default = null;
+        };
+        endpoint = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Endpoint for the S3 storage backend.";
+          default = null;
+        };
+      };
+
+      azureblob = {
+        accountName = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Account name for the Azure Blob storage backend.";
+          default = null;
+        };
+        accountKey = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Account key for the Azure Blob storage backend. Warning: this is stored in plain text in the config file.";
+          default = null;
+        };
+        containerName = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Container name for the Azure Blob storage backend.";
+          default = null;
+        };
+      };
+
+      external = {
+        url = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "URL of the backend storage layer.";
+          example = "https://athens.example.com";
+          default = null;
+        };
+      };
+    };
+
+    index = {
+      mysql = {
+        protocol = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Protocol for the MySQL database.";
+          default = "tcp";
+        };
+        host = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Host for the MySQL database.";
+          default = "localhost";
+        };
+        port = mkOption {
+          type = types.int;
+          description = lib.mdDoc "Port for the MySQL database.";
+          default = 3306;
+        };
+        user = mkOption {
+          type = types.str;
+          description = lib.mdDoc "User for the MySQL database.";
+          default = "root";
+        };
+        password = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Password for the MySQL database. Warning: this is stored in plain text in the config file.";
+          default = null;
+        };
+        database = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Database name for the MySQL database.";
+          default = "athens";
+        };
+        params = {
+          parseTime = mkOption {
+            type = types.nullOr types.str;
+            description = lib.mdDoc "Parse time for the MySQL database.";
+            default = "true";
+          };
+          timeout = mkOption {
+            type = types.nullOr types.str;
+            description = lib.mdDoc "Timeout for the MySQL database.";
+            default = "30s";
+          };
+        };
+      };
+
+      postgres = {
+        host = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Host for the Postgres database.";
+          default = "localhost";
+        };
+        port = mkOption {
+          type = types.int;
+          description = lib.mdDoc "Port for the Postgres database.";
+          default = 5432;
+        };
+        user = mkOption {
+          type = types.str;
+          description = lib.mdDoc "User for the Postgres database.";
+          default = "postgres";
+        };
+        password = mkOption {
+          type = types.nullOr types.str;
+          description = lib.mdDoc "Password for the Postgres database. Warning: this is stored in plain text in the config file.";
+          default = null;
+        };
+        database = mkOption {
+          type = types.str;
+          description = lib.mdDoc "Database name for the Postgres database.";
+          default = "athens";
+        };
+        params = {
+          connect_timeout = mkOption {
+            type = types.nullOr types.str;
+            description = lib.mdDoc "Connect timeout for the Postgres database.";
+            default = "30s";
+          };
+          sslmode = mkOption {
+            type = types.nullOr types.str;
+            description = lib.mdDoc "SSL mode for the Postgres database.";
+            default = "disable";
+          };
+        };
+      };
+    };
+
+    extraConfig = mkOption {
+      type = types.attrs;
+      description = lib.mdDoc ''
+        Extra configuration options for the athens config file.
+      '';
+      default = { };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.athens = {
+      description = "Athens Go module proxy";
+      documentation = [ "https://docs.gomods.io" ];
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
+
+      serviceConfig = {
+        Restart = "on-abnormal";
+        Nice = 5;
+        ExecStart = ''${cfg.package}/bin/athens -config_file=${configFile}'';
+
+        KillMode = "mixed";
+        KillSignal = "SIGINT";
+        TimeoutStopSec = cfg.shutdownTimeout;
+
+        LimitNOFILE = 1048576;
+        LimitNPROC = 512;
+
+        DynamicUser = true;
+        PrivateTmp = true;
+        PrivateDevices = true;
+        ProtectHome = "read-only";
+        ProtectSystem = "full";
+
+        ReadWritePaths = mkIf (cfg.storage.disk.rootPath != null && (! hasPrefix "/var/lib/" cfg.storage.disk.rootPath)) [ cfg.storage.disk.rootPath ];
+        StateDirectory = mkIf (hasPrefix "/var/lib/" cfg.storage.disk.rootPath) [ (removePrefix "/var/lib/" cfg.storage.disk.rootPath) ];
+
+        CapabilityBoundingSet = [ "CAP_NET_BIND_SERVICE" ];
+        AmbientCapabilities = [ "CAP_NET_BIND_SERVICE" ];
+        NoNewPrivileges = true;
+      };
+    };
+
+    networking.firewall = {
+      allowedTCPPorts = optionals (cfg.unixSocket == null) [ cfg.port ]
+        ++ optionals cfg.enablePprof [ cfg.pprofPort ];
+    };
+  };
+
+}
diff --git a/nixos/modules/services/matrix/maubot.md b/nixos/modules/services/matrix/maubot.md
new file mode 100644
index 000000000000..f6a05db56caf
--- /dev/null
+++ b/nixos/modules/services/matrix/maubot.md
@@ -0,0 +1,103 @@
+# Maubot {#module-services-maubot}
+
+[Maubot](https://github.com/maubot/maubot) is a plugin-based bot
+framework for Matrix.
+
+## Configuration {#module-services-maubot-configuration}
+
+1. Set [](#opt-services.maubot.enable) to `true`. The service will use
+   SQLite by default.
+2. If you want to use PostgreSQL instead of SQLite, do this:
+
+   ```nix
+   services.maubot.settings.database = "postgresql://maubot@localhost/maubot";
+   ```
+
+   If the PostgreSQL connection requires a password, you will have to
+   add it later on step 8.
+3. If you plan to expose your Maubot interface to the web, do something
+   like this:
+   ```nix
+   services.nginx.virtualHosts."matrix.example.org".locations = {
+     "/_matrix/maubot/" = {
+       proxyPass = "http://127.0.0.1:${toString config.services.maubot.settings.server.port}";
+       proxyWebsockets = true;
+     };
+   };
+   services.maubot.settings.server.public_url = "matrix.example.org";
+   # do the following only if you want to use something other than /_matrix/maubot...
+   services.maubot.settings.server.ui_base_path = "/another/base/path";
+   ```
+4. Optionally, set `services.maubot.pythonPackages` to a list of python3
+   packages to make available for Maubot plugins.
+5. Optionally, set `services.maubot.plugins` to a list of Maubot
+   plugins (full list available at https://plugins.maubot.xyz/):
+   ```nix
+   services.maubot.plugins = with config.services.maubot.package.plugins; [
+     reactbot
+     # This will only change the default config! After you create a
+     # plugin instance, the default config will be copied into that
+     # instance's config in Maubot's database, and further base config
+     # changes won't affect the running plugin.
+     (rss.override {
+       base_config = {
+         update_interval = 60;
+         max_backoff = 7200;
+         spam_sleep = 2;
+         command_prefix = "rss";
+         admins = [ "@chayleaf:pavluk.org" ];
+       };
+     })
+   ];
+   # ...or...
+   services.maubot.plugins = config.services.maubot.package.plugins.allOfficialPlugins;
+   # ...or...
+   services.maubot.plugins = config.services.maubot.package.plugins.allPlugins;
+   # ...or...
+   services.maubot.plugins = with config.services.maubot.package.plugins; [
+     (weather.override {
+       # you can pass base_config as a string
+       base_config = ''
+         default_location: New York
+         default_units: M
+         default_language:
+         show_link: true
+         show_image: false
+       '';
+     })
+   ];
+   ```
+6. Start Maubot at least once before doing the following steps (it's
+   necessary to generate the initial config).
+7. If your PostgreSQL connection requires a password, add
+   `database: postgresql://user:password@localhost/maubot`
+   to `/var/lib/maubot/config.yaml`. This overrides the Nix-provided
+   config. Even then, don't remove the `database` line from Nix config
+   so the module knows you use PostgreSQL!
+8. To create a user account for logging into Maubot web UI and
+   configuring it, generate a password using the shell command
+   `mkpasswd -R 12 -m bcrypt`, and edit `/var/lib/maubot/config.yaml`
+   with the following:
+
+   ```yaml
+   admins:
+       admin_username: $2b$12$g.oIStUeUCvI58ebYoVMtO/vb9QZJo81PsmVOomHiNCFbh0dJpZVa
+   ```
+
+   Where `admin_username` is your username, and `$2b...` is the bcrypted
+   password.
+9. Optional: if you want to be able to register new users with the
+   Maubot CLI (`mbc`), and your homeserver is private, add your
+   homeserver's registration key to `/var/lib/maubot/config.yaml`:
+
+   ```yaml
+   homeservers:
+       matrix.example.org:
+           url: https://matrix.example.org
+           secret: your-very-secret-key
+   ```
+10. Restart Maubot after editing `/var/lib/maubot/config.yaml`,and
+    Maubot will be available at
+    `https://matrix.example.org/_matrix/maubot`. If you want to use the
+    `mbc` CLI, it's available using the `maubot` package (`nix-shell -p
+    maubot`).
diff --git a/nixos/modules/services/matrix/maubot.nix b/nixos/modules/services/matrix/maubot.nix
new file mode 100644
index 000000000000..6cdb57fa72ef
--- /dev/null
+++ b/nixos/modules/services/matrix/maubot.nix
@@ -0,0 +1,459 @@
+{ lib
+, config
+, pkgs
+, ...
+}:
+
+let
+  cfg = config.services.maubot;
+
+  wrapper1 =
+    if cfg.plugins == [ ]
+    then cfg.package
+    else cfg.package.withPlugins (_: cfg.plugins);
+
+  wrapper2 =
+    if cfg.pythonPackages == [ ]
+    then wrapper1
+    else wrapper1.withPythonPackages (_: cfg.pythonPackages);
+
+  settings = lib.recursiveUpdate cfg.settings {
+    plugin_directories.trash =
+      if cfg.settings.plugin_directories.trash == null
+      then "delete"
+      else cfg.settings.plugin_directories.trash;
+    server.unshared_secret = "generate";
+  };
+
+  finalPackage = wrapper2.withBaseConfig settings;
+
+  isPostgresql = db: builtins.isString db && lib.hasPrefix "postgresql://" db;
+  isLocalPostgresDB = db: isPostgresql db && builtins.any (x: lib.hasInfix x db) [
+    "@127.0.0.1/"
+    "@::1/"
+    "@[::1]/"
+    "@localhost/"
+  ];
+  parsePostgresDB = db:
+    let
+      noSchema = lib.removePrefix "postgresql://" db;
+    in {
+      username = builtins.head (lib.splitString "@" noSchema);
+      database = lib.last (lib.splitString "/" noSchema);
+    };
+
+  postgresDBs = [
+    cfg.settings.database
+    cfg.settings.crypto_database
+    cfg.settings.plugin_databases.postgres
+  ];
+
+  localPostgresDBs = builtins.filter isLocalPostgresDB postgresDBs;
+
+  parsedLocalPostgresDBs = map parsePostgresDB localPostgresDBs;
+  parsedPostgresDBs = map parsePostgresDB postgresDBs;
+
+  hasLocalPostgresDB = localPostgresDBs != [ ];
+in
+{
+  options.services.maubot = with lib; {
+    enable = mkEnableOption (mdDoc "maubot");
+
+    package = lib.mkPackageOptionMD pkgs "maubot" { };
+
+    plugins = mkOption {
+      type = types.listOf types.package;
+      default = [ ];
+      example = literalExpression ''
+        with config.services.maubot.package.plugins; [
+          xyz.maubot.reactbot
+          xyz.maubot.rss
+        ];
+      '';
+      description = mdDoc ''
+        List of additional maubot plugins to make available.
+      '';
+    };
+
+    pythonPackages = mkOption {
+      type = types.listOf types.package;
+      default = [ ];
+      example = literalExpression ''
+        with pkgs.python3Packages; [
+          aiohttp
+        ];
+      '';
+      description = mdDoc ''
+        List of additional Python packages to make available for maubot.
+      '';
+    };
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/maubot";
+      description = mdDoc ''
+        The directory where maubot stores its stateful data.
+      '';
+    };
+
+    extraConfigFile = mkOption {
+      type = types.str;
+      default = "./config.yaml";
+      defaultText = literalExpression ''"''${config.services.maubot.dataDir}/config.yaml"'';
+      description = mdDoc ''
+        A file for storing secrets. You can pass homeserver registration keys here.
+        If it already exists, **it must contain `server.unshared_secret`** which is used for signing API keys.
+        If `configMutable` is not set to true, **maubot user must have write access to this file**.
+      '';
+    };
+
+    configMutable = mkOption {
+      type = types.bool;
+      default = false;
+      description = mdDoc ''
+        Whether maubot should write updated config into `extraConfigFile`. **This will make your Nix module settings have no effect besides the initial config, as extraConfigFile takes precedence over NixOS settings!**
+      '';
+    };
+
+    settings = mkOption {
+      default = { };
+      description = mdDoc ''
+        YAML settings for maubot. See the
+        [example configuration](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml)
+        for more info.
+
+        Secrets should be passed in by using `extraConfigFile`.
+      '';
+      type = with types; submodule {
+        options = {
+          database = mkOption {
+            type = str;
+            default = "sqlite:maubot.db";
+            example = "postgresql://username:password@hostname/dbname";
+            description = mdDoc ''
+              The full URI to the database. SQLite and Postgres are fully supported.
+              Other DBMSes supported by SQLAlchemy may or may not work.
+            '';
+          };
+
+          crypto_database = mkOption {
+            type = str;
+            default = "default";
+            example = "postgresql://username:password@hostname/dbname";
+            description = mdDoc ''
+              Separate database URL for the crypto database. By default, the regular database is also used for crypto.
+            '';
+          };
+
+          database_opts = mkOption {
+            type = types.attrs;
+            default = { };
+            description = mdDoc ''
+              Additional arguments for asyncpg.create_pool() or sqlite3.connect()
+            '';
+          };
+
+          plugin_directories = mkOption {
+            default = { };
+            description = mdDoc "Plugin directory paths";
+            type = submodule {
+              options = {
+                upload = mkOption {
+                  type = types.str;
+                  default = "./plugins";
+                  defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
+                  description = mdDoc ''
+                    The directory where uploaded new plugins should be stored.
+                  '';
+                };
+                load = mkOption {
+                  type = types.listOf types.str;
+                  default = [ "./plugins" ];
+                  defaultText = literalExpression ''[ "''${config.services.maubot.dataDir}/plugins" ]'';
+                  description = mdDoc ''
+                    The directories from which plugins should be loaded. Duplicate plugin IDs will be moved to the trash.
+                  '';
+                };
+                trash = mkOption {
+                  type = with types; nullOr str;
+                  default = "./trash";
+                  defaultText = literalExpression ''"''${config.services.maubot.dataDir}/trash"'';
+                  description = mdDoc ''
+                    The directory where old plugin versions and conflicting plugins should be moved. Set to null to delete files immediately.
+                  '';
+                };
+              };
+            };
+          };
+
+          plugin_databases = mkOption {
+            description = mdDoc "Plugin database settings";
+            default = { };
+            type = submodule {
+              options = {
+                sqlite = mkOption {
+                  type = types.str;
+                  default = "./plugins";
+                  defaultText = literalExpression ''"''${config.services.maubot.dataDir}/plugins"'';
+                  description = mdDoc ''
+                    The directory where SQLite plugin databases should be stored.
+                  '';
+                };
+
+                postgres = mkOption {
+                  type = types.nullOr types.str;
+                  default = if isPostgresql cfg.settings.database then "default" else null;
+                  defaultText = literalExpression ''if isPostgresql config.services.maubot.settings.database then "default" else null'';
+                  description = mdDoc ''
+                    The connection URL for plugin database. See [example config](https://github.com/maubot/maubot/blob/master/maubot/example-config.yaml) for exact format.
+                  '';
+                };
+
+                postgres_max_conns_per_plugin = mkOption {
+                  type = types.nullOr types.int;
+                  default = 3;
+                  description = mdDoc ''
+                    Maximum number of connections per plugin instance.
+                  '';
+                };
+
+                postgres_opts = mkOption {
+                  type = types.attrs;
+                  default = { };
+                  description = mdDoc ''
+                    Overrides for the default database_opts when using a non-default postgres connection URL.
+                  '';
+                };
+              };
+            };
+          };
+
+          server = mkOption {
+            default = { };
+            description = mdDoc "Listener config";
+            type = submodule {
+              options = {
+                hostname = mkOption {
+                  type = types.str;
+                  default = "127.0.0.1";
+                  description = mdDoc ''
+                    The IP to listen on
+                  '';
+                };
+                port = mkOption {
+                  type = types.port;
+                  default = 29316;
+                  description = mdDoc ''
+                    The port to listen on
+                  '';
+                };
+                public_url = mkOption {
+                  type = types.str;
+                  default = "http://${cfg.settings.server.hostname}:${toString cfg.settings.server.port}";
+                  defaultText = literalExpression ''"http://''${config.services.maubot.settings.server.hostname}:''${toString config.services.maubot.settings.server.port}"'';
+                  description = mdDoc ''
+                    Public base URL where the server is visible.
+                  '';
+                };
+                ui_base_path = mkOption {
+                  type = types.str;
+                  default = "/_matrix/maubot";
+                  description = mdDoc ''
+                    The base path for the UI.
+                  '';
+                };
+                plugin_base_path = mkOption {
+                  type = types.str;
+                  default = "${config.services.maubot.settings.server.ui_base_path}/plugin/";
+                  defaultText = literalExpression ''
+                    "''${config.services.maubot.settings.server.ui_base_path}/plugin/"
+                  '';
+                  description = mdDoc ''
+                    The base path for plugin endpoints. The instance ID will be appended directly.
+                  '';
+                };
+                override_resource_path = mkOption {
+                  type = types.nullOr types.str;
+                  default = null;
+                  description = mdDoc ''
+                    Override path from where to load UI resources.
+                  '';
+                };
+              };
+            };
+          };
+
+          homeservers = mkOption {
+            type = types.attrsOf (types.submodule {
+              options = {
+                url = mkOption {
+                  type = types.str;
+                  description = mdDoc ''
+                    Client-server API URL
+                  '';
+                };
+              };
+            });
+            default = {
+              "matrix.org" = {
+                url = "https://matrix-client.matrix.org";
+              };
+            };
+            description = mdDoc ''
+              Known homeservers. This is required for the `mbc auth` command and also allows more convenient access from the management UI.
+              If you want to specify registration secrets, pass this via extraConfigFile instead.
+            '';
+          };
+
+          admins = mkOption {
+            type = types.attrsOf types.str;
+            default = { root = ""; };
+            description = mdDoc ''
+              List of administrator users. Plaintext passwords will be bcrypted on startup. Set empty password
+              to prevent normal login. Root is a special user that can't have a password and will always exist.
+            '';
+          };
+
+          api_features = mkOption {
+            type = types.attrsOf bool;
+            default = {
+              login = true;
+              plugin = true;
+              plugin_upload = true;
+              instance = true;
+              instance_database = true;
+              client = true;
+              client_proxy = true;
+              client_auth = true;
+              dev_open = true;
+              log = true;
+            };
+            description = mdDoc ''
+              API feature switches.
+            '';
+          };
+
+          logging = mkOption {
+            type = types.attrs;
+            description = mdDoc ''
+              Python logging configuration. See [section 16.7.2 of the Python
+              documentation](https://docs.python.org/3.6/library/logging.config.html#configuration-dictionary-schema)
+              for more info.
+            '';
+            default = {
+              version = 1;
+              formatters = {
+                colored = {
+                  "()" = "maubot.lib.color_log.ColorFormatter";
+                  format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
+                };
+                normal = {
+                  format = "[%(asctime)s] [%(levelname)s@%(name)s] %(message)s";
+                };
+              };
+              handlers = {
+                file = {
+                  class = "logging.handlers.RotatingFileHandler";
+                  formatter = "normal";
+                  filename = "./maubot.log";
+                  maxBytes = 10485760;
+                  backupCount = 10;
+                };
+                console = {
+                  class = "logging.StreamHandler";
+                  formatter = "colored";
+                };
+              };
+              loggers = {
+                maubot = {
+                  level = "DEBUG";
+                };
+                mau = {
+                  level = "DEBUG";
+                };
+                aiohttp = {
+                  level = "INFO";
+                };
+              };
+              root = {
+                level = "DEBUG";
+                handlers = [ "file" "console" ];
+              };
+            };
+          };
+        };
+      };
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    warnings = lib.optional (builtins.any (x: x.username != x.database) parsedLocalPostgresDBs) ''
+      The Maubot database username doesn't match the database name! This means the user won't be automatically
+      granted ownership of the database. Consider changing either the username or the database name.
+    '';
+    assertions = [
+      {
+        assertion = builtins.all (x: !lib.hasInfix ":" x.username) parsedPostgresDBs;
+        message = ''
+          Putting database passwords in your Nix config makes them world-readable. To securely put passwords
+          in your Maubot config, change /var/lib/maubot/config.yaml after running Maubot at least once as
+          described in the NixOS manual.
+        '';
+      }
+      {
+        assertion = hasLocalPostgresDB -> config.services.postgresql.enable;
+        message = ''
+          Cannot deploy maubot with a configuration for a local postgresql database and a missing postgresql service.
+        '';
+      }
+    ];
+
+    services.postgresql = lib.mkIf hasLocalPostgresDB {
+      enable = true;
+      ensureDatabases = map (x: x.database) parsedLocalPostgresDBs;
+      ensureUsers = lib.flip map parsedLocalPostgresDBs (x: {
+        name = x.username;
+        ensureDBOwnership = lib.mkIf (x.username == x.database) true;
+      });
+    };
+
+    users.users.maubot = {
+      group = "maubot";
+      home = cfg.dataDir;
+      # otherwise StateDirectory is enough
+      createHome = lib.mkIf (cfg.dataDir != "/var/lib/maubot") true;
+      isSystemUser = true;
+    };
+
+    users.groups.maubot = { };
+
+    systemd.services.maubot = rec {
+      description = "maubot - a plugin-based Matrix bot system written in Python";
+      after = [ "network.target" ] ++ wants ++ lib.optional hasLocalPostgresDB "postgresql.service";
+      # all plugins get automatically disabled if maubot starts before synapse
+      wants = lib.optional config.services.matrix-synapse.enable config.services.matrix-synapse.serviceUnit;
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        if [ ! -f "${cfg.extraConfigFile}" ]; then
+          echo "server:" > "${cfg.extraConfigFile}"
+          echo "    unshared_secret: $(head -c40 /dev/random | base32 | ${pkgs.gawk}/bin/awk '{print tolower($0)}')" > "${cfg.extraConfigFile}"
+          chmod 640 "${cfg.extraConfigFile}"
+        fi
+      '';
+
+      serviceConfig = {
+        ExecStart = "${finalPackage}/bin/maubot --config ${cfg.extraConfigFile}" + lib.optionalString (!cfg.configMutable) " --no-update";
+        User = "maubot";
+        Group = "maubot";
+        Restart = "on-failure";
+        RestartSec = "10s";
+        StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/maubot") "maubot";
+        WorkingDirectory = cfg.dataDir;
+      };
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ chayleaf ];
+  meta.doc = ./maubot.md;
+}
diff --git a/nixos/modules/services/misc/forgejo.md b/nixos/modules/services/misc/forgejo.md
index 3df8bc20976a..14b21933e6b0 100644
--- a/nixos/modules/services/misc/forgejo.md
+++ b/nixos/modules/services/misc/forgejo.md
@@ -20,7 +20,7 @@ If you experience issues with your instance using `services.gitea`,
 
 ::: {.note}
 Migrating is, while not strictly necessary at this point, highly recommended.
-Both modules and projects are likely to divide further with each release.
+Both modules and projects are likely to diverge further with each release.
 Which might lead to an even more involved migration.
 :::
 
diff --git a/nixos/modules/services/misc/sourcehut/default.nix b/nixos/modules/services/misc/sourcehut/default.nix
index d44219008466..af9768c98ac3 100644
--- a/nixos/modules/services/misc/sourcehut/default.nix
+++ b/nixos/modules/services/misc/sourcehut/default.nix
@@ -1325,6 +1325,11 @@ in
     (import ./service.nix "paste" {
       inherit configIniOfService;
       port = 5011;
+      extraServices.pastesrht-api = {
+        serviceConfig.Restart = "always";
+        serviceConfig.RestartSec = "5s";
+        serviceConfig.ExecStart = "${pkgs.sourcehut.pastesrht}/bin/pastesrht-api -b ${cfg.listenAddress}:${toString (cfg.paste.port + 100)}";
+      };
     })
 
     (import ./service.nix "todo" {
diff --git a/nixos/modules/services/networking/x2goserver.nix b/nixos/modules/services/networking/x2goserver.nix
index 1242229a0b60..f1eba9fafc1c 100644
--- a/nixos/modules/services/networking/x2goserver.nix
+++ b/nixos/modules/services/networking/x2goserver.nix
@@ -160,5 +160,8 @@ in {
     security.sudo.extraConfig = ''
       Defaults  env_keep+=QT_GRAPHICSSYSTEM
     '';
+    security.sudo-rs.extraConfig = ''
+      Defaults  env_keep+=QT_GRAPHICSSYSTEM
+    '';
   };
 }
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
index c3893f4b09b2..72a195d3a04e 100644
--- a/nixos/modules/services/security/clamav.nix
+++ b/nixos/modules/services/security/clamav.nix
@@ -145,7 +145,8 @@ in
 
     systemd.services.clamav-daemon = mkIf cfg.daemon.enable {
       description = "ClamAV daemon (clamd)";
-      after = optional cfg.updater.enable "clamav-freshclam.service";
+      after = optionals cfg.updater.enable [ "clamav-freshclam.service" ];
+      wants = optionals cfg.updater.enable [ "clamav-freshclam.service" ];
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ clamdConfigFile ];
 
diff --git a/nixos/modules/system/boot/unl0kr.nix b/nixos/modules/system/boot/unl0kr.nix
new file mode 100644
index 000000000000..8d9af37382e0
--- /dev/null
+++ b/nixos/modules/system/boot/unl0kr.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.boot.initrd.unl0kr;
+in
+{
+  options.boot.initrd.unl0kr = {
+    enable = lib.mkEnableOption (lib.mdDoc "unl0kr in initrd") // {
+      description = lib.mdDoc ''
+        Whether to enable the unl0kr on-screen keyboard in initrd to unlock LUKS.
+      '';
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    meta.maintainers = with lib.maintainers; [ tomfitzhenry ];
+    assertions = [
+      {
+        assertion = cfg.enable -> config.boot.initrd.systemd.enable;
+        message = "boot.initrd.unl0kr is only supported with boot.initrd.systemd.";
+      }
+    ];
+
+    boot.initrd.systemd = {
+      storePaths = with pkgs; [
+        "${pkgs.gnugrep}/bin/grep"
+        libinput
+        xkeyboard_config
+        "${config.boot.initrd.systemd.package}/lib/systemd/systemd-reply-password"
+        "${pkgs.unl0kr}/bin/unl0kr"
+      ];
+      services = {
+        unl0kr-ask-password = {
+          description = "Forward Password Requests to unl0kr";
+          conflicts = [
+            "emergency.service"
+            "initrd-switch-root.target"
+            "shutdown.target"
+          ];
+          unitConfig.DefaultDependencies = false;
+          after = [
+            "systemd-vconsole-setup.service"
+            "udev.service"
+          ];
+          before = [
+            "shutdown.target"
+          ];
+          script = ''
+            # This script acts as a Password Agent: https://systemd.io/PASSWORD_AGENTS/
+
+            DIR=/run/systemd/ask-password/
+            # If a user has multiple encrypted disks, the requests might come in different times,
+            # so make sure to answer as many requests as we can. Once boot succeeds, other
+            # password agents will be responsible for watching for requests.
+            while [ -d $DIR ] && [ "$(ls -A $DIR/ask.*)" ];
+            do
+              for file in `ls $DIR/ask.*`; do
+                socket="$(cat "$file" | ${pkgs.gnugrep}/bin/grep "Socket=" | cut -d= -f2)"
+                ${pkgs.unl0kr}/bin/unl0kr | ${config.boot.initrd.systemd.package}/lib/systemd/systemd-reply-password 1 "$socket"
+              done
+            done
+          '';
+        };
+      };
+
+      paths = {
+        unl0kr-ask-password = {
+          description = "Forward Password Requests to unl0kr";
+          conflicts = [
+            "emergency.service"
+            "initrd-switch-root.target"
+            "shutdown.target"
+          ];
+          unitConfig.DefaultDependencies = false;
+          before = [
+            "shutdown.target"
+            "paths.target"
+            "cryptsetup.target"
+          ];
+          wantedBy = [ "sysinit.target" ];
+          pathConfig = {
+            DirectoryNotEmpty = "/run/systemd/ask-password";
+            MakeDirectory = true;
+          };
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 1e11cc220805..480439c2a25e 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -813,6 +813,7 @@ in {
   systemd-initrd-luks-empty-passphrase = handleTest ./initrd-luks-empty-passphrase.nix { systemdStage1 = true; };
   systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {};
   systemd-initrd-luks-tpm2 = handleTest ./systemd-initrd-luks-tpm2.nix {};
+  systemd-initrd-luks-unl0kr = handleTest ./systemd-initrd-luks-unl0kr.nix {};
   systemd-initrd-modprobe = handleTest ./systemd-initrd-modprobe.nix {};
   systemd-initrd-shutdown = handleTest ./systemd-shutdown.nix { systemdStage1 = true; };
   systemd-initrd-simple = handleTest ./systemd-initrd-simple.nix {};
diff --git a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
index e638f2e5b861..addc898bd760 100644
--- a/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
+++ b/nixos/tests/nextcloud/with-declarative-redis-and-secrets.nix
@@ -41,10 +41,13 @@ in {
         };
         secretFile = "/etc/nextcloud-secrets.json";
 
-        extraOptions.redis = {
-          dbindex = 0;
-          timeout = 1.5;
-          # password handled via secretfile below
+        extraOptions = {
+          allow_local_remote_servers = true;
+          redis = {
+            dbindex = 0;
+            timeout = 1.5;
+            # password handled via secretfile below
+          };
         };
         configureRedis = true;
       };
@@ -62,6 +65,7 @@ in {
 
       services.postgresql = {
         enable = true;
+        package = pkgs.postgresql_14;
       };
       systemd.services.postgresql.postStart = pkgs.lib.mkAfter ''
         password=$(cat ${passFile})
diff --git a/nixos/tests/nixops/default.nix b/nixos/tests/nixops/default.nix
index b8f747b2a19f..f7a26f2461c4 100644
--- a/nixos/tests/nixops/default.nix
+++ b/nixos/tests/nixops/default.nix
@@ -1,4 +1,6 @@
-{ pkgs, ... }:
+{ pkgs
+, testers
+, ... }:
 let
   inherit (pkgs) lib;
 
@@ -19,7 +21,7 @@ let
     passthru.override = args': testsForPackage (args // args');
   };
 
-  testLegacyNetwork = { nixopsPkg, ... }: pkgs.nixosTest ({
+  testLegacyNetwork = { nixopsPkg, ... }: testers.nixosTest ({
     name = "nixops-legacy-network";
     nodes = {
       deployer = { config, lib, nodes, pkgs, ... }: {
diff --git a/nixos/tests/systemd-initrd-luks-unl0kr.nix b/nixos/tests/systemd-initrd-luks-unl0kr.nix
new file mode 100644
index 000000000000..0658a098cfa2
--- /dev/null
+++ b/nixos/tests/systemd-initrd-luks-unl0kr.nix
@@ -0,0 +1,75 @@
+import ./make-test-python.nix ({ lib, pkgs, ... }: let
+  passphrase = "secret";
+in {
+  name = "systemd-initrd-luks-unl0kr";
+  meta = with pkgs.lib.maintainers; {
+    maintainers = [ tomfitzhenry ];
+  };
+
+  enableOCR = true;
+
+  nodes.machine = { pkgs, ... }: {
+    virtualisation = {
+      emptyDiskImages = [ 512 512 ];
+      useBootLoader = true;
+      mountHostNixStore = true;
+      useEFIBoot = true;
+      qemu.options = [
+        "-vga virtio"
+      ];
+    };
+    boot.loader.systemd-boot.enable = true;
+
+    boot.initrd.availableKernelModules = [
+      "evdev" # for entering pw
+      "bochs"
+    ];
+
+    environment.systemPackages = with pkgs; [ cryptsetup ];
+    boot.initrd = {
+      systemd = {
+        enable = true;
+        emergencyAccess = true;
+      };
+      unl0kr.enable = true;
+    };
+
+    specialisation.boot-luks.configuration = {
+      boot.initrd.luks.devices = lib.mkVMOverride {
+        # We have two disks and only type one password - key reuse is in place
+        cryptroot.device = "/dev/vdb";
+        cryptroot2.device = "/dev/vdc";
+      };
+      virtualisation.rootDevice = "/dev/mapper/cryptroot";
+      virtualisation.fileSystems."/".autoFormat = true;
+      # test mounting device unlocked in initrd after switching root
+      virtualisation.fileSystems."/cryptroot2".device = "/dev/mapper/cryptroot2";
+    };
+  };
+
+  testScript = ''
+    # Create encrypted volume
+    machine.wait_for_unit("multi-user.target")
+    machine.succeed("echo -n ${passphrase} | cryptsetup luksFormat -q --iter-time=1 /dev/vdb -")
+    machine.succeed("echo -n ${passphrase} | cryptsetup luksFormat -q --iter-time=1 /dev/vdc -")
+    machine.succeed("echo -n ${passphrase} | cryptsetup luksOpen   -q               /dev/vdc cryptroot2")
+    machine.succeed("mkfs.ext4 /dev/mapper/cryptroot2")
+
+    # Boot from the encrypted disk
+    machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks.conf")
+    machine.succeed("sync")
+    machine.crash()
+
+    # Boot and decrypt the disk
+    machine.start()
+    machine.wait_for_text("Password required for booting")
+    machine.screenshot("prompt")
+    machine.send_chars("${passphrase}")
+    machine.screenshot("pw")
+    machine.send_chars("\n")
+    machine.wait_for_unit("multi-user.target")
+
+    assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount"), "/dev/mapper/cryptroot do not appear in mountpoints list"
+    assert "/dev/mapper/cryptroot2 on /cryptroot2 type ext4" in machine.succeed("mount")
+  '';
+})
diff --git a/nixos/tests/systemd-timesyncd.nix b/nixos/tests/systemd-timesyncd.nix
index f38d06be1516..02f49f49b8a5 100644
--- a/nixos/tests/systemd-timesyncd.nix
+++ b/nixos/tests/systemd-timesyncd.nix
@@ -15,13 +15,14 @@ in {
       # create the path that should be migrated by our activation script when
       # upgrading to a newer nixos version
       system.stateVersion = "19.03";
-      systemd.tmpfiles.rules = [
-        "r /var/lib/systemd/timesync -"
-        "d /var/lib/systemd -"
-        "d /var/lib/private/systemd/timesync -"
-        "L /var/lib/systemd/timesync - - - - /var/lib/private/systemd/timesync"
-        "d /var/lib/private/systemd/timesync - systemd-timesync systemd-timesync -"
-      ];
+      systemd.tmpfiles.settings.systemd-timesyncd-test = {
+        "/var/lib/systemd/timesync".R = { };
+        "/var/lib/systemd/timesync".L.argument = "/var/lib/private/systemd/timesync";
+        "/var/lib/private/systemd/timesync".d = {
+          user = "systemd-timesync";
+          group = "systemd-timesync";
+        };
+      };
     });
   };