about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorpennae <82953136+pennae@users.noreply.github.com>2022-09-01 16:10:09 +0200
committerGitHub <noreply@github.com>2022-09-01 16:10:09 +0200
commit3bddcf5f9002814a8bec50025418faa3fd3f6138 (patch)
tree5ae3ebf10acce3ad00d4a2468f8e64c41a2c749b /nixos
parent1d41cff3dc4c8f37bb5841f51fcbff705e169178 (diff)
parente7312d54f184e5c3e0f1ef29028f6dae8fa34a97 (diff)
downloadnixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.tar
nixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.tar.gz
nixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.tar.bz2
nixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.tar.lz
nixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.tar.xz
nixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.tar.zst
nixlib-3bddcf5f9002814a8bec50025418faa3fd3f6138.zip
Merge branch 'master' into option-docs-md
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/from_md/release-notes/rl-2211.section.xml14
-rw-r--r--nixos/doc/manual/release-notes/rl-2211.section.md4
-rw-r--r--nixos/lib/qemu-common.nix62
-rw-r--r--nixos/modules/installer/tools/nix-fallback-paths.nix10
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/programs/gnupg.nix12
-rw-r--r--nixos/modules/programs/rust-motd.nix92
-rw-r--r--nixos/modules/services/cluster/k3s/default.nix56
-rw-r--r--nixos/modules/services/networking/kea.nix2
-rw-r--r--nixos/modules/services/networking/keepalived/default.nix26
-rw-r--r--nixos/modules/services/networking/searx.nix5
-rw-r--r--nixos/modules/services/networking/syncthing.nix10
-rw-r--r--nixos/modules/services/security/vaultwarden/default.nix1
-rw-r--r--nixos/modules/services/web-apps/keycloak.nix6
-rw-r--r--nixos/modules/services/web-apps/writefreely.nix485
-rw-r--r--nixos/modules/services/x11/display-managers/lightdm.nix1
-rw-r--r--nixos/modules/services/x11/window-managers/awesome.nix2
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py6
-rw-r--r--nixos/modules/system/boot/networkd.nix40
-rw-r--r--nixos/modules/virtualisation/containers.nix14
-rw-r--r--nixos/modules/virtualisation/cri-o.nix4
-rw-r--r--nixos/modules/virtualisation/podman/default.nix1
-rw-r--r--nixos/modules/virtualisation/qemu-vm.nix23
-rw-r--r--nixos/tests/all-tests.nix1
-rw-r--r--nixos/tests/k3s/multi-node.nix31
-rw-r--r--nixos/tests/web-apps/writefreely.nix44
26 files changed, 886 insertions, 68 deletions
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
index a951835a0764..e3c76918911a 100644
--- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
+++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml
@@ -258,6 +258,14 @@
           <link xlink:href="options.html#opt-services.patroni.enable">services.patroni</link>.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <link xlink:href="https://writefreely.org">WriteFreely</link>,
+          a simple blogging platform with ActivityPub support. Available
+          as
+          <link xlink:href="options.html#opt-services.writefreely.enable">services.writefreely</link>.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.11-incompatibilities">
@@ -428,6 +436,12 @@
           due to upstream dropping support.
         </para>
       </listitem>
+      <listitem>
+        <para>
+          <literal>k3s</literal> supports <literal>clusterInit</literal>
+          option, and it is enabled by default, for servers.
+        </para>
+      </listitem>
     </itemizedlist>
   </section>
   <section xml:id="sec-release-22.11-notable-changes">
diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md
index 73348007cb73..afeaa7aaac73 100644
--- a/nixos/doc/manual/release-notes/rl-2211.section.md
+++ b/nixos/doc/manual/release-notes/rl-2211.section.md
@@ -92,6 +92,8 @@ In addition to numerous new and upgraded packages, this release has the followin
 - [Patroni](https://github.com/zalando/patroni), a template for PostgreSQL HA with ZooKeeper, etcd or Consul.
 Available as [services.patroni](options.html#opt-services.patroni.enable).
 
+- [WriteFreely](https://writefreely.org), a simple blogging platform with ActivityPub support. Available as [services.writefreely](options.html#opt-services.writefreely.enable).
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 ## Backward Incompatibilities {#sec-release-22.11-incompatibilities}
@@ -150,6 +152,8 @@ Use `configure.packages` instead.
 
 - `k3s` no longer supports docker as runtime due to upstream dropping support.
 
+- `k3s` supports `clusterInit` option, and it is enabled by default, for servers.
+
 <!-- To avoid merge conflicts, consider adding your item at an arbitrary place in the list instead. -->
 
 ## Other Notable Changes {#sec-release-22.11-notable-changes}
diff --git a/nixos/lib/qemu-common.nix b/nixos/lib/qemu-common.nix
index fc3dcb24ab9c..3f4d674e9a93 100644
--- a/nixos/lib/qemu-common.nix
+++ b/nixos/lib/qemu-common.nix
@@ -4,29 +4,61 @@
 let
   zeroPad = n:
     lib.optionalString (n < 16) "0" +
-      (if n > 255
-       then throw "Can't have more than 255 nets or nodes!"
-       else lib.toHexString n);
+    (if n > 255
+    then throw "Can't have more than 255 nets or nodes!"
+    else lib.toHexString n);
 in
 
 rec {
   qemuNicMac = net: machine: "52:54:00:12:${zeroPad net}:${zeroPad machine}";
 
   qemuNICFlags = nic: net: machine:
-    [ "-device virtio-net-pci,netdev=vlan${toString nic},mac=${qemuNicMac net machine}"
+    [
+      "-device virtio-net-pci,netdev=vlan${toString nic},mac=${qemuNicMac net machine}"
       ''-netdev vde,id=vlan${toString nic},sock="$QEMU_VDE_SOCKET_${toString net}"''
     ];
 
-  qemuSerialDevice = if pkgs.stdenv.hostPlatform.isx86 || pkgs.stdenv.hostPlatform.isRiscV then "ttyS0"
-        else if (with pkgs.stdenv.hostPlatform; isAarch || isPower) then "ttyAMA0"
-        else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'";
+  qemuSerialDevice =
+    if pkgs.stdenv.hostPlatform.isx86 || pkgs.stdenv.hostPlatform.isRiscV then "ttyS0"
+    else if (with pkgs.stdenv.hostPlatform; isAarch || isPower) then "ttyAMA0"
+    else throw "Unknown QEMU serial device for system '${pkgs.stdenv.hostPlatform.system}'";
 
-  qemuBinary = qemuPkg: {
-    x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu max";
-    armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=kvm:tcg -cpu max";
-    aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=kvm:tcg -cpu max";
-    powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
-    powerpc64-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
-    x86_64-darwin = "${qemuPkg}/bin/qemu-kvm -cpu max";
-  }.${pkgs.stdenv.hostPlatform.system} or "${qemuPkg}/bin/qemu-kvm";
+  qemuBinary = qemuPkg:
+    let
+      hostStdenv = qemuPkg.stdenv;
+      hostSystem = hostStdenv.system;
+      guestSystem = pkgs.stdenv.hostPlatform.system;
+
+      linuxHostGuestMatrix = {
+        x86_64-linux = "${qemuPkg}/bin/qemu-kvm -cpu max";
+        armv7l-linux = "${qemuPkg}/bin/qemu-system-arm -machine virt,accel=kvm:tcg -cpu max";
+        aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=max,accel=kvm:tcg -cpu max";
+        powerpc64le-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
+        powerpc64-linux = "${qemuPkg}/bin/qemu-system-ppc64 -machine powernv";
+        x86_64-darwin = "${qemuPkg}/bin/qemu-kvm -cpu max";
+      };
+      otherHostGuestMatrix = {
+        aarch64-darwin = {
+          aarch64-linux = "${qemuPkg}/bin/qemu-system-aarch64 -machine virt,gic-version=2,accel=hvf:tcg -cpu max";
+        };
+        x86_64-darwin = {
+          x86_64-linux = "${qemuPkg}/bin/qemu-system-x86_64 -machine type=q35,accel=hvf:tcg -cpu max";
+        };
+      };
+
+      throwUnsupportedHostSystem =
+        let
+          supportedSystems = [ "linux" ] ++ (lib.attrNames otherHostGuestMatrix);
+        in
+        throw "Unsupported host system ${hostSystem}, supported: ${lib.concatStringsSep ", " supportedSystems}";
+      throwUnsupportedGuestSystem = guestMap:
+        throw "Unsupported guest system ${guestSystem} for host ${hostSystem}, supported: ${lib.concatStringsSep ", " (lib.attrNames guestMap)}";
+    in
+    if hostStdenv.isLinux then
+      linuxHostGuestMatrix.${guestSystem} or "${qemuPkg}/bin/qemu-kvm"
+    else
+      let
+        guestMap = (otherHostGuestMatrix.${hostSystem} or throwUnsupportedHostSystem);
+      in
+      (guestMap.${guestSystem} or (throwUnsupportedGuestSystem guestMap));
 }
diff --git a/nixos/modules/installer/tools/nix-fallback-paths.nix b/nixos/modules/installer/tools/nix-fallback-paths.nix
index 0035ceca6fc9..4700895cec8e 100644
--- a/nixos/modules/installer/tools/nix-fallback-paths.nix
+++ b/nixos/modules/installer/tools/nix-fallback-paths.nix
@@ -1,7 +1,7 @@
 {
-  x86_64-linux = "/nix/store/3af6g226v4hsv6x7xzh23d6wqyq0nzjp-nix-2.10.3";
-  i686-linux = "/nix/store/43xxh2jip6rpdhylc5z9a5fxx54dw206-nix-2.10.3";
-  aarch64-linux = "/nix/store/6qw3r57nra08ars8j8zyj3fl8lz4cvnd-nix-2.10.3";
-  x86_64-darwin = "/nix/store/3b7qrm0qjw57fmznrsvm0ai568i89hc2-nix-2.10.3";
-  aarch64-darwin = "/nix/store/gp7k17iy1n7hgf97qwnxw28c6v9nhb1i-nix-2.10.3";
+  x86_64-linux = "/nix/store/nmq5zcd93qb1yskx42rs910ff0247nn2-nix-2.11.0";
+  i686-linux = "/nix/store/ja6im1sw9a8lzczi10lc0iddffl9kzmn-nix-2.11.0";
+  aarch64-linux = "/nix/store/myr6fcqa9y4y2fb83zz73dck52vcn81z-nix-2.11.0";
+  x86_64-darwin = "/nix/store/2pfjz9b22k9997gh7cb0hjk1qa4lxrvy-nix-2.11.0";
+  aarch64-darwin = "/nix/store/lr32i0bdarx1iqsch4sy24jj1jkfw9vf-nix-2.11.0";
 }
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 6e95c45f0d56..cb3599589cfe 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -204,6 +204,7 @@
   ./programs/plotinus.nix
   ./programs/proxychains.nix
   ./programs/qt5ct.nix
+  ./programs/rust-motd.nix
   ./programs/screen.nix
   ./programs/sedutil.nix
   ./programs/seahorse.nix
@@ -1119,6 +1120,7 @@
   ./services/web-apps/wiki-js.nix
   ./services/web-apps/whitebophir.nix
   ./services/web-apps/wordpress.nix
+  ./services/web-apps/writefreely.nix
   ./services/web-apps/youtrack.nix
   ./services/web-apps/zabbix.nix
   ./services/web-servers/agate.nix
diff --git a/nixos/modules/programs/gnupg.nix b/nixos/modules/programs/gnupg.nix
index 1028ef53bae1..1a9006aad14e 100644
--- a/nixos/modules/programs/gnupg.nix
+++ b/nixos/modules/programs/gnupg.nix
@@ -129,12 +129,14 @@ in
     environment.interactiveShellInit = ''
       # Bind gpg-agent to this TTY if gpg commands are used.
       export GPG_TTY=$(tty)
+    '';
 
-    '' + (optionalString cfg.agent.enableSSHSupport ''
-      # SSH agent protocol doesn't support changing TTYs, so bind the agent
-      # to every new TTY.
-      ${cfg.package}/bin/gpg-connect-agent --quiet updatestartuptty /bye > /dev/null
-    '');
+    programs.ssh.extraConfig = optionalString cfg.agent.enableSSHSupport ''
+      # The SSH agent protocol doesn't have support for changing TTYs; however we
+      # can simulate this with the `exec` feature of openssh (see ssh_config(5))
+      # that hooks a command to the shell currently running the ssh program.
+      Match host * exec "${cfg.package}/bin/gpg-connect-agent --quiet updatestartuptty /bye > /dev/null"
+    '';
 
     environment.extraInit = mkIf cfg.agent.enableSSHSupport ''
       if [ -z "$SSH_AUTH_SOCK" ]; then
diff --git a/nixos/modules/programs/rust-motd.nix b/nixos/modules/programs/rust-motd.nix
new file mode 100644
index 000000000000..671e701cd195
--- /dev/null
+++ b/nixos/modules/programs/rust-motd.nix
@@ -0,0 +1,92 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.programs.rust-motd;
+  format = pkgs.formats.toml { };
+in {
+  options.programs.rust-motd = {
+    enable = mkEnableOption "rust-motd";
+    enableMotdInSSHD = mkOption {
+      default = true;
+      type = types.bool;
+      description = mdDoc ''
+        Whether to let `openssh` print the
+        result when entering a new `ssh`-session.
+        By default either nothing or a static file defined via
+        [](#opt-users.motd) is printed. Because of that,
+        the latter option is incompatible with this module.
+      '';
+    };
+    refreshInterval = mkOption {
+      default = "*:0/5";
+      type = types.str;
+      description = mdDoc ''
+        Interval in which the {manpage}`motd(5)` file is refreshed.
+        For possible formats, please refer to {manpage}`systemd.time(7)`.
+      '';
+    };
+    settings = mkOption {
+      type = types.submodule {
+        freeformType = format.type;
+      };
+      description = mdDoc ''
+        Settings on what to generate. Please read the
+        [upstream documentation](https://github.com/rust-motd/rust-motd/blob/main/README.md#configuration)
+        for further information.
+      '';
+    };
+  };
+  config = mkIf cfg.enable {
+    assertions = [
+      { assertion = config.users.motd == null;
+        message = ''
+          `programs.rust-motd` is incompatible with `users.motd`!
+        '';
+      }
+    ];
+    systemd.services.rust-motd = {
+      path = with pkgs; [ bash ];
+      documentation = [ "https://github.com/rust-motd/rust-motd/blob/v${pkgs.rust-motd.version}/README.md" ];
+      description = "motd generator";
+      serviceConfig = {
+        ExecStart = "${pkgs.writeShellScript "update-motd" ''
+          ${pkgs.rust-motd}/bin/rust-motd ${format.generate "motd.conf" cfg.settings} > motd
+        ''}";
+        CapabilityBoundingSet = [ "" ];
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectClock = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectHostname = true;
+        ProtectKernelModules = true;
+        ProtectKernelLogs = true;
+        ProtectKernelTunables = true;
+        ProtectSystem = "full";
+        StateDirectory = "rust-motd";
+        RestrictAddressFamilies = "none";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        RestrictSUIDSGID = true;
+        RemoveIPC = true;
+        WorkingDirectory = "/var/lib/rust-motd";
+      };
+    };
+    systemd.timers.rust-motd = {
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnCalendar = cfg.refreshInterval;
+    };
+    security.pam.services.sshd.text = mkIf cfg.enableMotdInSSHD (mkDefault (mkAfter ''
+      session optional ${pkgs.pam}/lib/security/pam_motd.so motd=/var/lib/rust-motd/motd
+    ''));
+    services.openssh.extraConfig = mkIf (cfg.settings ? last_login && cfg.settings.last_login != {}) ''
+      PrintLastLog no
+    '';
+  };
+  meta.maintainers = with maintainers; [ ma27 ];
+}
diff --git a/nixos/modules/services/cluster/k3s/default.nix b/nixos/modules/services/cluster/k3s/default.nix
index 0876f4ad955d..693f388de14a 100644
--- a/nixos/modules/services/cluster/k3s/default.nix
+++ b/nixos/modules/services/cluster/k3s/default.nix
@@ -25,7 +25,17 @@ in
     role = mkOption {
       description = lib.mdDoc ''
         Whether k3s should run as a server or agent.
-        Note that the server, by default, also runs as an agent.
+
+        If it's a server:
+
+        - By default it also runs workloads as an agent.
+        - Starts by default as a standalone server using an embedded sqlite datastore.
+        - Configure `clusterInit = true` to switch over to embedded etcd datastore and enable HA mode.
+        - Configure `serverAddr` to join an already-initialized HA cluster.
+
+        If it's an agent:
+
+        - `serverAddr` is required.
       '';
       default = "server";
       type = types.enum [ "server" "agent" ];
@@ -33,15 +43,44 @@ in
 
     serverAddr = mkOption {
       type = types.str;
-      description = lib.mdDoc "The k3s server to connect to. This option only makes sense for an agent.";
+      description = lib.mdDoc ''
+        The k3s server to connect to.
+
+        Servers and agents need to communicate each other. Read
+        [the networking docs](https://rancher.com/docs/k3s/latest/en/installation/installation-requirements/#networking)
+        to know how to configure the firewall.
+      '';
       example = "https://10.0.0.10:6443";
       default = "";
     };
 
+    clusterInit = mkOption {
+      type = types.bool;
+      default = false;
+      description = lib.mdDoc ''
+        Initialize HA cluster using an embedded etcd datastore.
+
+        If this option is `false` and `role` is `server`
+
+        On a server that was using the default embedded sqlite backend,
+        enabling this option will migrate to an embedded etcd DB.
+
+        If an HA cluster using the embedded etcd datastore was already initialized,
+        this option has no effect.
+
+        This option only makes sense in a server that is not connecting to another server.
+
+        If you are configuring an HA cluster with an embedded etcd,
+        the 1st server must have `clusterInit = true`
+        and other servers must connect to it using `serverAddr`.
+      '';
+    };
+
     token = mkOption {
       type = types.str;
       description = lib.mdDoc ''
-        The k3s token to use when connecting to the server. This option only makes sense for an agent.
+        The k3s token to use when connecting to a server.
+
         WARNING: This option will expose store your token unencrypted world-readable in the nix store.
         If this is undesired use the tokenFile option instead.
       '';
@@ -50,7 +89,7 @@ in
 
     tokenFile = mkOption {
       type = types.nullOr types.path;
-      description = lib.mdDoc "File path containing k3s token to use when connecting to the server. This option only makes sense for an agent.";
+      description = lib.mdDoc "File path containing k3s token to use when connecting to the server.";
       default = null;
     };
 
@@ -86,6 +125,14 @@ in
         assertion = cfg.role == "agent" -> cfg.configPath != null || cfg.tokenFile != null || cfg.token != "";
         message = "token or tokenFile or configPath (with 'token' or 'token-file' keys) should be set if role is 'agent'";
       }
+      {
+        assertion = cfg.role == "agent" -> !cfg.disableAgent;
+        message = "disableAgent must be false if role is 'agent'";
+      }
+      {
+        assertion = cfg.role == "agent" -> !cfg.clusterInit;
+        message = "clusterInit must be false if role is 'agent'";
+      }
     ];
 
     environment.systemPackages = [ config.services.k3s.package ];
@@ -111,6 +158,7 @@ in
           [
             "${cfg.package}/bin/k3s ${cfg.role}"
           ]
+          ++ (optional cfg.clusterInit "--cluster-init")
           ++ (optional cfg.disableAgent "--disable-agent")
           ++ (optional (cfg.serverAddr != "") "--server ${cfg.serverAddr}")
           ++ (optional (cfg.token != "") "--token ${cfg.token}")
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
index 8a98c0ceafc6..f39b149dd609 100644
--- a/nixos/modules/services/networking/kea.nix
+++ b/nixos/modules/services/networking/kea.nix
@@ -298,7 +298,7 @@ in
       ];
 
       serviceConfig = {
-        ExecStart = "${package}/bin/kea-ctrl-agent -c /etc/kea/ctrl-agent.conf ${lib.escapeShellArgs cfg.dhcp4.extraArgs}";
+        ExecStart = "${package}/bin/kea-ctrl-agent -c /etc/kea/ctrl-agent.conf ${lib.escapeShellArgs cfg.ctrl-agent.extraArgs}";
         KillMode = "process";
         Restart = "on-failure";
       } // commonServiceConfig;
diff --git a/nixos/modules/services/networking/keepalived/default.nix b/nixos/modules/services/networking/keepalived/default.nix
index 768c8e4b13c7..1ab25c879916 100644
--- a/nixos/modules/services/networking/keepalived/default.nix
+++ b/nixos/modules/services/networking/keepalived/default.nix
@@ -264,6 +264,19 @@ in
         '';
       };
 
+      secretFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        example = "/run/keys/keepalived.env";
+        description = ''
+          Environment variables from this file will be interpolated into the
+          final config file using envsubst with this syntax: <literal>$ENVIRONMENT</literal>
+          or <literal>''${VARIABLE}</literal>.
+          The file should contain lines formatted as <literal>SECRET_VAR=SECRET_VALUE</literal>.
+          This is useful to avoid putting secrets into the nix store.
+        '';
+      };
+
     };
   };
 
@@ -282,7 +295,9 @@ in
       };
     };
 
-    systemd.services.keepalived = {
+    systemd.services.keepalived = let
+      finalConfigFile = if cfg.secretFile == null then keepalivedConf else "/run/keepalived/keepalived.conf";
+    in {
       description = "Keepalive Daemon (LVS and VRRP)";
       after = [ "network.target" "network-online.target" "syslog.target" ];
       wants = [ "network-online.target" ];
@@ -290,8 +305,15 @@ in
         Type = "forking";
         PIDFile = pidFile;
         KillMode = "process";
+        RuntimeDirectory = "keepalived";
+        EnvironmentFile = lib.optional (cfg.secretFile != null) cfg.secretFile;
+        ExecStartPre = lib.optional (cfg.secretFile != null)
+        (pkgs.writeShellScript "keepalived-pre-start" ''
+          umask 077
+          ${pkgs.envsubst}/bin/envsubst -i "${keepalivedConf}" > ${finalConfigFile}
+        '');
         ExecStart = "${pkgs.keepalived}/sbin/keepalived"
-          + " -f ${keepalivedConf}"
+          + " -f ${finalConfigFile}"
           + " -p ${pidFile}"
           + optionalString cfg.snmp.enable " --snmp";
         ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
diff --git a/nixos/modules/services/networking/searx.nix b/nixos/modules/services/networking/searx.nix
index e594d0083d4f..214b6c6a787a 100644
--- a/nixos/modules/services/networking/searx.nix
+++ b/nixos/modules/services/networking/searx.nix
@@ -192,7 +192,10 @@ in
         ExecStart = "${cfg.package}/bin/searx-run";
       } // optionalAttrs (cfg.environmentFile != null)
         { EnvironmentFile = builtins.toPath cfg.environmentFile; };
-      environment.SEARX_SETTINGS_PATH = cfg.settingsFile;
+      environment = {
+        SEARX_SETTINGS_PATH = cfg.settingsFile;
+        SEARXNG_SETTINGS_PATH = cfg.settingsFile;
+      };
     };
 
     systemd.services.uwsgi = mkIf (cfg.runInUwsgi)
diff --git a/nixos/modules/services/networking/syncthing.nix b/nixos/modules/services/networking/syncthing.nix
index 26641618fb43..0b6b4bf9e5c7 100644
--- a/nixos/modules/services/networking/syncthing.nix
+++ b/nixos/modules/services/networking/syncthing.nix
@@ -268,10 +268,10 @@ in {
                   {
                     versioning = {
                       type = "staggered";
+                      fsPath = "/syncthing/backup";
                       params = {
                         cleanInterval = "3600";
                         maxAge = "31536000";
-                        versionsPath = "/syncthing/backup";
                       };
                     };
                   }
@@ -296,6 +296,14 @@ in {
                       See <https://docs.syncthing.net/users/versioning.html>.
                     '';
                   };
+                  fsPath = mkOption {
+                    default = "";
+                    type = either str path;
+                    description = mdDoc ''
+                      Path to the versioning folder.
+                      See <https://docs.syncthing.net/users/versioning.html>.
+                    '';
+                  };
                   params = mkOption {
                     type = attrsOf (either str path);
                     description = mdDoc ''
diff --git a/nixos/modules/services/security/vaultwarden/default.nix b/nixos/modules/services/security/vaultwarden/default.nix
index 0df0e5d211bb..7e4863dd871e 100644
--- a/nixos/modules/services/security/vaultwarden/default.nix
+++ b/nixos/modules/services/security/vaultwarden/default.nix
@@ -196,6 +196,7 @@ in {
         ProtectSystem = "strict";
         AmbientCapabilities = "CAP_NET_BIND_SERVICE";
         StateDirectory = "bitwarden_rs";
+        StateDirectoryMode = "0700";
       };
       wantedBy = [ "multi-user.target" ];
     };
diff --git a/nixos/modules/services/web-apps/keycloak.nix b/nixos/modules/services/web-apps/keycloak.nix
index 301efe7e5d94..da53d4ea76f4 100644
--- a/nixos/modules/services/web-apps/keycloak.nix
+++ b/nixos/modules/services/web-apps/keycloak.nix
@@ -25,6 +25,7 @@ let
     catAttrs
     collect
     splitString
+    hasPrefix
     ;
 
   inherit (builtins)
@@ -312,8 +313,9 @@ in
 
             http-relative-path = mkOption {
               type = str;
-              default = "";
+              default = "/";
               example = "/auth";
+              apply = x: if !(hasPrefix "/") x then "/" + x else x;
               description = lib.mdDoc ''
                 The path relative to `/` for serving
                 resources.
@@ -636,7 +638,7 @@ in
             '' + ''
               export KEYCLOAK_ADMIN=admin
               export KEYCLOAK_ADMIN_PASSWORD=${cfg.initialAdminPassword}
-              kc.sh start
+              kc.sh start --optimized
             '';
           };
 
diff --git a/nixos/modules/services/web-apps/writefreely.nix b/nixos/modules/services/web-apps/writefreely.nix
new file mode 100644
index 000000000000..c363760d5c2d
--- /dev/null
+++ b/nixos/modules/services/web-apps/writefreely.nix
@@ -0,0 +1,485 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (builtins) toString;
+  inherit (lib) types mkIf mkOption mkDefault;
+  inherit (lib) optional optionals optionalAttrs optionalString;
+
+  inherit (pkgs) sqlite;
+
+  format = pkgs.formats.ini {
+    mkKeyValue = key: value:
+      let
+        value' = if builtins.isNull value then
+          ""
+        else if builtins.isBool value then
+          if value == true then "true" else "false"
+        else
+          toString value;
+      in "${key} = ${value'}";
+  };
+
+  cfg = config.services.writefreely;
+
+  isSqlite = cfg.database.type == "sqlite3";
+  isMysql = cfg.database.type == "mysql";
+  isMysqlLocal = isMysql && cfg.database.createLocally == true;
+
+  hostProtocol = if cfg.acme.enable then "https" else "http";
+
+  settings = cfg.settings // {
+    app = cfg.settings.app or { } // {
+      host = cfg.settings.app.host or "${hostProtocol}://${cfg.host}";
+    };
+
+    database = if cfg.database.type == "sqlite3" then {
+      type = "sqlite3";
+      filename = cfg.settings.database.filename or "writefreely.db";
+      database = cfg.database.name;
+    } else {
+      type = "mysql";
+      username = cfg.database.user;
+      password = "#dbpass#";
+      database = cfg.database.name;
+      host = cfg.database.host;
+      port = cfg.database.port;
+      tls = cfg.database.tls;
+    };
+
+    server = cfg.settings.server or { } // {
+      bind = cfg.settings.server.bind or "localhost";
+      gopher_port = cfg.settings.server.gopher_port or 0;
+      autocert = !cfg.nginx.enable && cfg.acme.enable;
+      templates_parent_dir =
+        cfg.settings.server.templates_parent_dir or cfg.package.src;
+      static_parent_dir = cfg.settings.server.static_parent_dir or assets;
+      pages_parent_dir =
+        cfg.settings.server.pages_parent_dir or cfg.package.src;
+      keys_parent_dir = cfg.settings.server.keys_parent_dir or cfg.stateDir;
+    };
+  };
+
+  configFile = format.generate "config.ini" settings;
+
+  assets = pkgs.stdenvNoCC.mkDerivation {
+    pname = "writefreely-assets";
+
+    inherit (cfg.package) version src;
+
+    nativeBuildInputs = with pkgs.nodePackages; [ less ];
+
+    buildPhase = ''
+      mkdir -p $out
+
+      cp -r static $out/
+    '';
+
+    installPhase = ''
+      less_dir=$src/less
+      css_dir=$out/static/css
+
+      lessc $less_dir/app.less $css_dir/write.css
+      lessc $less_dir/fonts.less $css_dir/fonts.css
+      lessc $less_dir/icons.less $css_dir/icons.css
+      lessc $less_dir/prose.less $css_dir/prose.css
+    '';
+  };
+
+  withConfigFile = text: ''
+    db_pass=${
+      optionalString (cfg.database.passwordFile != null)
+      "$(head -n1 ${cfg.database.passwordFile})"
+    }
+
+    cp -f ${configFile} '${cfg.stateDir}/config.ini'
+    sed -e "s,#dbpass#,$db_pass,g" -i '${cfg.stateDir}/config.ini'
+    chmod 440 '${cfg.stateDir}/config.ini'
+
+    ${text}
+  '';
+
+  withMysql = text:
+    withConfigFile ''
+      query () {
+        local result=$(${config.services.mysql.package}/bin/mysql \
+          --user=${cfg.database.user} \
+          --password=$db_pass \
+          --database=${cfg.database.name} \
+          --silent \
+          --raw \
+          --skip-column-names \
+          --execute "$1" \
+        )
+
+        echo $result
+      }
+
+      ${text}
+    '';
+
+  withSqlite = text:
+    withConfigFile ''
+      query () {
+        local result=$(${sqlite}/bin/sqlite3 \
+          '${cfg.stateDir}/${settings.database.filename}'
+          "$1" \
+        )
+
+        echo $result
+      }
+
+      ${text}
+    '';
+in {
+  options.services.writefreely = {
+    enable =
+      lib.mkEnableOption "Writefreely, build a digital writing community";
+
+    package = lib.mkOption {
+      type = lib.types.package;
+      default = pkgs.writefreely;
+      defaultText = lib.literalExpression "pkgs.writefreely";
+      description = "Writefreely package to use.";
+    };
+
+    stateDir = mkOption {
+      type = types.path;
+      default = "/var/lib/writefreely";
+      description = "The state directory where keys and data are stored.";
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "writefreely";
+      description = "User under which Writefreely is ran.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "writefreely";
+      description = "Group under which Writefreely is ran.";
+    };
+
+    host = mkOption {
+      type = types.str;
+      default = "";
+      description = "The public host name to serve.";
+      example = "example.com";
+    };
+
+    settings = mkOption {
+      default = { };
+      description = ''
+        Writefreely configuration (<filename>config.ini</filename>). Refer to
+        <link xlink:href="https://writefreely.org/docs/latest/admin/config" />
+        for details.
+      '';
+
+      type = types.submodule {
+        freeformType = format.type;
+
+        options = {
+          app = {
+            theme = mkOption {
+              type = types.str;
+              default = "write";
+              description = "The theme to apply.";
+            };
+          };
+
+          server = {
+            port = mkOption {
+              type = types.port;
+              default = if cfg.nginx.enable then 18080 else 80;
+              defaultText = "80";
+              description = "The port WriteFreely should listen on.";
+            };
+          };
+        };
+      };
+    };
+
+    database = {
+      type = mkOption {
+        type = types.enum [ "sqlite3" "mysql" ];
+        default = "sqlite3";
+        description = "The database provider to use.";
+      };
+
+      name = mkOption {
+        type = types.str;
+        default = "writefreely";
+        description = "The name of the database to store data in.";
+      };
+
+      user = mkOption {
+        type = types.nullOr types.str;
+        default = if cfg.database.type == "mysql" then "writefreely" else null;
+        defaultText = "writefreely";
+        description = "The database user to connect as.";
+      };
+
+      passwordFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "The file to load the database password from.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "The database host to connect to.";
+      };
+
+      port = mkOption {
+        type = types.port;
+        default = 3306;
+        description = "The port used when connecting to the database host.";
+      };
+
+      tls = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Whether or not TLS should be used for the database connection.";
+      };
+
+      migrate = mkOption {
+        type = types.bool;
+        default = true;
+        description =
+          "Whether or not to automatically run migrations on startup.";
+      };
+
+      createLocally = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          When <option>services.writefreely.database.type</option> is set to
+          <code>"mysql"</code>, this option will enable the MySQL service locally.
+        '';
+      };
+    };
+
+    admin = {
+      name = mkOption {
+        type = types.nullOr types.str;
+        description = "The name of the first admin user.";
+        default = null;
+      };
+
+      initialPasswordFile = mkOption {
+        type = types.path;
+        description = ''
+          Path to a file containing the initial password for the admin user.
+          If not provided, the default password will be set to <code>nixos</code>.
+        '';
+        default = pkgs.writeText "default-admin-pass" "nixos";
+        defaultText = "/nix/store/xxx-default-admin-pass";
+      };
+    };
+
+    nginx = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Whether or not to enable and configure nginx as a proxy for WriteFreely.";
+      };
+
+      forceSSL = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether or not to force the use of SSL.";
+      };
+    };
+
+    acme = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          "Whether or not to automatically fetch and configure SSL certs.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    assertions = [
+      {
+        assertion = cfg.host != "";
+        message = "services.writefreely.host must be set";
+      }
+      {
+        assertion = isMysqlLocal -> cfg.database.passwordFile != null;
+        message =
+          "services.writefreely.database.passwordFile must be set if services.writefreely.database.createLocally is set to true";
+      }
+      {
+        assertion = isSqlite -> !cfg.database.createLocally;
+        message =
+          "services.writefreely.database.createLocally has no use when services.writefreely.database.type is set to sqlite3";
+      }
+    ];
+
+    users = {
+      users = optionalAttrs (cfg.user == "writefreely") {
+        writefreely = {
+          group = cfg.group;
+          home = cfg.stateDir;
+          isSystemUser = true;
+        };
+      };
+
+      groups =
+        optionalAttrs (cfg.group == "writefreely") { writefreely = { }; };
+    };
+
+    systemd.tmpfiles.rules =
+      [ "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" ];
+
+    systemd.services.writefreely = {
+      after = [ "network.target" ]
+        ++ optional isSqlite "writefreely-sqlite-init.service"
+        ++ optional isMysql "writefreely-mysql-init.service"
+        ++ optional isMysqlLocal "mysql.service";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "simple";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.stateDir;
+        Restart = "always";
+        RestartSec = 20;
+        ExecStart =
+          "${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' serve";
+        AmbientCapabilities =
+          optionalString (settings.server.port < 1024) "cap_net_bind_service";
+      };
+
+      preStart = ''
+        if ! test -d "${cfg.stateDir}/keys"; then
+          mkdir -p ${cfg.stateDir}/keys
+
+          # Key files end up with the wrong permissions by default.
+          # We need to correct them so that Writefreely can read them.
+          chmod -R 750 "${cfg.stateDir}/keys"
+
+          ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' keys generate
+        fi
+      '';
+    };
+
+    systemd.services.writefreely-sqlite-init = mkIf isSqlite {
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.stateDir;
+        ReadOnlyPaths = optional (cfg.admin.initialPasswordFile != null)
+          cfg.admin.initialPasswordFile;
+      };
+
+      script = let
+        migrateDatabase = optionalString cfg.database.migrate ''
+          ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
+        '';
+
+        createAdmin = optionalString (cfg.admin.name != null) ''
+          if [[ $(query "SELECT COUNT(*) FROM users") == 0 ]]; then
+            admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
+
+            ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
+          fi
+        '';
+      in withSqlite ''
+        if ! test -f '${settings.database.filename}'; then
+          ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
+        fi
+
+        ${migrateDatabase}
+
+        ${createAdmin}
+      '';
+    };
+
+    systemd.services.writefreely-mysql-init = mkIf isMysql {
+      wantedBy = [ "multi-user.target" ];
+      after = optional isMysqlLocal "mysql.service";
+
+      serviceConfig = {
+        Type = "oneshot";
+        User = cfg.user;
+        Group = cfg.group;
+        WorkingDirectory = cfg.stateDir;
+        ReadOnlyPaths = optional isMysqlLocal cfg.database.passwordFile
+          ++ optional (cfg.admin.initialPasswordFile != null)
+          cfg.admin.initialPasswordFile;
+      };
+
+      script = let
+        updateUser = optionalString isMysqlLocal ''
+          # WriteFreely currently *requires* a password for authentication, so we
+          # need to update the user in MySQL accordingly. By default MySQL users
+          # authenticate with auth_socket or unix_socket.
+          # See: https://github.com/writefreely/writefreely/issues/568
+          ${config.services.mysql.package}/bin/mysql --skip-column-names --execute "ALTER USER '${cfg.database.user}'@'localhost' IDENTIFIED VIA unix_socket OR mysql_native_password USING PASSWORD('$db_pass'); FLUSH PRIVILEGES;"
+        '';
+
+        migrateDatabase = optionalString cfg.database.migrate ''
+          ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db migrate
+        '';
+
+        createAdmin = optionalString (cfg.admin.name != null) ''
+          if [[ $(query 'SELECT COUNT(*) FROM users') == 0 ]]; then
+            admin_pass=$(head -n1 ${cfg.admin.initialPasswordFile})
+            ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' --create-admin ${cfg.admin.name}:$admin_pass
+          fi
+        '';
+      in withMysql ''
+        ${updateUser}
+
+        if [[ $(query "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${cfg.database.name}'") == 0 ]]; then
+          ${cfg.package}/bin/writefreely -c '${cfg.stateDir}/config.ini' db init
+        fi
+
+        ${migrateDatabase}
+
+        ${createAdmin}
+      '';
+    };
+
+    services.mysql = mkIf isMysqlLocal {
+      enable = true;
+      package = mkDefault pkgs.mariadb;
+      ensureDatabases = [ cfg.database.name ];
+      ensureUsers = [{
+        name = cfg.database.user;
+        ensurePermissions = {
+          "${cfg.database.name}.*" = "ALL PRIVILEGES";
+          # WriteFreely requires the use of passwords, so we need permissions
+          # to `ALTER` the user to add password support and also to reload
+          # permissions so they can be used.
+          "*.*" = "CREATE USER, RELOAD";
+        };
+      }];
+    };
+
+    services.nginx = lib.mkIf cfg.nginx.enable {
+      enable = true;
+      recommendedProxySettings = true;
+
+      virtualHosts."${cfg.host}" = {
+        enableACME = cfg.acme.enable;
+        forceSSL = cfg.nginx.forceSSL;
+
+        locations."/" = {
+          proxyPass = "http://127.0.0.1:${toString settings.server.port}";
+        };
+      };
+    };
+  };
+}
diff --git a/nixos/modules/services/x11/display-managers/lightdm.nix b/nixos/modules/services/x11/display-managers/lightdm.nix
index 1d557fb5f33d..b0508c3b4f79 100644
--- a/nixos/modules/services/x11/display-managers/lightdm.nix
+++ b/nixos/modules/services/x11/display-managers/lightdm.nix
@@ -311,7 +311,6 @@ in
       home = "/var/lib/lightdm";
       group = "lightdm";
       uid = config.ids.uids.lightdm;
-      shell = pkgs.bash;
     };
 
     systemd.tmpfiles.rules = [
diff --git a/nixos/modules/services/x11/window-managers/awesome.nix b/nixos/modules/services/x11/window-managers/awesome.nix
index 5bb74fd15910..c1231d3fbf38 100644
--- a/nixos/modules/services/x11/window-managers/awesome.nix
+++ b/nixos/modules/services/x11/window-managers/awesome.nix
@@ -6,7 +6,7 @@ let
 
   cfg = config.services.xserver.windowManager.awesome;
   awesome = cfg.package;
-  getLuaPath = lib : dir : "${lib}/${dir}/lua/${pkgs.luaPackages.lua.luaversion}";
+  getLuaPath = lib: dir: "${lib}/${dir}/lua/${awesome.lua.luaversion}";
   makeSearchPath = lib.concatMapStrings (path:
     " --search " + (getLuaPath path "share") +
     " --search " + (getLuaPath path "lib")
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
index 77280a9680e3..5398ef6b84eb 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py
@@ -240,11 +240,11 @@ def main() -> None:
         if "@graceful@" == "1":
             flags.append("--graceful")
 
-        subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@"] + flags + ["install"])
+        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + flags + ["install"])
     else:
         # Update bootloader to latest if needed
         available_out = subprocess.check_output(["@systemd@/bin/bootctl", "--version"], universal_newlines=True).split()[2]
-        installed_out = subprocess.check_output(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "status"], universal_newlines=True)
+        installed_out = subprocess.check_output(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@", "status"], universal_newlines=True)
 
         # See status_binaries() in systemd bootctl.c for code which generates this
         installed_match = re.search(r"^\W+File:.*/EFI/(?:BOOT|systemd)/.*\.efi \(systemd-boot ([\d.]+[^)]*)\)$",
@@ -263,7 +263,7 @@ def main() -> None:
 
         if installed_version < available_version:
             print("updating systemd-boot from %s to %s" % (installed_version, available_version))
-            subprocess.check_call(["@systemd@/bin/bootctl", "--path=@efiSysMountPoint@", "update"])
+            subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@", "update"])
 
     mkdir_p("@efiSysMountPoint@/efi/nixos")
     mkdir_p("@efiSysMountPoint@/loader/entries")
diff --git a/nixos/modules/system/boot/networkd.nix b/nixos/modules/system/boot/networkd.nix
index a0b433889d66..0b38a94c25fd 100644
--- a/nixos/modules/system/boot/networkd.nix
+++ b/nixos/modules/system/boot/networkd.nix
@@ -879,6 +879,15 @@ let
         (assertValueOneOf "OnLink" boolValues)
       ];
 
+      sectionIPv6RoutePrefix = checkUnitConfig "IPv6RoutePrefix" [
+        (assertOnlyFields [
+          "Route"
+          "LifetimeSec"
+        ])
+        (assertHasField "Route")
+        (assertInt "LifetimeSec")
+      ];
+
       sectionDHCPServerStaticLease = checkUnitConfig "DHCPServerStaticLease" [
         (assertOnlyFields [
           "MACAddress"
@@ -1242,6 +1251,22 @@ let
     };
   };
 
+  ipv6RoutePrefixOptions = {
+    options = {
+      ipv6RoutePrefixConfig = mkOption {
+        default = {};
+        example = { Route = "fd00::/64"; };
+        type = types.addCheck (types.attrsOf unitOption) check.network.sectionIPv6RoutePrefix;
+        description = ''
+          Each attribute in this set specifies an option in the
+          <literal>[IPv6RoutePrefix]</literal> section of the unit.  See
+          <citerefentry><refentrytitle>systemd.network</refentrytitle>
+          <manvolnum>5</manvolnum></citerefentry> for details.
+        '';
+      };
+    };
+  };
+
   dhcpServerStaticLeaseOptions = {
     options = {
       dhcpServerStaticLeaseConfig = mkOption {
@@ -1384,6 +1409,17 @@ let
       '';
     };
 
+    ipv6RoutePrefixes = mkOption {
+      default = [];
+      example = [ { Route = "fd00::/64"; LifetimeSec = 3600; } ];
+      type = with types; listOf (submodule ipv6RoutePrefixOptions);
+      description = ''
+        A list of ipv6RoutePrefix sections to be added to the unit.  See
+        <citerefentry><refentrytitle>systemd.network</refentrytitle>
+        <manvolnum>5</manvolnum></citerefentry> for details.
+      '';
+    };
+
     name = mkOption {
       type = types.nullOr types.str;
       default = null;
@@ -1775,6 +1811,10 @@ let
           [IPv6Prefix]
           ${attrsToSection x.ipv6PrefixConfig}
         '')
+        + flip concatMapStrings def.ipv6RoutePrefixes (x: ''
+          [IPv6RoutePrefix]
+          ${attrsToSection x.ipv6RoutePrefixConfig}
+        '')
         + flip concatMapStrings def.dhcpServerStaticLeases (x: ''
           [DHCPServerStaticLease]
           ${attrsToSection x.dhcpServerStaticLeaseConfig}
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index 956844352f9a..fb9c19d79c13 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -11,20 +11,6 @@ in
     maintainers = [ ] ++ lib.teams.podman.members;
   };
 
-
-  imports = [
-    (
-      lib.mkRemovedOptionModule
-        [ "virtualisation" "containers" "users" ]
-        "All users with `isNormalUser = true` set now get appropriate subuid/subgid mappings."
-    )
-    (
-      lib.mkRemovedOptionModule
-        [ "virtualisation" "containers" "containersConf" "extraConfig" ]
-        "Use virtualisation.containers.containersConf.settings instead."
-    )
-  ];
-
   options.virtualisation.containers = {
 
     enable =
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 89aa60fbd791..95ce1fea58bb 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -11,10 +11,6 @@ let
   cfgFile = format.generate "00-default.conf" cfg.settings;
 in
 {
-  imports = [
-    (mkRenamedOptionModule [ "virtualisation" "cri-o" "registries" ] [ "virtualisation" "containers" "registries" "search" ])
-  ];
-
   meta = {
     maintainers = teams.podman.members;
   };
diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix
index 376555ed2015..118bf82cdd66 100644
--- a/nixos/modules/virtualisation/podman/default.nix
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -46,7 +46,6 @@ in
   imports = [
     ./dnsname.nix
     ./network-socket.nix
-    (lib.mkRenamedOptionModule [ "virtualisation" "podman" "libpod" ] [ "virtualisation" "containers" "containersConf" ])
   ];
 
   meta = {
diff --git a/nixos/modules/virtualisation/qemu-vm.nix b/nixos/modules/virtualisation/qemu-vm.nix
index 7d9ec5875803..0207bfba82ad 100644
--- a/nixos/modules/virtualisation/qemu-vm.nix
+++ b/nixos/modules/virtualisation/qemu-vm.nix
@@ -102,7 +102,9 @@ let
   # Shell script to start the VM.
   startVM =
     ''
-      #! ${pkgs.runtimeShell}
+      #! ${cfg.host.pkgs.runtimeShell}
+
+      export PATH=${makeBinPath [ cfg.host.pkgs.coreutils ]}''${PATH:+:}$PATH
 
       set -e
 
@@ -575,11 +577,24 @@ in
         description = lib.mdDoc "Primary IP address used in /etc/hosts.";
       };
 
+    virtualisation.host.pkgs = mkOption {
+      type = options.nixpkgs.pkgs.type;
+      default = pkgs;
+      defaultText = "pkgs";
+      example = literalExpression ''
+        import pkgs.path { system = "x86_64-darwin"; }
+      '';
+      description = ''
+        pkgs set to use for the host-specific packages of the vm runner.
+        Changing this to e.g. a Darwin package set allows running NixOS VMs on Darwin.
+      '';
+    };
+
     virtualisation.qemu = {
       package =
         mkOption {
           type = types.package;
-          default = pkgs.qemu_kvm;
+          default = cfg.host.pkgs.qemu_kvm;
           example = "pkgs.qemu_test";
           description = lib.mdDoc "QEMU package to use.";
         };
@@ -1076,14 +1091,14 @@ in
 
     services.qemuGuest.enable = cfg.qemu.guestAgent.enable;
 
-    system.build.vm = pkgs.runCommand "nixos-vm" {
+    system.build.vm = cfg.host.pkgs.runCommand "nixos-vm" {
       preferLocalBuild = true;
       meta.mainProgram = "run-${config.system.name}-vm";
     }
       ''
         mkdir -p $out/bin
         ln -s ${config.system.build.toplevel} $out/system
-        ln -s ${pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
+        ln -s ${cfg.host.pkgs.writeScript "run-nixos-vm" startVM} $out/bin/run-${config.system.name}-vm
       '';
 
     # When building a regular system configuration, override whatever
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index 7e1ba8f5ed91..1cf310cb3321 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -621,6 +621,7 @@ in {
   wmderland = handleTest ./wmderland.nix {};
   wpa_supplicant = handleTest ./wpa_supplicant.nix {};
   wordpress = handleTest ./wordpress.nix {};
+  writefreely = handleTest ./web-apps/writefreely.nix {};
   xandikos = handleTest ./xandikos.nix {};
   xautolock = handleTest ./xautolock.nix {};
   xfce = handleTest ./xfce.nix {};
diff --git a/nixos/tests/k3s/multi-node.nix b/nixos/tests/k3s/multi-node.nix
index afb8c78f2339..ae9609fbccc9 100644
--- a/nixos/tests/k3s/multi-node.nix
+++ b/nixos/tests/k3s/multi-node.nix
@@ -53,9 +53,10 @@ import ../make-test-python.nix ({ pkgs, ... }:
           enable = true;
           role = "server";
           package = pkgs.k3s;
+          clusterInit = true;
           extraFlags = "--no-deploy coredns,servicelb,traefik,local-storage,metrics-server --pause-image test.local/pause:local --node-ip 192.168.1.1";
         };
-        networking.firewall.allowedTCPPorts = [ 6443 ];
+        networking.firewall.allowedTCPPorts = [ 2379 2380 6443 ];
         networking.firewall.allowedUDPPorts = [ 8472 ];
         networking.firewall.trustedInterfaces = [ "flannel.1" ];
         networking.useDHCP = false;
@@ -65,6 +66,28 @@ import ../make-test-python.nix ({ pkgs, ... }:
         ];
       };
 
+      server2 = { pkgs, ... }: {
+        environment.systemPackages = with pkgs; [ gzip jq ];
+        virtualisation.memorySize = 1536;
+        virtualisation.diskSize = 4096;
+
+        services.k3s = {
+          inherit tokenFile;
+          enable = true;
+          serverAddr = "https://192.168.1.1:6443";
+          clusterInit = false;
+          extraFlags = "--no-deploy coredns,servicelb,traefik,local-storage,metrics-server --pause-image test.local/pause:local --node-ip 192.168.1.3";
+        };
+        networking.firewall.allowedTCPPorts = [ 2379 2380 6443 ];
+        networking.firewall.allowedUDPPorts = [ 8472 ];
+        networking.firewall.trustedInterfaces = [ "flannel.1" ];
+        networking.useDHCP = false;
+        networking.defaultGateway = "192.168.1.3";
+        networking.interfaces.eth1.ipv4.addresses = pkgs.lib.mkForce [
+          { address = "192.168.1.3"; prefixLength = 24; }
+        ];
+      };
+
       agent = { pkgs, ... }: {
         virtualisation.memorySize = 1024;
         virtualisation.diskSize = 2048;
@@ -72,7 +95,7 @@ import ../make-test-python.nix ({ pkgs, ... }:
           inherit tokenFile;
           enable = true;
           role = "agent";
-          serverAddr = "https://192.168.1.1:6443";
+          serverAddr = "https://192.168.1.3:6443";
           extraFlags = "--pause-image test.local/pause:local --node-ip 192.168.1.2";
         };
         networking.firewall.allowedTCPPorts = [ 6443 ];
@@ -91,9 +114,9 @@ import ../make-test-python.nix ({ pkgs, ... }:
     };
 
     testScript = ''
-      start_all()
-      machines = [server, agent]
+      machines = [server, server2, agent]
       for m in machines:
+          m.start()
           m.wait_for_unit("k3s")
 
       # wait for the agent to show up
diff --git a/nixos/tests/web-apps/writefreely.nix b/nixos/tests/web-apps/writefreely.nix
new file mode 100644
index 000000000000..ce614909706b
--- /dev/null
+++ b/nixos/tests/web-apps/writefreely.nix
@@ -0,0 +1,44 @@
+{ system ? builtins.currentSystem, config ? { }
+, pkgs ? import ../../.. { inherit system config; } }:
+
+with import ../../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  writefreelyTest = { name, type }:
+    makeTest {
+      name = "writefreely-${name}";
+
+      nodes.machine = { config, pkgs, ... }: {
+        services.writefreely = {
+          enable = true;
+          host = "localhost:3000";
+          admin.name = "nixos";
+
+          database = {
+            inherit type;
+            createLocally = type == "mysql";
+            passwordFile = pkgs.writeText "db-pass" "pass";
+          };
+
+          settings.server.port = 3000;
+        };
+      };
+
+      testScript = ''
+        start_all()
+        machine.wait_for_unit("writefreely.service")
+        machine.wait_for_open_port(3000)
+        machine.succeed("curl --fail http://localhost:3000")
+      '';
+    };
+in {
+  sqlite = writefreelyTest {
+    name = "sqlite";
+    type = "sqlite3";
+  };
+  mysql = writefreelyTest {
+    name = "mysql";
+    type = "mysql";
+  };
+}