about summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/config/shells-environment.nix3
-rw-r--r--nixos/modules/image/repart-image.nix36
-rw-r--r--nixos/modules/image/repart.nix76
-rw-r--r--nixos/modules/misc/version.nix34
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/programs/ssh.nix1
-rw-r--r--nixos/modules/security/acme/default.nix4
-rw-r--r--nixos/modules/services/audio/gmediarender.nix1
-rw-r--r--nixos/modules/services/audio/jmusicbot.nix1
-rw-r--r--nixos/modules/services/audio/spotifyd.nix1
-rw-r--r--nixos/modules/services/audio/ympd.nix1
-rw-r--r--nixos/modules/services/continuous-integration/buildbot/master.nix1
-rw-r--r--nixos/modules/services/continuous-integration/gitea-actions-runner.nix1
-rw-r--r--nixos/modules/services/continuous-integration/github-runner/options.nix1
-rw-r--r--nixos/modules/services/continuous-integration/hydra/default.nix1
-rw-r--r--nixos/modules/services/databases/firebird.nix2
-rw-r--r--nixos/modules/services/databases/lldap.nix1
-rw-r--r--nixos/modules/services/databases/openldap.nix1
-rw-r--r--nixos/modules/services/desktops/geoclue2.nix2
-rw-r--r--nixos/modules/services/editors/emacs.nix21
-rw-r--r--nixos/modules/services/home-automation/evcc.nix1
-rw-r--r--nixos/modules/services/home-automation/home-assistant.nix1
-rw-r--r--nixos/modules/services/logging/journaldriver.nix1
-rw-r--r--nixos/modules/services/mail/dovecot.nix5
-rw-r--r--nixos/modules/services/mail/roundcube.nix1
-rw-r--r--nixos/modules/services/mail/sympa.nix2
-rw-r--r--nixos/modules/services/matrix/synapse.nix2
-rw-r--r--nixos/modules/services/misc/amazon-ssm-agent.nix1
-rw-r--r--nixos/modules/services/misc/bcg.nix2
-rw-r--r--nixos/modules/services/misc/domoticz.nix1
-rw-r--r--nixos/modules/services/misc/etesync-dav.nix1
-rw-r--r--nixos/modules/services/misc/mediatomb.nix1
-rw-r--r--nixos/modules/services/misc/metabase.nix1
-rw-r--r--nixos/modules/services/misc/moonraker.nix2
-rw-r--r--nixos/modules/services/misc/nix-ssh-serve.nix4
-rw-r--r--nixos/modules/services/misc/ollama.nix8
-rw-r--r--nixos/modules/services/misc/paperless.nix1
-rw-r--r--nixos/modules/services/misc/portunus.nix5
-rw-r--r--nixos/modules/services/misc/taskserver/helper-tool.py34
-rw-r--r--nixos/modules/services/monitoring/mackerel-agent.nix1
-rw-r--r--nixos/modules/services/monitoring/prometheus/alertmanager.nix1
-rw-r--r--nixos/modules/services/monitoring/teamviewer.nix1
-rw-r--r--nixos/modules/services/monitoring/telegraf.nix1
-rw-r--r--nixos/modules/services/monitoring/watchdogd.nix131
-rw-r--r--nixos/modules/services/network-filesystems/openafs/client.nix1
-rw-r--r--nixos/modules/services/network-filesystems/samba.nix2
-rw-r--r--nixos/modules/services/networking/bird.nix9
-rw-r--r--nixos/modules/services/networking/bitcoind.nix1
-rw-r--r--nixos/modules/services/networking/dante.nix1
-rw-r--r--nixos/modules/services/networking/ergo.nix1
-rw-r--r--nixos/modules/services/networking/expressvpn.nix1
-rw-r--r--nixos/modules/services/networking/headscale.nix1
-rw-r--r--nixos/modules/services/networking/ircd-hybrid/default.nix3
-rw-r--r--nixos/modules/services/networking/ivpn.nix2
-rw-r--r--nixos/modules/services/networking/kea.nix7
-rw-r--r--nixos/modules/services/networking/mosquitto.nix1
-rw-r--r--nixos/modules/services/networking/mullvad-vpn.nix2
-rw-r--r--nixos/modules/services/networking/nbd.nix1
-rw-r--r--nixos/modules/services/networking/ocserv.nix1
-rw-r--r--nixos/modules/services/networking/pleroma.nix1
-rw-r--r--nixos/modules/services/networking/rosenpass.nix1
-rw-r--r--nixos/modules/services/networking/rxe.nix2
-rw-r--r--nixos/modules/services/networking/soju.nix1
-rw-r--r--nixos/modules/services/networking/strongswan-swanctl/module.nix1
-rw-r--r--nixos/modules/services/networking/strongswan.nix1
-rw-r--r--nixos/modules/services/networking/syncplay.nix1
-rw-r--r--nixos/modules/services/networking/wasabibackend.nix1
-rw-r--r--nixos/modules/services/networking/znc/default.nix1
-rw-r--r--nixos/modules/services/security/certmgr.nix1
-rw-r--r--nixos/modules/services/security/clamav.nix2
-rw-r--r--nixos/modules/services/security/oauth2_proxy.nix1
-rw-r--r--nixos/modules/services/system/cachix-agent/default.nix1
-rw-r--r--nixos/modules/services/system/cachix-watch-store.nix1
-rw-r--r--nixos/modules/services/system/cloud-init.nix5
-rw-r--r--nixos/modules/services/video/go2rtc/default.nix1
-rw-r--r--nixos/modules/services/web-apps/akkoma.nix2
-rw-r--r--nixos/modules/services/web-apps/alps.nix1
-rw-r--r--nixos/modules/services/web-apps/c2fmzq-server.nix1
-rw-r--r--nixos/modules/services/web-apps/code-server.nix1
-rw-r--r--nixos/modules/services/web-apps/healthchecks.nix1
-rw-r--r--nixos/modules/services/web-apps/netbox.nix1
-rw-r--r--nixos/modules/services/web-apps/nextcloud.nix211
-rw-r--r--nixos/modules/services/web-apps/openvscode-server.nix1
-rw-r--r--nixos/modules/services/web-apps/peering-manager.nix1
-rw-r--r--nixos/modules/services/web-apps/suwayomi-server.md108
-rw-r--r--nixos/modules/services/web-apps/suwayomi-server.nix260
-rw-r--r--nixos/modules/services/web-apps/wordpress.nix10
-rw-r--r--nixos/modules/services/web-servers/agate.nix1
-rw-r--r--nixos/modules/services/web-servers/mighttpd2.nix1
-rw-r--r--nixos/modules/services/web-servers/minio.nix1
-rw-r--r--nixos/modules/services/web-servers/traefik.nix1
-rw-r--r--nixos/modules/services/web-servers/ttyd.nix3
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot-builder.py89
-rw-r--r--nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix6
-rw-r--r--nixos/modules/system/boot/resolved.nix24
-rw-r--r--nixos/modules/system/boot/systemd.nix46
-rw-r--r--nixos/modules/virtualisation/amazon-image.nix1
-rw-r--r--nixos/modules/virtualisation/oci-containers.nix1
-rw-r--r--nixos/modules/virtualisation/podman/default.nix33
99 files changed, 1028 insertions, 233 deletions
diff --git a/nixos/modules/config/shells-environment.nix b/nixos/modules/config/shells-environment.nix
index bc6583442edf..a8476bd2aaed 100644
--- a/nixos/modules/config/shells-environment.nix
+++ b/nixos/modules/config/shells-environment.nix
@@ -214,7 +214,8 @@ in
       ''
         # Create the required /bin/sh symlink; otherwise lots of things
         # (notably the system() function) won't work.
-        mkdir -m 0755 -p /bin
+        mkdir -p /bin
+        chmod 0755 /bin
         ln -sfn "${cfg.binsh}" /bin/.sh.tmp
         mv /bin/.sh.tmp /bin/sh # atomically replace /bin/sh
       '';
diff --git a/nixos/modules/image/repart-image.nix b/nixos/modules/image/repart-image.nix
index b4a1dfe51ff3..a12b4fb14fb1 100644
--- a/nixos/modules/image/repart-image.nix
+++ b/nixos/modules/image/repart-image.nix
@@ -10,6 +10,8 @@
 , systemd
 , fakeroot
 , util-linux
+
+  # filesystem tools
 , dosfstools
 , mtools
 , e2fsprogs
@@ -18,8 +20,13 @@
 , btrfs-progs
 , xfsprogs
 
+  # compression tools
+, zstd
+, xz
+
   # arguments
-, name
+, imageFileBasename
+, compression
 , fileSystems
 , partitions
 , split
@@ -52,14 +59,25 @@ let
   };
 
   fileSystemTools = builtins.concatMap (f: fileSystemToolMapping."${f}") fileSystems;
+
+  compressionPkg = {
+    "zstd" = zstd;
+    "xz" = xz;
+  }."${compression.algorithm}";
+
+  compressionCommand = {
+    "zstd" = "zstd --no-progress --threads=0 -${toString compression.level}";
+    "xz" = "xz --keep --verbose --threads=0 -${toString compression.level}";
+  }."${compression.algorithm}";
 in
 
-runCommand name
+runCommand imageFileBasename
 {
   nativeBuildInputs = [
     systemd
     fakeroot
     util-linux
+    compressionPkg
   ] ++ fileSystemTools;
 } ''
   amendedRepartDefinitions=$(${amendRepartDefinitions} ${partitions} ${definitionsDirectory})
@@ -67,6 +85,7 @@ runCommand name
   mkdir -p $out
   cd $out
 
+  echo "Building image with systemd-repart..."
   unshare --map-root-user fakeroot systemd-repart \
     --dry-run=no \
     --empty=create \
@@ -75,6 +94,17 @@ runCommand name
     --definitions="$amendedRepartDefinitions" \
     --split="${lib.boolToString split}" \
     --json=pretty \
-    image.raw \
+    ${imageFileBasename}.raw \
     | tee repart-output.json
+
+  # Compression is implemented in the same derivation as opposed to in a
+  # separate derivation to allow users to save disk space. Disk images are
+  # already very space intensive so we want to allow users to mitigate this.
+  if ${lib.boolToString compression.enable}; then
+    for f in ${imageFileBasename}*; do
+      echo "Compressing $f with ${compression.algorithm}..."
+      # Keep the original file when compressing and only delete it afterwards
+      ${compressionCommand} $f && rm $f
+    done
+  fi
 ''
diff --git a/nixos/modules/image/repart.nix b/nixos/modules/image/repart.nix
index da4f45d9a639..ed584d9bf997 100644
--- a/nixos/modules/image/repart.nix
+++ b/nixos/modules/image/repart.nix
@@ -66,7 +66,53 @@ in
 
     name = lib.mkOption {
       type = lib.types.str;
-      description = lib.mdDoc "The name of the image.";
+      description = lib.mdDoc ''
+        Name of the image.
+
+        If this option is unset but config.system.image.id is set,
+        config.system.image.id is used as the default value.
+      '';
+    };
+
+    version = lib.mkOption {
+      type = lib.types.nullOr lib.types.str;
+      default = config.system.image.version;
+      defaultText = lib.literalExpression "config.system.image.version";
+      description = lib.mdDoc "Version of the image";
+    };
+
+    imageFileBasename = lib.mkOption {
+      type = lib.types.str;
+      readOnly = true;
+      description = lib.mdDoc ''
+        Basename of the image filename without any extension (e.g. `image_1`).
+      '';
+    };
+
+    imageFile = lib.mkOption {
+      type = lib.types.str;
+      readOnly = true;
+      description = lib.mdDoc ''
+        Filename of the image including all extensions (e.g `image_1.raw` or
+        `image_1.raw.zst`).
+      '';
+    };
+
+    compression = {
+      enable = lib.mkEnableOption (lib.mdDoc "Image compression");
+
+      algorithm = lib.mkOption {
+        type = lib.types.enum [ "zstd" "xz" ];
+        default = "zstd";
+        description = lib.mdDoc "Compression algorithm";
+      };
+
+      level = lib.mkOption {
+        type = lib.types.int;
+        description = lib.mdDoc ''
+          Compression level. The available range depends on the used algorithm.
+        '';
+      };
     };
 
     seed = lib.mkOption {
@@ -131,6 +177,32 @@ in
 
   config = {
 
+    image.repart =
+      let
+        version = config.image.repart.version;
+        versionInfix = if version != null then "_${version}" else "";
+        compressionSuffix = lib.optionalString cfg.compression.enable
+          {
+            "zstd" = ".zst";
+            "xz" = ".xz";
+          }."${cfg.compression.algorithm}";
+      in
+      {
+        name = lib.mkIf (config.system.image.id != null) (lib.mkOptionDefault config.system.image.id);
+        imageFileBasename = cfg.name + versionInfix;
+        imageFile = cfg.imageFileBasename + ".raw" + compressionSuffix;
+
+        compression = {
+          # Generally default to slightly faster than default compression
+          # levels under the assumption that most of the building will be done
+          # for development and release builds will be customized.
+          level = lib.mkOptionDefault {
+            "zstd" = 3;
+            "xz" = 3;
+          }."${cfg.compression.algorithm}";
+        };
+      };
+
     system.build.image =
       let
         fileSystems = lib.filter
@@ -160,7 +232,7 @@ in
       in
       pkgs.callPackage ./repart-image.nix {
         systemd = cfg.package;
-        inherit (cfg) name split seed;
+        inherit (cfg) imageFileBasename compression split seed;
         inherit fileSystems definitionsDirectory partitions;
       };
 
diff --git a/nixos/modules/misc/version.nix b/nixos/modules/misc/version.nix
index 45dbf45b3ae7..c929c3b37285 100644
--- a/nixos/modules/misc/version.nix
+++ b/nixos/modules/misc/version.nix
@@ -28,6 +28,8 @@ let
     DOCUMENTATION_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/learn.html";
     SUPPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://nixos.org/community.html";
     BUG_REPORT_URL = lib.optionalString (cfg.distroId == "nixos") "https://github.com/NixOS/nixpkgs/issues";
+    IMAGE_ID = lib.optionalString (config.system.image.id != null) config.system.image.id;
+    IMAGE_VERSION = lib.optionalString (config.system.image.version != null) config.system.image.version;
   } // lib.optionalAttrs (cfg.variant_id != null) {
     VARIANT_ID = cfg.variant_id;
   };
@@ -110,6 +112,38 @@ in
       example = "installer";
     };
 
+    image = {
+
+      id = lib.mkOption {
+        type = types.nullOr (types.strMatching "^[a-z0-9._-]+$");
+        default = null;
+        description = lib.mdDoc ''
+          Image identifier.
+
+          This corresponds to the IMAGE_ID field in os-release. See the
+          upstream docs for more details on valid characters for this field:
+          https://www.freedesktop.org/software/systemd/man/latest/os-release.html#IMAGE_ID=
+
+          You would only want to set this option if you're build NixOS appliance images.
+        '';
+      };
+
+      version = lib.mkOption {
+        type = types.nullOr (types.strMatching "^[a-z0-9._-]+$");
+        default = null;
+        description = lib.mdDoc ''
+          Image version.
+
+          This corresponds to the IMAGE_VERSION field in os-release. See the
+          upstream docs for more details on valid characters for this field:
+          https://www.freedesktop.org/software/systemd/man/latest/os-release.html#IMAGE_VERSION=
+
+          You would only want to set this option if you're build NixOS appliance images.
+        '';
+      };
+
+    };
+
     stateVersion = mkOption {
       type = types.str;
       # TODO Remove this and drop the default of the option so people are forced to set it.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index 698514410560..b31154b9624e 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -849,6 +849,7 @@
   ./services/monitoring/vmagent.nix
   ./services/monitoring/vmalert.nix
   ./services/monitoring/vnstat.nix
+  ./services/monitoring/watchdogd.nix
   ./services/monitoring/zabbix-agent.nix
   ./services/monitoring/zabbix-proxy.nix
   ./services/monitoring/zabbix-server.nix
@@ -1339,6 +1340,7 @@
   ./services/web-apps/restya-board.nix
   ./services/web-apps/rimgo.nix
   ./services/web-apps/sftpgo.nix
+  ./services/web-apps/suwayomi-server.nix
   ./services/web-apps/rss-bridge.nix
   ./services/web-apps/selfoss.nix
   ./services/web-apps/shiori.nix
diff --git a/nixos/modules/programs/ssh.nix b/nixos/modules/programs/ssh.nix
index c39a3c8d509b..0c1461709c22 100644
--- a/nixos/modules/programs/ssh.nix
+++ b/nixos/modules/programs/ssh.nix
@@ -12,6 +12,7 @@ let
     ''
       #! ${pkgs.runtimeShell} -e
       export DISPLAY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^DISPLAY=\(.*\)/\1/; t; d')"
+      export XAUTHORITY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^XAUTHORITY=\(.*\)/\1/; t; d')"
       export WAYLAND_DISPLAY="$(systemctl --user show-environment | ${pkgs.gnused}/bin/sed 's/^WAYLAND_DISPLAY=\(.*\)/\1/; t; d')"
       exec ${cfg.askPassword} "$@"
     '';
diff --git a/nixos/modules/security/acme/default.nix b/nixos/modules/security/acme/default.nix
index 7cc302969fb6..40d9c487996b 100644
--- a/nixos/modules/security/acme/default.nix
+++ b/nixos/modules/security/acme/default.nix
@@ -897,10 +897,10 @@ in {
         certs = attrValues cfg.certs;
       in [
         {
-          assertion = cfg.email != null || all (certOpts: certOpts.email != null) certs;
+          assertion = cfg.defaults.email != null || all (certOpts: certOpts.email != null) certs;
           message = ''
             You must define `security.acme.certs.<name>.email` or
-            `security.acme.email` to register with the CA. Note that using
+            `security.acme.defaults.email` to register with the CA. Note that using
             many different addresses for certs may trigger account rate limits.
           '';
         }
diff --git a/nixos/modules/services/audio/gmediarender.nix b/nixos/modules/services/audio/gmediarender.nix
index 545f2b1a2b60..a4cb89098db7 100644
--- a/nixos/modules/services/audio/gmediarender.nix
+++ b/nixos/modules/services/audio/gmediarender.nix
@@ -64,6 +64,7 @@ in
   config = mkIf cfg.enable {
     systemd = {
       services.gmediarender = {
+        wants = [ "network-online.target" ];
         after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         description = "gmediarender server daemon";
diff --git a/nixos/modules/services/audio/jmusicbot.nix b/nixos/modules/services/audio/jmusicbot.nix
index fd1d4da19284..e7803677d0fd 100644
--- a/nixos/modules/services/audio/jmusicbot.nix
+++ b/nixos/modules/services/audio/jmusicbot.nix
@@ -26,6 +26,7 @@ in
   config = mkIf cfg.enable {
     systemd.services.jmusicbot = {
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       description = "Discord music bot that's easy to set up and run yourself!";
       serviceConfig = mkMerge [{
diff --git a/nixos/modules/services/audio/spotifyd.nix b/nixos/modules/services/audio/spotifyd.nix
index 975be5a87cba..1194b6f200d7 100644
--- a/nixos/modules/services/audio/spotifyd.nix
+++ b/nixos/modules/services/audio/spotifyd.nix
@@ -50,6 +50,7 @@ in
 
     systemd.services.spotifyd = {
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" "sound.target" ];
       description = "spotifyd, a Spotify playing daemon";
       environment.SHELL = "/bin/sh";
diff --git a/nixos/modules/services/audio/ympd.nix b/nixos/modules/services/audio/ympd.nix
index b74cc3f9c0b4..6e8d22dab3c8 100644
--- a/nixos/modules/services/audio/ympd.nix
+++ b/nixos/modules/services/audio/ympd.nix
@@ -50,6 +50,7 @@ in {
       description = "Standalone MPD Web GUI written in C";
 
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/continuous-integration/buildbot/master.nix b/nixos/modules/services/continuous-integration/buildbot/master.nix
index 446d19b8fd5a..c86cb81e5df4 100644
--- a/nixos/modules/services/continuous-integration/buildbot/master.nix
+++ b/nixos/modules/services/continuous-integration/buildbot/master.nix
@@ -268,6 +268,7 @@ in {
     systemd.services.buildbot-master = {
       description = "Buildbot Continuous Integration Server.";
       after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       path = cfg.packages ++ cfg.pythonPackages python.pkgs;
       environment.PYTHONPATH = "${python.withPackages (self: cfg.pythonPackages self ++ [ package ])}/${python.sitePackages}";
diff --git a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
index 3f2be9464849..06f0da3451a6 100644
--- a/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
+++ b/nixos/modules/services/continuous-integration/gitea-actions-runner.nix
@@ -188,6 +188,7 @@ in
         nameValuePair "gitea-runner-${escapeSystemdPath name}" {
           inherit (instance) enable;
           description = "Gitea Actions Runner";
+          wants = [ "network-online.target" ];
           after = [
             "network-online.target"
           ] ++ optionals (wantsDocker) [
diff --git a/nixos/modules/services/continuous-integration/github-runner/options.nix b/nixos/modules/services/continuous-integration/github-runner/options.nix
index 2335826e8b66..b9b1ea05e967 100644
--- a/nixos/modules/services/continuous-integration/github-runner/options.nix
+++ b/nixos/modules/services/continuous-integration/github-runner/options.nix
@@ -153,6 +153,7 @@ with lib;
     type = types.attrs;
     description = lib.mdDoc ''
       Modify the systemd service. Can be used to, e.g., adjust the sandboxing options.
+      See {manpage}`systemd.exec(5)` for more options.
     '';
     example = {
       ProtectHome = false;
diff --git a/nixos/modules/services/continuous-integration/hydra/default.nix b/nixos/modules/services/continuous-integration/hydra/default.nix
index 46b03bba37be..54bbe69703f9 100644
--- a/nixos/modules/services/continuous-integration/hydra/default.nix
+++ b/nixos/modules/services/continuous-integration/hydra/default.nix
@@ -393,6 +393,7 @@ in
     systemd.services.hydra-evaluator =
       { wantedBy = [ "multi-user.target" ];
         requires = [ "hydra-init.service" ];
+        wants = [ "network-online.target" ];
         after = [ "hydra-init.service" "network.target" "network-online.target" ];
         path = with pkgs; [ hydra-package nettools jq ];
         restartTriggers = [ hydraConf ];
diff --git a/nixos/modules/services/databases/firebird.nix b/nixos/modules/services/databases/firebird.nix
index 36c12eaaf5f1..431233ce5ed4 100644
--- a/nixos/modules/services/databases/firebird.nix
+++ b/nixos/modules/services/databases/firebird.nix
@@ -143,7 +143,7 @@ in
       # ConnectionTimeout = 180
 
       #RemoteServiceName = gds_db
-      RemoteServicePort = ${cfg.port}
+      RemoteServicePort = ${toString cfg.port}
 
       # randomly choose port for server Event Notification
       #RemoteAuxPort = 0
diff --git a/nixos/modules/services/databases/lldap.nix b/nixos/modules/services/databases/lldap.nix
index d1574c98fe67..e821da8e58aa 100644
--- a/nixos/modules/services/databases/lldap.nix
+++ b/nixos/modules/services/databases/lldap.nix
@@ -104,6 +104,7 @@ in
   config = lib.mkIf cfg.enable {
     systemd.services.lldap = {
       description = "Lightweight LDAP server (lldap)";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
diff --git a/nixos/modules/services/databases/openldap.nix b/nixos/modules/services/databases/openldap.nix
index a7a0909f55e1..df36e37976a4 100644
--- a/nixos/modules/services/databases/openldap.nix
+++ b/nixos/modules/services/databases/openldap.nix
@@ -294,6 +294,7 @@ in {
         "man:slapd-mdb"
       ];
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
         User = cfg.user;
diff --git a/nixos/modules/services/desktops/geoclue2.nix b/nixos/modules/services/desktops/geoclue2.nix
index b04f46c26a56..2a68bb0b55f3 100644
--- a/nixos/modules/services/desktops/geoclue2.nix
+++ b/nixos/modules/services/desktops/geoclue2.nix
@@ -200,6 +200,7 @@ in
     };
 
     systemd.services.geoclue = {
+      wants = lib.optionals cfg.enableWifi [ "network-online.target" ];
       after = lib.optionals cfg.enableWifi [ "network-online.target" ];
       # restart geoclue service when the configuration changes
       restartTriggers = [
@@ -217,6 +218,7 @@ in
         # we can't be part of a system service, and the agent should
         # be okay with the main service coming and going
         wantedBy = [ "default.target" ];
+        wants = lib.optionals cfg.enableWifi [ "network-online.target" ];
         after = lib.optionals cfg.enableWifi [ "network-online.target" ];
         unitConfig.ConditionUser = "!@system";
         serviceConfig = {
diff --git a/nixos/modules/services/editors/emacs.nix b/nixos/modules/services/editors/emacs.nix
index 6f45be6640bc..ff6fd85d8a9b 100644
--- a/nixos/modules/services/editors/emacs.nix
+++ b/nixos/modules/services/editors/emacs.nix
@@ -15,25 +15,6 @@ let
     fi
   '';
 
-  desktopApplicationFile = pkgs.writeTextFile {
-    name = "emacsclient.desktop";
-    destination = "/share/applications/emacsclient.desktop";
-    text = ''
-      [Desktop Entry]
-      Name=Emacsclient
-      GenericName=Text Editor
-      Comment=Edit text
-      MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;
-      Exec=emacseditor %F
-      Icon=emacs
-      Type=Application
-      Terminal=false
-      Categories=Development;TextEditor;
-      StartupWMClass=Emacs
-      Keywords=Text;Editor;
-    '';
-  };
-
 in
 {
 
@@ -102,7 +83,7 @@ in
       wantedBy = if cfg.startWithGraphical then [ "graphical-session.target" ] else [ "default.target" ];
     };
 
-    environment.systemPackages = [ cfg.package editorScript desktopApplicationFile ];
+    environment.systemPackages = [ cfg.package editorScript ];
 
     environment.variables.EDITOR = mkIf cfg.defaultEditor (mkOverride 900 "emacseditor");
   };
diff --git a/nixos/modules/services/home-automation/evcc.nix b/nixos/modules/services/home-automation/evcc.nix
index d0ce3fb4a1ce..f360f525b04b 100644
--- a/nixos/modules/services/home-automation/evcc.nix
+++ b/nixos/modules/services/home-automation/evcc.nix
@@ -41,6 +41,7 @@ in
 
   config = mkIf cfg.enable {
     systemd.services.evcc = {
+      wants = [ "network-online.target" ];
       after = [
         "network-online.target"
         "mosquitto.target"
diff --git a/nixos/modules/services/home-automation/home-assistant.nix b/nixos/modules/services/home-automation/home-assistant.nix
index bc470576b759..a01628968966 100644
--- a/nixos/modules/services/home-automation/home-assistant.nix
+++ b/nixos/modules/services/home-automation/home-assistant.nix
@@ -435,6 +435,7 @@ in {
 
     systemd.services.home-assistant = {
       description = "Home Assistant";
+      wants = [ "network-online.target" ];
       after = [
         "network-online.target"
 
diff --git a/nixos/modules/services/logging/journaldriver.nix b/nixos/modules/services/logging/journaldriver.nix
index 59eedff90d60..4d21464018aa 100644
--- a/nixos/modules/services/logging/journaldriver.nix
+++ b/nixos/modules/services/logging/journaldriver.nix
@@ -84,6 +84,7 @@ in {
     systemd.services.journaldriver = {
       description = "Stackdriver Logging journal forwarder";
       script      = "${pkgs.journaldriver}/bin/journaldriver";
+      wants       = [ "network-online.target" ];
       after       = [ "network-online.target" ];
       wantedBy    = [ "multi-user.target" ];
 
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index 25c7017a1d25..79c8fec75252 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -119,9 +119,10 @@ let
     ''
       plugin {
         sieve_plugins = ${concatStringsSep " " cfg.sieve.plugins}
-        sieve_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions)}
-        sieve_global_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions)}
     ''
+    (optionalString (cfg.sieve.extensions != []) ''sieve_extensions = ${concatMapStringsSep " " (el: "+${el}") cfg.sieve.extensions}'')
+    (optionalString (cfg.sieve.globalExtensions != []) ''sieve_global_extensions = ${concatMapStringsSep " " (el: "+${el}") cfg.sieve.globalExtensions}'')
+
     (optionalString (cfg.imapsieve.mailbox != []) ''
       ${
         concatStringsSep "\n" (flatten (imap1 (
diff --git a/nixos/modules/services/mail/roundcube.nix b/nixos/modules/services/mail/roundcube.nix
index c883c143e523..3f1a695ab91a 100644
--- a/nixos/modules/services/mail/roundcube.nix
+++ b/nixos/modules/services/mail/roundcube.nix
@@ -250,6 +250,7 @@ in
         path = [ config.services.postgresql.package ];
       })
       {
+        wants = [ "network-online.target" ];
         after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         script = let
diff --git a/nixos/modules/services/mail/sympa.nix b/nixos/modules/services/mail/sympa.nix
index 04ae46f66eea..13fc8656a2b5 100644
--- a/nixos/modules/services/mail/sympa.nix
+++ b/nixos/modules/services/mail/sympa.nix
@@ -435,7 +435,7 @@ in
 
       wantedBy = [ "multi-user.target" ];
       after = [ "network-online.target" ];
-      wants = sympaSubServices;
+      wants = sympaSubServices ++ [ "network-online.target" ];
       before = sympaSubServices;
       serviceConfig = sympaServiceConfig "sympa_msg";
 
diff --git a/nixos/modules/services/matrix/synapse.nix b/nixos/modules/services/matrix/synapse.nix
index 50019d2a25cb..4c1c396eac05 100644
--- a/nixos/modules/services/matrix/synapse.nix
+++ b/nixos/modules/services/matrix/synapse.nix
@@ -1056,6 +1056,7 @@ in {
 
     systemd.targets.matrix-synapse = lib.mkIf hasWorkers {
       description = "Synapse Matrix parent target";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
       wantedBy = [ "multi-user.target" ];
     };
@@ -1071,6 +1072,7 @@ in {
             requires = optional hasLocalPostgresDB "postgresql.service";
           }
           else {
+            wants = [ "network-online.target" ];
             after = [ "network-online.target" ] ++ optional hasLocalPostgresDB "postgresql.service";
             requires = optional hasLocalPostgresDB "postgresql.service";
             wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/misc/amazon-ssm-agent.nix b/nixos/modules/services/misc/amazon-ssm-agent.nix
index 20b836abe164..89a1c0766510 100644
--- a/nixos/modules/services/misc/amazon-ssm-agent.nix
+++ b/nixos/modules/services/misc/amazon-ssm-agent.nix
@@ -41,6 +41,7 @@ in {
     # See https://github.com/aws/amazon-ssm-agent/blob/mainline/packaging/linux/amazon-ssm-agent.service
     systemd.services.amazon-ssm-agent = {
       inherit (cfg.package.meta) description;
+      wants    = [ "network-online.target" ];
       after    = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
 
diff --git a/nixos/modules/services/misc/bcg.nix b/nixos/modules/services/misc/bcg.nix
index 9da4a879cdd0..ad0b9c871342 100644
--- a/nixos/modules/services/misc/bcg.nix
+++ b/nixos/modules/services/misc/bcg.nix
@@ -154,7 +154,7 @@ in
     in {
       description = "BigClown Gateway";
       wantedBy = [ "multi-user.target" ];
-      wants = mkIf config.services.mosquitto.enable [ "mosquitto.service" ];
+      wants = [ "network-online.target" ] ++ lib.optional config.services.mosquitto.enable "mosquitto.service";
       after = [ "network-online.target" ];
       preStart = ''
         umask 077
diff --git a/nixos/modules/services/misc/domoticz.nix b/nixos/modules/services/misc/domoticz.nix
index fd9fcf0b78eb..315092f93351 100644
--- a/nixos/modules/services/misc/domoticz.nix
+++ b/nixos/modules/services/misc/domoticz.nix
@@ -35,6 +35,7 @@ in {
     systemd.services."domoticz" = {
       description = pkgDesc;
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
         DynamicUser = true;
diff --git a/nixos/modules/services/misc/etesync-dav.nix b/nixos/modules/services/misc/etesync-dav.nix
index 9d99d548d95b..ae2b5ad04343 100644
--- a/nixos/modules/services/misc/etesync-dav.nix
+++ b/nixos/modules/services/misc/etesync-dav.nix
@@ -59,6 +59,7 @@ in
 
       systemd.services.etesync-dav = {
         description = "etesync-dav - A CalDAV and CardDAV adapter for EteSync";
+        wants = [ "network-online.target" ];
         after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         path = [ pkgs.etesync-dav ];
diff --git a/nixos/modules/services/misc/mediatomb.nix b/nixos/modules/services/misc/mediatomb.nix
index d421d74c53ad..03235e9a1265 100644
--- a/nixos/modules/services/misc/mediatomb.nix
+++ b/nixos/modules/services/misc/mediatomb.nix
@@ -357,6 +357,7 @@ in {
       description = "${cfg.serverName} media Server";
       # Gerbera might fail if the network interface is not available on startup
       # https://github.com/gerbera/gerbera/issues/1324
+      wants = [ "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig.ExecStart = "${binaryCommand} --port ${toString cfg.port} ${interfaceFlag} ${configFlag} --home ${cfg.dataDir}";
diff --git a/nixos/modules/services/misc/metabase.nix b/nixos/modules/services/misc/metabase.nix
index 883fa0b95911..5fc18e27eaae 100644
--- a/nixos/modules/services/misc/metabase.nix
+++ b/nixos/modules/services/misc/metabase.nix
@@ -77,6 +77,7 @@ in {
     systemd.services.metabase = {
       description = "Metabase server";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       environment = {
         MB_PLUGINS_DIR = "${dataDir}/plugins";
diff --git a/nixos/modules/services/misc/moonraker.nix b/nixos/modules/services/misc/moonraker.nix
index 750dca9d0373..4e419aafa990 100644
--- a/nixos/modules/services/misc/moonraker.nix
+++ b/nixos/modules/services/misc/moonraker.nix
@@ -103,7 +103,7 @@ in {
 
   config = mkIf cfg.enable {
     warnings = []
-      ++ optional (cfg.settings ? update_manager)
+      ++ optional (cfg.settings.update_manager.enable_system_updates or false)
         ''Enabling update_manager is not supported on NixOS and will lead to non-removable warnings in some clients.''
       ++ optional (cfg.configDir != null)
         ''
diff --git a/nixos/modules/services/misc/nix-ssh-serve.nix b/nixos/modules/services/misc/nix-ssh-serve.nix
index b656692ca01c..cf9d6339c69b 100644
--- a/nixos/modules/services/misc/nix-ssh-serve.nix
+++ b/nixos/modules/services/misc/nix-ssh-serve.nix
@@ -1,4 +1,4 @@
-{ config, lib, ... }:
+{ config, lib, pkgs, ... }:
 
 with lib;
 let cfg = config.nix.sshServe;
@@ -46,7 +46,7 @@ in {
       description = "Nix SSH store user";
       isSystemUser = true;
       group = "nix-ssh";
-      useDefaultShell = true;
+      shell = pkgs.bashInteractive;
     };
     users.groups.nix-ssh = {};
 
diff --git a/nixos/modules/services/misc/ollama.nix b/nixos/modules/services/misc/ollama.nix
index 9794bbbec464..d9359d2b5cd4 100644
--- a/nixos/modules/services/misc/ollama.nix
+++ b/nixos/modules/services/misc/ollama.nix
@@ -9,6 +9,13 @@ in {
       enable = lib.mkEnableOption (
         lib.mdDoc "Server for local large language models"
       );
+      listenAddress = lib.mkOption {
+        type = lib.types.str;
+        default = "127.0.0.1:11434";
+        description = lib.mdDoc ''
+          Specifies the bind address on which the ollama server HTTP interface listens.
+        '';
+      };
       package = lib.mkPackageOption pkgs "ollama" { };
     };
   };
@@ -23,6 +30,7 @@ in {
         environment = {
           HOME = "%S/ollama";
           OLLAMA_MODELS = "%S/ollama/models";
+          OLLAMA_HOST = cfg.listenAddress;
         };
         serviceConfig = {
           ExecStart = "${lib.getExe cfg.package} serve";
diff --git a/nixos/modules/services/misc/paperless.nix b/nixos/modules/services/misc/paperless.nix
index 3c6832958f59..ca34a327dbdf 100644
--- a/nixos/modules/services/misc/paperless.nix
+++ b/nixos/modules/services/misc/paperless.nix
@@ -297,6 +297,7 @@ in
       wantedBy = [ "paperless-scheduler.service" ];
       before = [ "paperless-scheduler.service" ];
       after = [ "network-online.target" ];
+      wants = [ "network-online.target" ];
       serviceConfig = defaultServiceConfig // {
         User = cfg.user;
         Type = "oneshot";
diff --git a/nixos/modules/services/misc/portunus.nix b/nixos/modules/services/misc/portunus.nix
index 7036a372d1ea..47af24f024cd 100644
--- a/nixos/modules/services/misc/portunus.nix
+++ b/nixos/modules/services/misc/portunus.nix
@@ -230,7 +230,10 @@ in
         description = "Self-contained authentication service";
         wantedBy = [ "multi-user.target" ];
         after = [ "network.target" ];
-        serviceConfig.ExecStart = "${cfg.package.out}/bin/portunus-orchestrator";
+        serviceConfig = {
+          ExecStart = "${cfg.package}/bin/portunus-orchestrator";
+          Restart = "on-failure";
+        };
         environment = {
           PORTUNUS_LDAP_SUFFIX = cfg.ldap.suffix;
           PORTUNUS_SERVER_BINARY = "${cfg.package}/bin/portunus-server";
diff --git a/nixos/modules/services/misc/taskserver/helper-tool.py b/nixos/modules/services/misc/taskserver/helper-tool.py
index fec05728b2b6..b1eebb07686b 100644
--- a/nixos/modules/services/misc/taskserver/helper-tool.py
+++ b/nixos/modules/services/misc/taskserver/helper-tool.py
@@ -61,6 +61,10 @@ def run_as_taskd_user():
     os.setuid(uid)
 
 
+def run_as_taskd_group():
+    gid = grp.getgrnam(TASKD_GROUP).gr_gid
+    os.setgid(gid)
+
 def taskd_cmd(cmd, *args, **kwargs):
     """
     Invoke taskd with the specified command with the privileges of the 'taskd'
@@ -90,7 +94,7 @@ def certtool_cmd(*args, **kwargs):
     """
     return subprocess.check_output(
         [CERTTOOL_COMMAND] + list(args),
-        preexec_fn=lambda: os.umask(0o077),
+        preexec_fn=run_as_taskd_group,
         stderr=subprocess.STDOUT,
         **kwargs
     )
@@ -156,17 +160,33 @@ def generate_key(org, user):
         sys.stderr.write(msg.format(user))
         return
 
-    basedir = os.path.join(TASKD_DATA_DIR, "keys", org, user)
-    if os.path.exists(basedir):
+    keysdir = os.path.join(TASKD_DATA_DIR, "keys" )
+    orgdir  = os.path.join(keysdir       , org    )
+    userdir = os.path.join(orgdir        , user   )
+    if os.path.exists(userdir):
         raise OSError("Keyfile directory for {} already exists.".format(user))
 
-    privkey = os.path.join(basedir, "private.key")
-    pubcert = os.path.join(basedir, "public.cert")
+    privkey = os.path.join(userdir, "private.key")
+    pubcert = os.path.join(userdir, "public.cert")
 
     try:
-        os.makedirs(basedir, mode=0o700)
+        # We change the permissions and the owner ship of the base directories
+        # so that cfg.group and cfg.user could read the directories' contents.
+        # See also: https://bugs.python.org/issue42367
+        for bd in [keysdir, orgdir, userdir]:
+            # Allow cfg.group, but not others to read the contents of this group
+            os.makedirs(bd, exist_ok=True)
+            # not using mode= argument to makedirs intentionally - forcing the
+            # permissions we want
+            os.chmod(bd, mode=0o750)
+            os.chown(
+                bd,
+                uid=pwd.getpwnam(TASKD_USER).pw_uid,
+                gid=grp.getgrnam(TASKD_GROUP).gr_gid,
+            )
 
         certtool_cmd("-p", "--bits", CERT_BITS, "--outfile", privkey)
+        os.chmod(privkey, 0o640)
 
         template_data = [
             "organization = {0}".format(org),
@@ -187,7 +207,7 @@ def generate_key(org, user):
                 "--outfile", pubcert
             )
     except:
-        rmtree(basedir)
+        rmtree(userdir)
         raise
 
 
diff --git a/nixos/modules/services/monitoring/mackerel-agent.nix b/nixos/modules/services/monitoring/mackerel-agent.nix
index 62a7858500f2..5915634ed26f 100644
--- a/nixos/modules/services/monitoring/mackerel-agent.nix
+++ b/nixos/modules/services/monitoring/mackerel-agent.nix
@@ -84,6 +84,7 @@ in {
     # upstream service file in https://git.io/JUt4Q
     systemd.services.mackerel-agent = {
       description = "mackerel.io agent";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" "nss-lookup.target" ];
       wantedBy = [ "multi-user.target" ];
       environment = {
diff --git a/nixos/modules/services/monitoring/prometheus/alertmanager.nix b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
index 4fd630015f35..bb426d8b7beb 100644
--- a/nixos/modules/services/monitoring/prometheus/alertmanager.nix
+++ b/nixos/modules/services/monitoring/prometheus/alertmanager.nix
@@ -174,6 +174,7 @@ in {
 
       systemd.services.alertmanager = {
         wantedBy = [ "multi-user.target" ];
+        wants    = [ "network-online.target" ];
         after    = [ "network-online.target" ];
         preStart = ''
            ${lib.getBin pkgs.envsubst}/bin/envsubst -o "/tmp/alert-manager-substituted.yaml" \
diff --git a/nixos/modules/services/monitoring/teamviewer.nix b/nixos/modules/services/monitoring/teamviewer.nix
index 9b1278317943..7c45247aa6d5 100644
--- a/nixos/modules/services/monitoring/teamviewer.nix
+++ b/nixos/modules/services/monitoring/teamviewer.nix
@@ -30,6 +30,7 @@ in
       description = "TeamViewer remote control daemon";
 
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" "network.target" "dbus.service" ];
       requires = [ "dbus.service" ];
       preStart = "mkdir -pv /var/lib/teamviewer /var/log/teamviewer";
diff --git a/nixos/modules/services/monitoring/telegraf.nix b/nixos/modules/services/monitoring/telegraf.nix
index ee28ee03adf3..3bab8aba7bd6 100644
--- a/nixos/modules/services/monitoring/telegraf.nix
+++ b/nixos/modules/services/monitoring/telegraf.nix
@@ -59,6 +59,7 @@ in {
     in {
       description = "Telegraf Agent";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       path = lib.optional (config.services.telegraf.extraConfig.inputs ? procstat) pkgs.procps;
       serviceConfig = {
diff --git a/nixos/modules/services/monitoring/watchdogd.nix b/nixos/modules/services/monitoring/watchdogd.nix
new file mode 100644
index 000000000000..e8d104651c6a
--- /dev/null
+++ b/nixos/modules/services/monitoring/watchdogd.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+with lib;
+let
+  cfg = config.services.watchdogd;
+
+  mkPluginOpts = plugin: defWarn: defCrit: {
+    enabled = mkEnableOption "watchdogd plugin ${plugin}";
+    interval = mkOption {
+      type = types.ints.unsigned;
+      default = 300;
+      description = ''
+        Amount of seconds between every poll.
+      '';
+    };
+    logmark = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to log current stats every poll interval.
+      '';
+    };
+    warning = mkOption {
+      type = types.numbers.nonnegative;
+      default = defWarn;
+      description = ''
+        The high watermark level. Alert sent to log.
+      '';
+    };
+    critical = mkOption {
+      type = types.numbers.nonnegative;
+      default = defCrit;
+      description = ''
+        The critical watermark level. Alert sent to log, followed by reboot or script action.
+      '';
+    };
+  };
+in {
+  options.services.watchdogd = {
+    enable = mkEnableOption "watchdogd, an advanced system & process supervisor";
+    package = mkPackageOption pkgs "watchdogd" { };
+
+    settings = mkOption {
+      type = with types; submodule {
+        freeformType = let
+          valueType = oneOf [
+            bool
+            int
+            float
+            str
+          ];
+        in attrsOf (either valueType (attrsOf valueType));
+
+        options = {
+          timeout = mkOption {
+            type = types.ints.unsigned;
+            default = 15;
+            description = ''
+              The WDT timeout before reset.
+            '';
+          };
+          interval = mkOption {
+            type = types.ints.unsigned;
+            default = 5;
+            description = ''
+              The kick interval, i.e. how often {manpage}`watchdogd(8)` should reset the WDT timer.
+            '';
+          };
+
+          safe-exit = mkOption {
+            type = types.bool;
+            default = true;
+            description = ''
+              With {var}`safeExit` enabled, the daemon will ask the driver to disable the WDT before exiting.
+              However, some WDT drivers (or hardware) may not support this.
+            '';
+          };
+
+          filenr = mkPluginOpts "filenr" 0.9 1.0;
+
+          loadavg = mkPluginOpts "loadavg" 1.0 2.0;
+
+          meminfo = mkPluginOpts "meminfo" 0.9 0.95;
+        };
+      };
+      default = { };
+      description = ''
+        Configuration to put in {file}`watchdogd.conf`.
+        See {manpage}`watchdogd.conf(5)` for more details.
+      '';
+    };
+  };
+
+  config = let
+    toConfig = attrs: concatStringsSep "\n" (mapAttrsToList toValue attrs);
+
+    toValue = name: value:
+      if isAttrs value
+        then pipe value [
+          (mapAttrsToList toValue)
+          (map (s: "  ${s}"))
+          (concatStringsSep "\n")
+          (s: "${name} {\n${s}\n}")
+        ]
+      else if isBool value
+        then "${name} = ${boolToString value}"
+      else if any (f: f value) [isString isInt isFloat]
+        then "${name} = ${toString value}"
+      else throw ''
+        Found invalid type in `services.watchdogd.settings`: '${typeOf value}'
+      '';
+
+    watchdogdConf = pkgs.writeText "watchdogd.conf" (toConfig cfg.settings);
+  in mkIf cfg.enable {
+    environment.systemPackages = [ cfg.package ];
+
+    systemd.services.watchdogd = {
+      documentation = [
+        "man:watchdogd(8)"
+        "man:watchdogd.conf(5)"
+      ];
+      wantedBy = [ "multi-user.target" ];
+      description = "Advanced system & process supervisor";
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${cfg.package}/bin/watchdogd -n -f ${watchdogdConf}";
+      };
+    };
+  };
+
+  meta.maintainers = with maintainers; [ vifino ];
+}
diff --git a/nixos/modules/services/network-filesystems/openafs/client.nix b/nixos/modules/services/network-filesystems/openafs/client.nix
index bb0fee087e62..02c3482ec657 100644
--- a/nixos/modules/services/network-filesystems/openafs/client.nix
+++ b/nixos/modules/services/network-filesystems/openafs/client.nix
@@ -215,6 +215,7 @@ in
     systemd.services.afsd = {
       description = "AFS client";
       wantedBy = [ "multi-user.target" ];
+      wants = lib.optional (!cfg.startDisconnected) "network-online.target";
       after = singleton (if cfg.startDisconnected then  "network.target" else "network-online.target");
       serviceConfig = { RemainAfterExit = true; };
       restartIfChanged = false;
diff --git a/nixos/modules/services/network-filesystems/samba.nix b/nixos/modules/services/network-filesystems/samba.nix
index 5d02eac8e9f1..ef368ddbeefd 100644
--- a/nixos/modules/services/network-filesystems/samba.nix
+++ b/nixos/modules/services/network-filesystems/samba.nix
@@ -154,7 +154,7 @@ in
       };
 
       securityType = mkOption {
-        type = types.str;
+        type = types.enum [ "auto" "user" "domain" "ads" ];
         default = "user";
         description = lib.mdDoc "Samba security type";
       };
diff --git a/nixos/modules/services/networking/bird.nix b/nixos/modules/services/networking/bird.nix
index 9deeb7694d2a..e25f5c7b0379 100644
--- a/nixos/modules/services/networking/bird.nix
+++ b/nixos/modules/services/networking/bird.nix
@@ -18,6 +18,13 @@ in
           <http://bird.network.cz/>
         '';
       };
+      autoReload = mkOption {
+        type = types.bool;
+        default = true;
+        description = lib.mdDoc ''
+          Whether bird2 should be automatically reloaded when the configuration changes.
+        '';
+      };
       checkConfig = mkOption {
         type = types.bool;
         default = true;
@@ -68,7 +75,7 @@ in
     systemd.services.bird2 = {
       description = "BIRD Internet Routing Daemon";
       wantedBy = [ "multi-user.target" ];
-      reloadTriggers = [ config.environment.etc."bird/bird2.conf".source ];
+      reloadTriggers = lib.optional cfg.autoReload config.environment.etc."bird/bird2.conf".source;
       serviceConfig = {
         Type = "forking";
         Restart = "on-failure";
diff --git a/nixos/modules/services/networking/bitcoind.nix b/nixos/modules/services/networking/bitcoind.nix
index 4512e666ba5b..59722e31c62a 100644
--- a/nixos/modules/services/networking/bitcoind.nix
+++ b/nixos/modules/services/networking/bitcoind.nix
@@ -198,6 +198,7 @@ in
         '';
       in {
         description = "Bitcoin daemon";
+        wants = [ "network-online.target" ];
         after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
diff --git a/nixos/modules/services/networking/dante.nix b/nixos/modules/services/networking/dante.nix
index 605f2d74f827..f0d1d6305c54 100644
--- a/nixos/modules/services/networking/dante.nix
+++ b/nixos/modules/services/networking/dante.nix
@@ -47,6 +47,7 @@ in
 
     systemd.services.dante = {
       description   = "Dante SOCKS v4 and v5 compatible proxy server";
+      wants         = [ "network-online.target" ];
       after         = [ "network-online.target" ];
       wantedBy      = [ "multi-user.target" ];
 
diff --git a/nixos/modules/services/networking/ergo.nix b/nixos/modules/services/networking/ergo.nix
index 033d4d9caf8a..1bee0f43f988 100644
--- a/nixos/modules/services/networking/ergo.nix
+++ b/nixos/modules/services/networking/ergo.nix
@@ -114,6 +114,7 @@ in {
     systemd.services.ergo = {
       description = "ergo server";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
         User = cfg.user;
diff --git a/nixos/modules/services/networking/expressvpn.nix b/nixos/modules/services/networking/expressvpn.nix
index 30de6987d31f..05c24d8bccff 100644
--- a/nixos/modules/services/networking/expressvpn.nix
+++ b/nixos/modules/services/networking/expressvpn.nix
@@ -21,6 +21,7 @@ with lib;
         RestartSec = 5;
       };
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
     };
   };
diff --git a/nixos/modules/services/networking/headscale.nix b/nixos/modules/services/networking/headscale.nix
index 4224a0578cc3..95b5fcf6ebde 100644
--- a/nixos/modules/services/networking/headscale.nix
+++ b/nixos/modules/services/networking/headscale.nix
@@ -460,6 +460,7 @@ in {
 
     systemd.services.headscale = {
       description = "headscale coordination server for Tailscale";
+      wants = [ "network-online.target" ];
       after = ["network-online.target"];
       wantedBy = ["multi-user.target"];
       restartTriggers = [configFile];
diff --git a/nixos/modules/services/networking/ircd-hybrid/default.nix b/nixos/modules/services/networking/ircd-hybrid/default.nix
index 554b0f7bb8b4..64a34cc52d25 100644
--- a/nixos/modules/services/networking/ircd-hybrid/default.nix
+++ b/nixos/modules/services/networking/ircd-hybrid/default.nix
@@ -125,7 +125,8 @@ in
 
     systemd.services.ircd-hybrid = {
       description = "IRCD Hybrid server";
-      after = [ "started networking" ];
+      wants = [ "network-online.target" ];
+      after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       script = "${ircdService}/bin/control start";
     };
diff --git a/nixos/modules/services/networking/ivpn.nix b/nixos/modules/services/networking/ivpn.nix
index 6df630c1f194..6c9ae599e670 100644
--- a/nixos/modules/services/networking/ivpn.nix
+++ b/nixos/modules/services/networking/ivpn.nix
@@ -27,7 +27,7 @@ with lib;
     systemd.services.ivpn-service = {
       description = "iVPN daemon";
       wantedBy = [ "multi-user.target" ];
-      wants = [ "network.target" ];
+      wants = [ "network.target" "network-online.target" ];
       after = [
         "network-online.target"
         "NetworkManager.service"
diff --git a/nixos/modules/services/networking/kea.nix b/nixos/modules/services/networking/kea.nix
index 5ca705976c41..656ddd41fd12 100644
--- a/nixos/modules/services/networking/kea.nix
+++ b/nixos/modules/services/networking/kea.nix
@@ -325,6 +325,9 @@ in
         "network-online.target"
         "time-sync.target"
       ];
+      wants = [
+        "network-online.target"
+      ];
       wantedBy = [
         "multi-user.target"
       ];
@@ -372,6 +375,9 @@ in
         "network-online.target"
         "time-sync.target"
       ];
+      wants = [
+        "network-online.target"
+      ];
       wantedBy = [
         "multi-user.target"
       ];
@@ -413,6 +419,7 @@ in
         "https://kea.readthedocs.io/en/kea-${package.version}/arm/ddns.html"
       ];
 
+      wants = [ "network-online.target" ];
       after = [
         "network-online.target"
         "time-sync.target"
diff --git a/nixos/modules/services/networking/mosquitto.nix b/nixos/modules/services/networking/mosquitto.nix
index f2b158b98942..ad9eefb42252 100644
--- a/nixos/modules/services/networking/mosquitto.nix
+++ b/nixos/modules/services/networking/mosquitto.nix
@@ -596,6 +596,7 @@ in
     systemd.services.mosquitto = {
       description = "Mosquitto MQTT Broker Daemon";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
         Type = "notify";
diff --git a/nixos/modules/services/networking/mullvad-vpn.nix b/nixos/modules/services/networking/mullvad-vpn.nix
index 446c71f40764..5da4ca1d1d80 100644
--- a/nixos/modules/services/networking/mullvad-vpn.nix
+++ b/nixos/modules/services/networking/mullvad-vpn.nix
@@ -53,7 +53,7 @@ with lib;
     systemd.services.mullvad-daemon = {
       description = "Mullvad VPN daemon";
       wantedBy = [ "multi-user.target" ];
-      wants = [ "network.target" ];
+      wants = [ "network.target" "network-online.target" ];
       after = [
         "network-online.target"
         "NetworkManager.service"
diff --git a/nixos/modules/services/networking/nbd.nix b/nixos/modules/services/networking/nbd.nix
index 454380aa3154..b4bf7ede8463 100644
--- a/nixos/modules/services/networking/nbd.nix
+++ b/nixos/modules/services/networking/nbd.nix
@@ -117,6 +117,7 @@ in
     boot.kernelModules = [ "nbd" ];
 
     systemd.services.nbd-server = {
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       before = [ "multi-user.target" ];
       wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/networking/ocserv.nix b/nixos/modules/services/networking/ocserv.nix
index 9548fd92dbda..3c61d56b893e 100644
--- a/nixos/modules/services/networking/ocserv.nix
+++ b/nixos/modules/services/networking/ocserv.nix
@@ -85,6 +85,7 @@ in
     systemd.services.ocserv = {
       description = "OpenConnect SSL VPN server";
       documentation = [ "man:ocserv(8)" ];
+      wants = [ "network-online.target" ];
       after = [ "dbus.service" "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
 
diff --git a/nixos/modules/services/networking/pleroma.nix b/nixos/modules/services/networking/pleroma.nix
index db0a61b83469..8470f5e9cbc0 100644
--- a/nixos/modules/services/networking/pleroma.nix
+++ b/nixos/modules/services/networking/pleroma.nix
@@ -92,6 +92,7 @@ in {
 
     systemd.services.pleroma = {
       description = "Pleroma social network";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" "postgresql.service" ];
       wantedBy = [ "multi-user.target" ];
       restartTriggers = [ config.environment.etc."/pleroma/config.exs".source ];
diff --git a/nixos/modules/services/networking/rosenpass.nix b/nixos/modules/services/networking/rosenpass.nix
index d2a264b83d67..487cb6f60142 100644
--- a/nixos/modules/services/networking/rosenpass.nix
+++ b/nixos/modules/services/networking/rosenpass.nix
@@ -208,6 +208,7 @@ in
       in
       rec {
         wantedBy = [ "multi-user.target" ];
+        wants = [ "network-online.target" ];
         after = [ "network-online.target" ];
         path = [ cfg.package pkgs.wireguard-tools ];
 
diff --git a/nixos/modules/services/networking/rxe.nix b/nixos/modules/services/networking/rxe.nix
index 7dbb4823b4bc..07437ed71195 100644
--- a/nixos/modules/services/networking/rxe.nix
+++ b/nixos/modules/services/networking/rxe.nix
@@ -33,7 +33,7 @@ in {
 
       wantedBy = [ "multi-user.target" ];
       after = [ "systemd-modules-load.service" "network-online.target" ];
-      wants = [ "network-pre.target" ];
+      wants = [ "network-pre.target" "network-online.target" ];
 
       serviceConfig = {
         Type = "oneshot";
diff --git a/nixos/modules/services/networking/soju.nix b/nixos/modules/services/networking/soju.nix
index 7f0ac3e3b8e6..d69ec08ca13a 100644
--- a/nixos/modules/services/networking/soju.nix
+++ b/nixos/modules/services/networking/soju.nix
@@ -110,6 +110,7 @@ in
     systemd.services.soju = {
       description = "soju IRC bouncer";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
         DynamicUser = true;
diff --git a/nixos/modules/services/networking/strongswan-swanctl/module.nix b/nixos/modules/services/networking/strongswan-swanctl/module.nix
index c8832ed4defb..a98850923955 100644
--- a/nixos/modules/services/networking/strongswan-swanctl/module.nix
+++ b/nixos/modules/services/networking/strongswan-swanctl/module.nix
@@ -55,6 +55,7 @@ in  {
     systemd.services.strongswan-swanctl = {
       description = "strongSwan IPsec IKEv1/IKEv2 daemon using swanctl";
       wantedBy = [ "multi-user.target" ];
+      wants    = [ "network-online.target" ];
       after    = [ "network-online.target" ];
       path     = with pkgs; [ kmod iproute2 iptables util-linux ];
       environment = {
diff --git a/nixos/modules/services/networking/strongswan.nix b/nixos/modules/services/networking/strongswan.nix
index e58526814d1a..dcf04d2a1917 100644
--- a/nixos/modules/services/networking/strongswan.nix
+++ b/nixos/modules/services/networking/strongswan.nix
@@ -153,6 +153,7 @@ in
       description = "strongSwan IPSec Service";
       wantedBy = [ "multi-user.target" ];
       path = with pkgs; [ kmod iproute2 iptables util-linux ]; # XXX Linux
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       environment = {
         STRONGSWAN_CONF = strongswanConf { inherit setup connections ca secretsFile managePlugins enabledPlugins; };
diff --git a/nixos/modules/services/networking/syncplay.nix b/nixos/modules/services/networking/syncplay.nix
index 0a66d93bf153..151259b6d4ad 100644
--- a/nixos/modules/services/networking/syncplay.nix
+++ b/nixos/modules/services/networking/syncplay.nix
@@ -107,6 +107,7 @@ in
     systemd.services.syncplay = {
       description = "Syncplay Service";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/networking/wasabibackend.nix b/nixos/modules/services/networking/wasabibackend.nix
index 938145b35ee8..e3a48afd2a2c 100644
--- a/nixos/modules/services/networking/wasabibackend.nix
+++ b/nixos/modules/services/networking/wasabibackend.nix
@@ -119,6 +119,7 @@ in {
     systemd.services.wasabibackend = {
       description = "wasabibackend server";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       environment = {
         DOTNET_PRINT_TELEMETRY_MESSAGE = "false";
diff --git a/nixos/modules/services/networking/znc/default.nix b/nixos/modules/services/networking/znc/default.nix
index d3ba4a524197..e15233293cf2 100644
--- a/nixos/modules/services/networking/znc/default.nix
+++ b/nixos/modules/services/networking/znc/default.nix
@@ -243,6 +243,7 @@ in
     systemd.services.znc = {
       description = "ZNC Server";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       serviceConfig = {
         User = cfg.user;
diff --git a/nixos/modules/services/security/certmgr.nix b/nixos/modules/services/security/certmgr.nix
index db80e943973d..02cb7afe87ba 100644
--- a/nixos/modules/services/security/certmgr.nix
+++ b/nixos/modules/services/security/certmgr.nix
@@ -182,6 +182,7 @@ in
     systemd.services.certmgr = {
       description = "certmgr";
       path = mkIf (cfg.svcManager == "command") [ pkgs.bash ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       inherit preStart;
diff --git a/nixos/modules/services/security/clamav.nix b/nixos/modules/services/security/clamav.nix
index d3164373ec01..4480c0cae60c 100644
--- a/nixos/modules/services/security/clamav.nix
+++ b/nixos/modules/services/security/clamav.nix
@@ -196,6 +196,7 @@ in
     systemd.services.clamav-freshclam = mkIf cfg.updater.enable {
       description = "ClamAV virus database updater (freshclam)";
       restartTriggers = [ freshclamConfigFile ];
+      requires = [ "network-online.target" ];
       after = [ "network-online.target" ];
 
       serviceConfig = {
@@ -243,6 +244,7 @@ in
     systemd.services.clamav-fangfrisch = mkIf cfg.fangfrisch.enable {
       description = "ClamAV virus database updater (fangfrisch)";
       restartTriggers = [ fangfrischConfigFile ];
+      requires = [ "network-online.target" ];
       after = [ "network-online.target" "clamav-fangfrisch-init.service" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/security/oauth2_proxy.nix b/nixos/modules/services/security/oauth2_proxy.nix
index 78916c907279..d1dc37d549d2 100644
--- a/nixos/modules/services/security/oauth2_proxy.nix
+++ b/nixos/modules/services/security/oauth2_proxy.nix
@@ -572,6 +572,7 @@ in
       description = "OAuth2 Proxy";
       path = [ cfg.package ];
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/system/cachix-agent/default.nix b/nixos/modules/services/system/cachix-agent/default.nix
index 196d3291d555..f8020fe970f1 100644
--- a/nixos/modules/services/system/cachix-agent/default.nix
+++ b/nixos/modules/services/system/cachix-agent/default.nix
@@ -49,6 +49,7 @@ in {
   config = mkIf cfg.enable {
     systemd.services.cachix-agent = {
       description = "Cachix Deploy Agent";
+      wants = [ "network-online.target" ];
       after = ["network-online.target"];
       path = [ config.nix.package ];
       wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/system/cachix-watch-store.nix b/nixos/modules/services/system/cachix-watch-store.nix
index 8aa5f0358fa9..d48af29465aa 100644
--- a/nixos/modules/services/system/cachix-watch-store.nix
+++ b/nixos/modules/services/system/cachix-watch-store.nix
@@ -61,6 +61,7 @@ in
   config = mkIf cfg.enable {
     systemd.services.cachix-watch-store-agent = {
       description = "Cachix watch store Agent";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       path = [ config.nix.package ];
       wantedBy = [ "multi-user.target" ];
diff --git a/nixos/modules/services/system/cloud-init.nix b/nixos/modules/services/system/cloud-init.nix
index d782bb1a3666..00ae77be4271 100644
--- a/nixos/modules/services/system/cloud-init.nix
+++ b/nixos/modules/services/system/cloud-init.nix
@@ -164,7 +164,10 @@ in
     systemd.services.cloud-init-local = {
       description = "Initial cloud-init job (pre-networking)";
       wantedBy = [ "multi-user.target" ];
-      before = [ "systemd-networkd.service" ];
+      # In certain environments (AWS for example), cloud-init-local will
+      # first configure an IP through DHCP, and later delete it.
+      # This can cause race conditions with anything else trying to set IP through DHCP.
+      before = [ "systemd-networkd.service" "dhcpcd.service" ];
       path = path;
       serviceConfig = {
         Type = "oneshot";
diff --git a/nixos/modules/services/video/go2rtc/default.nix b/nixos/modules/services/video/go2rtc/default.nix
index 13851fa0306f..9dddbb60baa8 100644
--- a/nixos/modules/services/video/go2rtc/default.nix
+++ b/nixos/modules/services/video/go2rtc/default.nix
@@ -94,6 +94,7 @@ in
 
   config = lib.mkIf cfg.enable {
     systemd.services.go2rtc = {
+      wants = [ "network-online.target" ];
       after = [
         "network-online.target"
       ];
diff --git a/nixos/modules/services/web-apps/akkoma.nix b/nixos/modules/services/web-apps/akkoma.nix
index 8980556ab014..4cd9e2664378 100644
--- a/nixos/modules/services/web-apps/akkoma.nix
+++ b/nixos/modules/services/web-apps/akkoma.nix
@@ -974,7 +974,7 @@ in {
       # This service depends on network-online.target and is sequenced after
       # it because it requires access to the Internet to function properly.
       bindsTo = [ "akkoma-config.service" ];
-      wants = [ "network-online.service" ];
+      wants = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       after = [
         "akkoma-config.target"
diff --git a/nixos/modules/services/web-apps/alps.nix b/nixos/modules/services/web-apps/alps.nix
index 05fb676102df..81c6b8ad30b5 100644
--- a/nixos/modules/services/web-apps/alps.nix
+++ b/nixos/modules/services/web-apps/alps.nix
@@ -94,6 +94,7 @@ in {
       description = "alps is a simple and extensible webmail.";
       documentation = [ "https://git.sr.ht/~migadu/alps" ];
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/web-apps/c2fmzq-server.nix b/nixos/modules/services/web-apps/c2fmzq-server.nix
index 87938fe160e1..dee131182de1 100644
--- a/nixos/modules/services/web-apps/c2fmzq-server.nix
+++ b/nixos/modules/services/web-apps/c2fmzq-server.nix
@@ -80,6 +80,7 @@ in {
       description = "c2FmZQ-server";
       documentation = [ "https://github.com/c2FmZQ/c2FmZQ/blob/main/README.md" ];
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
 
       serviceConfig = {
diff --git a/nixos/modules/services/web-apps/code-server.nix b/nixos/modules/services/web-apps/code-server.nix
index 11601f6c3044..d087deb7848d 100644
--- a/nixos/modules/services/web-apps/code-server.nix
+++ b/nixos/modules/services/web-apps/code-server.nix
@@ -205,6 +205,7 @@ in {
     systemd.services.code-server = {
       description = "Code server";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       path = cfg.extraPackages;
       environment = {
diff --git a/nixos/modules/services/web-apps/healthchecks.nix b/nixos/modules/services/web-apps/healthchecks.nix
index e5e425a29d54..1d439f162313 100644
--- a/nixos/modules/services/web-apps/healthchecks.nix
+++ b/nixos/modules/services/web-apps/healthchecks.nix
@@ -176,6 +176,7 @@ in
     systemd.targets.healthchecks = {
       description = "Target for all Healthchecks services";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
     };
 
diff --git a/nixos/modules/services/web-apps/netbox.nix b/nixos/modules/services/web-apps/netbox.nix
index 88d40b3abc52..72ec578146a7 100644
--- a/nixos/modules/services/web-apps/netbox.nix
+++ b/nixos/modules/services/web-apps/netbox.nix
@@ -267,6 +267,7 @@ in {
     systemd.targets.netbox = {
       description = "Target for all NetBox services";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" "redis-netbox.service" ];
     };
 
diff --git a/nixos/modules/services/web-apps/nextcloud.nix b/nixos/modules/services/web-apps/nextcloud.nix
index 38c51251aac1..0b19265942c0 100644
--- a/nixos/modules/services/web-apps/nextcloud.nix
+++ b/nixos/modules/services/web-apps/nextcloud.nix
@@ -99,11 +99,101 @@ let
   mysqlLocal = cfg.database.createLocally && cfg.config.dbtype == "mysql";
   pgsqlLocal = cfg.database.createLocally && cfg.config.dbtype == "pgsql";
 
+  nextcloudGreaterOrEqualThan = versionAtLeast cfg.package.version;
+  nextcloudOlderThan = versionOlder cfg.package.version;
+
   # https://github.com/nextcloud/documentation/pull/11179
-  ocmProviderIsNotAStaticDirAnymore = versionAtLeast cfg.package.version "27.1.2"
-    || (versionOlder cfg.package.version "27.0.0"
-      && versionAtLeast cfg.package.version "26.0.8");
+  ocmProviderIsNotAStaticDirAnymore = nextcloudGreaterOrEqualThan "27.1.2"
+    || (nextcloudOlderThan "27.0.0" && nextcloudGreaterOrEqualThan "26.0.8");
+
+  overrideConfig = let
+    c = cfg.config;
+    requiresReadSecretFunction = c.dbpassFile != null || c.objectstore.s3.enable;
+    objectstoreConfig = let s3 = c.objectstore.s3; in optionalString s3.enable ''
+      'objectstore' => [
+        'class' => '\\OC\\Files\\ObjectStore\\S3',
+        'arguments' => [
+          'bucket' => '${s3.bucket}',
+          'autocreate' => ${boolToString s3.autocreate},
+          'key' => '${s3.key}',
+          'secret' => nix_read_secret('${s3.secretFile}'),
+          ${optionalString (s3.hostname != null) "'hostname' => '${s3.hostname}',"}
+          ${optionalString (s3.port != null) "'port' => ${toString s3.port},"}
+          'use_ssl' => ${boolToString s3.useSsl},
+          ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
+          'use_path_style' => ${boolToString s3.usePathStyle},
+          ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('${s3.sseCKeyFile}'),"}
+        ],
+      ]
+    '';
+    showAppStoreSetting = cfg.appstoreEnable != null || cfg.extraApps != {};
+    renderedAppStoreSetting =
+      let
+        x = cfg.appstoreEnable;
+      in
+        if x == null then "false"
+        else boolToString x;
+    mkAppStoreConfig = name: { enabled, writable, ... }: optionalString enabled ''
+      [ 'path' => '${webroot}/${name}', 'url' => '/${name}', 'writable' => ${boolToString writable} ],
+    '';
+  in pkgs.writeText "nextcloud-config.php" ''
+    <?php
+    ${optionalString requiresReadSecretFunction ''
+      function nix_read_secret($file) {
+        if (!file_exists($file)) {
+          throw new \RuntimeException(sprintf(
+            "Cannot start Nextcloud, secret file %s set by NixOS doesn't seem to "
+            . "exist! Please make sure that the file exists and has appropriate "
+            . "permissions for user & group 'nextcloud'!",
+            $file
+          ));
+        }
+        return trim(file_get_contents($file));
+      }''}
+    function nix_decode_json_file($file, $error) {
+      if (!file_exists($file)) {
+        throw new \RuntimeException(sprintf($error, $file));
+      }
+      $decoded = json_decode(file_get_contents($file), true);
+
+      if (json_last_error() !== JSON_ERROR_NONE) {
+        throw new \RuntimeException(sprintf("Cannot decode %s, because: %s", $file, json_last_error_msg()));
+      }
 
+      return $decoded;
+    }
+    $CONFIG = [
+      'apps_paths' => [
+        ${concatStrings (mapAttrsToList mkAppStoreConfig appStores)}
+      ],
+      ${optionalString (showAppStoreSetting) "'appstoreenabled' => ${renderedAppStoreSetting},"}
+      ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
+      ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
+      ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
+      ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
+      ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
+      ${optionalString (c.dbpassFile != null) ''
+          'dbpassword' => nix_read_secret(
+            "${c.dbpassFile}"
+          ),
+        ''
+      }
+      'dbtype' => '${c.dbtype}',
+      ${objectstoreConfig}
+    ];
+
+    $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
+      "${jsonFormat.generate "nextcloud-extraOptions.json" cfg.extraOptions}",
+      "impossible: this should never happen (decoding generated extraOptions file %s failed)"
+    ));
+
+    ${optionalString (cfg.secretFile != null) ''
+      $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
+        "${cfg.secretFile}",
+        "Cannot start Nextcloud, secrets file %s set by NixOS doesn't exist!"
+      ));
+    ''}
+  '';
 in {
 
   imports = [
@@ -787,107 +877,23 @@ in {
         timerConfig.Unit = "nextcloud-cron.service";
       };
 
-      systemd.tmpfiles.rules = ["d ${cfg.home} 0750 nextcloud nextcloud"];
+      systemd.tmpfiles.rules = map (dir: "d ${dir} 0750 nextcloud nextcloud - -") [
+        "${cfg.home}"
+        "${datadir}/config"
+        "${datadir}/data"
+        "${cfg.home}/store-apps"
+      ] ++ [
+        "L+ ${datadir}/config/override.config.php - - - - ${overrideConfig}"
+      ];
 
       systemd.services = {
         # When upgrading the Nextcloud package, Nextcloud can report errors such as
         # "The files of the app [all apps in /var/lib/nextcloud/apps] were not replaced correctly"
         # Restarting phpfpm on Nextcloud package update fixes these issues (but this is a workaround).
-        phpfpm-nextcloud.restartTriggers = [ webroot ];
+        phpfpm-nextcloud.restartTriggers = [ webroot overrideConfig ];
 
         nextcloud-setup = let
           c = cfg.config;
-          requiresReadSecretFunction = c.dbpassFile != null || c.objectstore.s3.enable;
-          objectstoreConfig = let s3 = c.objectstore.s3; in optionalString s3.enable ''
-            'objectstore' => [
-              'class' => '\\OC\\Files\\ObjectStore\\S3',
-              'arguments' => [
-                'bucket' => '${s3.bucket}',
-                'autocreate' => ${boolToString s3.autocreate},
-                'key' => '${s3.key}',
-                'secret' => nix_read_secret('${s3.secretFile}'),
-                ${optionalString (s3.hostname != null) "'hostname' => '${s3.hostname}',"}
-                ${optionalString (s3.port != null) "'port' => ${toString s3.port},"}
-                'use_ssl' => ${boolToString s3.useSsl},
-                ${optionalString (s3.region != null) "'region' => '${s3.region}',"}
-                'use_path_style' => ${boolToString s3.usePathStyle},
-                ${optionalString (s3.sseCKeyFile != null) "'sse_c_key' => nix_read_secret('${s3.sseCKeyFile}'),"}
-              ],
-            ]
-          '';
-
-          showAppStoreSetting = cfg.appstoreEnable != null || cfg.extraApps != {};
-          renderedAppStoreSetting =
-            let
-              x = cfg.appstoreEnable;
-            in
-              if x == null then "false"
-              else boolToString x;
-
-          nextcloudGreaterOrEqualThan = req: versionAtLeast cfg.package.version req;
-
-          mkAppStoreConfig = name: { enabled, writable, ... }: optionalString enabled ''
-            [ 'path' => '${webroot}/${name}', 'url' => '/${name}', 'writable' => ${boolToString writable} ],
-          '';
-
-          overrideConfig = pkgs.writeText "nextcloud-config.php" ''
-            <?php
-            ${optionalString requiresReadSecretFunction ''
-              function nix_read_secret($file) {
-                if (!file_exists($file)) {
-                  throw new \RuntimeException(sprintf(
-                    "Cannot start Nextcloud, secret file %s set by NixOS doesn't seem to "
-                    . "exist! Please make sure that the file exists and has appropriate "
-                    . "permissions for user & group 'nextcloud'!",
-                    $file
-                  ));
-                }
-                return trim(file_get_contents($file));
-              }''}
-            function nix_decode_json_file($file, $error) {
-              if (!file_exists($file)) {
-                throw new \RuntimeException(sprintf($error, $file));
-              }
-              $decoded = json_decode(file_get_contents($file), true);
-
-              if (json_last_error() !== JSON_ERROR_NONE) {
-                throw new \RuntimeException(sprintf("Cannot decode %s, because: %s", $file, json_last_error_msg()));
-              }
-
-              return $decoded;
-            }
-            $CONFIG = [
-              'apps_paths' => [
-                ${concatStrings (mapAttrsToList mkAppStoreConfig appStores)}
-              ],
-              ${optionalString (showAppStoreSetting) "'appstoreenabled' => ${renderedAppStoreSetting},"}
-              ${optionalString cfg.caching.apcu "'memcache.local' => '\\OC\\Memcache\\APCu',"}
-              ${optionalString (c.dbname != null) "'dbname' => '${c.dbname}',"}
-              ${optionalString (c.dbhost != null) "'dbhost' => '${c.dbhost}',"}
-              ${optionalString (c.dbuser != null) "'dbuser' => '${c.dbuser}',"}
-              ${optionalString (c.dbtableprefix != null) "'dbtableprefix' => '${toString c.dbtableprefix}',"}
-              ${optionalString (c.dbpassFile != null) ''
-                  'dbpassword' => nix_read_secret(
-                    "${c.dbpassFile}"
-                  ),
-                ''
-              }
-              'dbtype' => '${c.dbtype}',
-              ${objectstoreConfig}
-            ];
-
-            $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
-              "${jsonFormat.generate "nextcloud-extraOptions.json" cfg.extraOptions}",
-              "impossible: this should never happen (decoding generated extraOptions file %s failed)"
-            ));
-
-            ${optionalString (cfg.secretFile != null) ''
-              $CONFIG = array_replace_recursive($CONFIG, nix_decode_json_file(
-                "${cfg.secretFile}",
-                "Cannot start Nextcloud, secrets file %s set by NixOS doesn't exist!"
-              ));
-            ''}
-          '';
           occInstallCmd = let
             mkExport = { arg, value }: "export ${arg}=${value}";
             dbpass = {
@@ -932,6 +938,7 @@ in {
           after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
           requires = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
           path = [ occ ];
+          restartTriggers = [ overrideConfig ];
           script = ''
             ${optionalString (c.dbpassFile != null) ''
               if [ ! -r "${c.dbpassFile}" ]; then
@@ -959,18 +966,6 @@ in {
               fi
             '') [ "nix-apps" "apps" ]}
 
-            # create nextcloud directories.
-            # if the directories exist already with wrong permissions, we fix that
-            for dir in ${datadir}/config ${datadir}/data ${cfg.home}/store-apps; do
-              if [ ! -e $dir ]; then
-                install -o nextcloud -g nextcloud -d $dir
-              elif [ $(stat -c "%G" $dir) != "nextcloud" ]; then
-                chgrp -R nextcloud $dir
-              fi
-            done
-
-            ln -sf ${overrideConfig} ${datadir}/config/override.config.php
-
             # Do not install if already installed
             if [[ ! -e ${datadir}/config/config.php ]]; then
               ${occInstallCmd}
diff --git a/nixos/modules/services/web-apps/openvscode-server.nix b/nixos/modules/services/web-apps/openvscode-server.nix
index 76a19dccae16..81b9d1f3b4c8 100644
--- a/nixos/modules/services/web-apps/openvscode-server.nix
+++ b/nixos/modules/services/web-apps/openvscode-server.nix
@@ -159,6 +159,7 @@ in
     systemd.services.openvscode-server = {
       description = "OpenVSCode server";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       path = cfg.extraPackages;
       environment = cfg.extraEnvironment;
diff --git a/nixos/modules/services/web-apps/peering-manager.nix b/nixos/modules/services/web-apps/peering-manager.nix
index d6f6077268d4..0382ce717473 100644
--- a/nixos/modules/services/web-apps/peering-manager.nix
+++ b/nixos/modules/services/web-apps/peering-manager.nix
@@ -196,6 +196,7 @@ in {
     systemd.targets.peering-manager = {
       description = "Target for all Peering Manager services";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" "redis-peering-manager.service" ];
     };
 
diff --git a/nixos/modules/services/web-apps/suwayomi-server.md b/nixos/modules/services/web-apps/suwayomi-server.md
new file mode 100644
index 000000000000..ff1e06c8a53a
--- /dev/null
+++ b/nixos/modules/services/web-apps/suwayomi-server.md
@@ -0,0 +1,108 @@
+# Suwayomi-Server {#module-services-suwayomi-server}
+
+A free and open source manga reader server that runs extensions built for Tachiyomi.
+
+## Basic usage {#module-services-suwayomi-server-basic-usage}
+
+By default, the module will execute Suwayomi-Server backend and web UI:
+
+```nix
+{ ... }:
+
+{
+  services.suwayomi-server = {
+    enable = true;
+  };
+}
+```
+
+It runs in the systemd service named `suwayomi-server` in the data directory `/var/lib/suwayomi-server`.
+
+You can change the default parameters with some other parameters:
+```nix
+{ ... }:
+
+{
+  services.suwayomi-server = {
+    enable = true;
+
+    dataDir = "/var/lib/suwayomi"; # Default is "/var/lib/suwayomi-server"
+    openFirewall = true;
+
+    settings = {
+      server.port = 4567;
+    };
+  };
+}
+```
+
+If you want to create a desktop icon, you can activate the system tray option:
+
+```nix
+{ ... }:
+
+{
+  services.suwayomi-server = {
+    enable = true;
+
+    dataDir = "/var/lib/suwayomi"; # Default is "/var/lib/suwayomi-server"
+    openFirewall = true;
+
+    settings = {
+      server.port = 4567;
+      server.enableSystemTray = true;
+    };
+  };
+}
+```
+
+## Basic authentication {#module-services-suwayomi-server-basic-auth}
+
+You can configure a basic authentication to the web interface with:
+
+```nix
+{ ... }:
+
+{
+  services.suwayomi-server = {
+    enable = true;
+
+    openFirewall = true;
+
+    settings = {
+      server.port = 4567;
+      server = {
+        basicAuthEnabled = true;
+        basicAuthUsername = "username";
+
+        # NOTE: this is not a real upstream option
+        basicAuthPasswordFile = ./path/to/the/password/file;
+      };
+    };
+  };
+}
+```
+
+## Extra configuration {#module-services-suwayomi-server-extra-config}
+
+Not all the configuration options are available directly in this module, but you can add the other options of suwayomi-server with:
+
+```nix
+{ ... }:
+
+{
+  services.suwayomi-server = {
+    enable = true;
+
+    openFirewall = true;
+
+    settings = {
+      server = {
+        port = 4567;
+        autoDownloadNewChapters = false;
+        maxSourcesInParallel" = 6;
+      };
+    };
+  };
+}
+```
diff --git a/nixos/modules/services/web-apps/suwayomi-server.nix b/nixos/modules/services/web-apps/suwayomi-server.nix
new file mode 100644
index 000000000000..c4c1540edbee
--- /dev/null
+++ b/nixos/modules/services/web-apps/suwayomi-server.nix
@@ -0,0 +1,260 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.suwayomi-server;
+  inherit (lib) mkOption mdDoc mkEnableOption mkIf types;
+in
+{
+  options = {
+    services.suwayomi-server = {
+      enable = mkEnableOption (mdDoc "Suwayomi, a free and open source manga reader server that runs extensions built for Tachiyomi.");
+
+      package = lib.mkPackageOptionMD pkgs "suwayomi-server" { };
+
+      dataDir = mkOption {
+        type = types.path;
+        default = "/var/lib/suwayomi-server";
+        example = "/var/data/mangas";
+        description = mdDoc ''
+          The path to the data directory in which Suwayomi-Server will download scans.
+        '';
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "suwayomi";
+        example = "root";
+        description = mdDoc ''
+          User account under which Suwayomi-Server runs.
+        '';
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "suwayomi";
+        example = "medias";
+        description = mdDoc ''
+          Group under which Suwayomi-Server runs.
+        '';
+      };
+
+      openFirewall = mkOption {
+        type = types.bool;
+        default = false;
+        description = mdDoc ''
+          Whether to open the firewall for the port in {option}`services.suwayomi-server.settings.server.port`.
+        '';
+      };
+
+      settings = mkOption {
+        type = types.submodule {
+          freeformType =
+            let
+              recursiveAttrsType = with types; attrsOf (nullOr (oneOf [
+                str
+                path
+                int
+                float
+                bool
+                (listOf str)
+                (recursiveAttrsType // { description = "instances of this type recursively"; })
+              ]));
+            in
+            recursiveAttrsType;
+          options = {
+            server = {
+              ip = mkOption {
+                type = types.str;
+                default = "0.0.0.0";
+                example = "127.0.0.1";
+                description = mdDoc ''
+                  The ip that Suwayomi will bind to.
+                '';
+              };
+
+              port = mkOption {
+                type = types.port;
+                default = 8080;
+                example = 4567;
+                description = mdDoc ''
+                  The port that Suwayomi will listen to.
+                '';
+              };
+
+              basicAuthEnabled = mkEnableOption (mdDoc ''
+                Add basic access authentication to Suwayomi-Server.
+                Enabling this option is useful when hosting on a public network/the Internet
+              '');
+
+              basicAuthUsername = mkOption {
+                type = types.nullOr types.str;
+                default = null;
+                description = mdDoc ''
+                  The username value that you have to provide when authenticating.
+                '';
+              };
+
+              # NOTE: this is not a real upstream option
+              basicAuthPasswordFile = mkOption {
+                type = types.nullOr types.path;
+                default = null;
+                example = "/var/secrets/suwayomi-server-password";
+                description = mdDoc ''
+                  The password file containing the value that you have to provide when authenticating.
+                '';
+              };
+
+              downloadAsCbz = mkOption {
+                type = types.bool;
+                default = false;
+                description = mdDoc ''
+                  Download chapters as `.cbz` files.
+                '';
+              };
+
+              localSourcePath = mkOption {
+                type = types.path;
+                default = cfg.dataDir;
+                defaultText = lib.literalExpression "suwayomi-server.dataDir";
+                example = "/var/data/local_mangas";
+                description = mdDoc ''
+                  Path to the local source folder.
+                '';
+              };
+
+              systemTrayEnabled = mkOption {
+                type = types.bool;
+                default = false;
+                description = mdDoc ''
+                  Whether to enable a system tray icon, if possible.
+                '';
+              };
+            };
+          };
+        };
+        description = mdDoc ''
+          Configuration to write to {file}`server.conf`.
+          See <https://github.com/Suwayomi/Suwayomi-Server/wiki/Configuring-Suwayomi-Server> for more information.
+        '';
+        default = { };
+        example = {
+          server.socksProxyEnabled = true;
+          server.socksProxyHost = "yourproxyhost.com";
+          server.socksProxyPort = "8080";
+        };
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    assertions = [{
+      assertion = with cfg.settings.server; basicAuthEnabled -> (basicAuthUsername != null && basicAuthPasswordFile != null);
+      message = ''
+        [suwayomi-server]: the username and the password file cannot be null when the basic auth is enabled
+      '';
+    }];
+
+    networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.settings.server.port ];
+
+    users.groups = mkIf (cfg.group == "suwayomi") {
+      suwayomi = { };
+    };
+
+    users.users = mkIf (cfg.user == "suwayomi") {
+      suwayomi = {
+        group = cfg.group;
+        # Need to set the user home because the package writes to ~/.local/Tachidesk
+        home = cfg.dataDir;
+        description = "Suwayomi Daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    systemd.tmpfiles.settings."10-suwayomi-server" = {
+      "${cfg.dataDir}/.local/share/Tachidesk".d = {
+        mode = "0700";
+        inherit (cfg) user group;
+      };
+    };
+
+    systemd.services.suwayomi-server =
+      let
+        flattenConfig = prefix: config:
+          lib.foldl'
+            lib.mergeAttrs
+            { }
+            (lib.attrValues
+              (lib.mapAttrs
+                (k: v:
+                  if !(lib.isAttrs v)
+                  then { "${prefix}${k}" = v; }
+                  else flattenConfig "${prefix}${k}." v
+                )
+                config
+              )
+            );
+
+        #  HOCON is a JSON superset that suwayomi-server use for configuration
+        toHOCON = attr:
+          let
+            attrType = builtins.typeOf attr;
+          in
+          if builtins.elem attrType [ "string" "path" "int" "float" ]
+          then ''"${toString attr}"''
+          else if attrType == "bool"
+          then lib.boolToString attr
+          else if attrType == "list"
+          then "[\n${lib.concatMapStringsSep ",\n" toHOCON attr}\n]"
+          else # attrs, lambda, null
+            throw ''
+              [suwayomi-server]: invalid config value type '${attrType}'.
+            '';
+
+        configFile = pkgs.writeText "server.conf" (lib.pipe cfg.settings [
+          (settings: lib.recursiveUpdate settings {
+            server.basicAuthPasswordFile = null;
+            server.basicAuthPassword =
+              if settings.server.basicAuthEnabled
+              then "$TACHIDESK_SERVER_BASIC_AUTH_PASSWORD"
+              else null;
+          })
+          (flattenConfig "")
+          (lib.filterAttrs (_: x: x != null))
+          (lib.mapAttrsToList (name: value: ''${name} = ${toHOCON value}''))
+          lib.concatLines
+        ]);
+
+      in
+      {
+        description = "A free and open source manga reader server that runs extensions built for Tachiyomi.";
+
+        wantedBy = [ "multi-user.target" ];
+        wants = [ "network-online.target" ];
+        after = [ "network-online.target" ];
+
+        script = ''
+          ${lib.optionalString cfg.settings.server.basicAuthEnabled ''
+            export TACHIDESK_SERVER_BASIC_AUTH_PASSWORD="$(<${cfg.settings.server.basicAuthPasswordFile})"
+          ''}
+          ${lib.getExe pkgs.envsubst} -i ${configFile} -o ${cfg.dataDir}/.local/share/Tachidesk/server.conf
+          ${lib.getExe cfg.package} -Dsuwayomi.tachidesk.config.server.rootDir=${cfg.dataDir}
+        '';
+
+        serviceConfig = {
+          User = cfg.user;
+          Group = cfg.group;
+
+          Type = "simple";
+          Restart = "on-failure";
+
+          StateDirectory = mkIf (cfg.dataDir == "/var/lib/suwayomi-server") "suwayomi-server";
+        };
+      };
+  };
+
+  meta = {
+    maintainers = with lib.maintainers; [ ratcornu ];
+    doc = ./suwayomi-server.md;
+  };
+}
diff --git a/nixos/modules/services/web-apps/wordpress.nix b/nixos/modules/services/web-apps/wordpress.nix
index 002d6683b2ed..2f7306309d69 100644
--- a/nixos/modules/services/web-apps/wordpress.nix
+++ b/nixos/modules/services/web-apps/wordpress.nix
@@ -174,22 +174,22 @@ let
             List of path(s) to respective language(s) which are copied from the 'languages' directory.
           '';
           example = literalExpression ''
-            [(
+            [
               # Let's package the German language.
               # For other languages try to replace language and country code in the download URL with your desired one.
               # Reference https://translate.wordpress.org for available translations and
               # codes.
-              language-de = pkgs.stdenv.mkDerivation {
+              (pkgs.stdenv.mkDerivation {
                 name = "language-de";
                 src = pkgs.fetchurl {
                   url = "https://de.wordpress.org/wordpress-''${pkgs.wordpress.version}-de_DE.tar.gz";
                   # Name is required to invalidate the hash when wordpress is updated
-                  name = "wordpress-''${pkgs.wordpress.version}-language-de"
+                  name = "wordpress-''${pkgs.wordpress.version}-language-de";
                   sha256 = "sha256-dlas0rXTSV4JAl8f/UyMbig57yURRYRhTMtJwF9g8h0=";
                 };
                 installPhase = "mkdir -p $out; cp -r ./wp-content/languages/* $out/";
-              };
-            )];
+              })
+            ];
           '';
         };
 
diff --git a/nixos/modules/services/web-servers/agate.nix b/nixos/modules/services/web-servers/agate.nix
index dce425035ff7..e03174c87945 100644
--- a/nixos/modules/services/web-servers/agate.nix
+++ b/nixos/modules/services/web-servers/agate.nix
@@ -71,6 +71,7 @@ in
     systemd.services.agate = {
       description = "Agate";
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = [ "network.target" "network-online.target" ];
 
       script =
diff --git a/nixos/modules/services/web-servers/mighttpd2.nix b/nixos/modules/services/web-servers/mighttpd2.nix
index bdd6d8b62aa3..bb75dc4f2ff4 100644
--- a/nixos/modules/services/web-servers/mighttpd2.nix
+++ b/nixos/modules/services/web-servers/mighttpd2.nix
@@ -101,6 +101,7 @@ in {
       ];
     systemd.services.mighttpd2 = {
       description = "Mighttpd2 web server";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       serviceConfig = {
diff --git a/nixos/modules/services/web-servers/minio.nix b/nixos/modules/services/web-servers/minio.nix
index 6431db250476..be6946657e23 100644
--- a/nixos/modules/services/web-servers/minio.nix
+++ b/nixos/modules/services/web-servers/minio.nix
@@ -98,6 +98,7 @@ in
 
       services.minio = {
         description = "Minio Object Storage";
+        wants = [ "network-online.target" ];
         after = [ "network-online.target" ];
         wantedBy = [ "multi-user.target" ];
         serviceConfig = {
diff --git a/nixos/modules/services/web-servers/traefik.nix b/nixos/modules/services/web-servers/traefik.nix
index cc2c680b3342..fc9eb504ebf8 100644
--- a/nixos/modules/services/web-servers/traefik.nix
+++ b/nixos/modules/services/web-servers/traefik.nix
@@ -144,6 +144,7 @@ in {
 
     systemd.services.traefik = {
       description = "Traefik web server";
+      wants = [ "network-online.target" ];
       after = [ "network-online.target" ];
       wantedBy = [ "multi-user.target" ];
       startLimitIntervalSec = 86400;
diff --git a/nixos/modules/services/web-servers/ttyd.nix b/nixos/modules/services/web-servers/ttyd.nix
index 3b1d87ccb483..e545869ca432 100644
--- a/nixos/modules/services/web-servers/ttyd.nix
+++ b/nixos/modules/services/web-servers/ttyd.nix
@@ -180,10 +180,11 @@ in
         # Runs login which needs to be run as root
         # login: Cannot possibly work without effective root
         User = "root";
+        LoadCredential = lib.optionalString (cfg.passwordFile != null) "TTYD_PASSWORD_FILE:${cfg.passwordFile}";
       };
 
       script = if cfg.passwordFile != null then ''
-        PASSWORD=$(cat ${escapeShellArg cfg.passwordFile})
+        PASSWORD=$(cat "$CREDENTIALS_DIRECTORY/TTYD_PASSWORD_FILE")
         ${pkgs.ttyd}/bin/ttyd ${lib.escapeShellArgs args} \
           --credential ${escapeShellArg cfg.username}:"$PASSWORD" \
           ${pkgs.shadow}/bin/login
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 6cd46f30373b..055afe95df60 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
@@ -15,6 +15,19 @@ import json
 from typing import NamedTuple, Dict, List
 from dataclasses import dataclass
 
+# These values will be replaced with actual values during the package build
+EFI_SYS_MOUNT_POINT = "@efiSysMountPoint@"
+TIMEOUT = "@timeout@"
+EDITOR = bool("@editor@")
+CONSOLE_MODE = "@consoleMode@"
+BOOTSPEC_TOOLS = "@bootspecTools@"
+DISTRO_NAME = "@distroName@"
+NIX = "@nix@"
+SYSTEMD = "@systemd@"
+CONFIGURATION_LIMIT = int("@configurationLimit@")
+CAN_TOUCH_EFI_VARIABLES = "@canTouchEfiVariables@"
+GRACEFUL = "@graceful@"
+COPY_EXTRA_FILES = "@copyExtraFiles@"
 
 @dataclass
 class BootSpec:
@@ -29,7 +42,6 @@ class BootSpec:
     initrdSecrets: str | None = None
 
 
-
 libc = ctypes.CDLL("libc.so.6")
 
 class SystemIdentifier(NamedTuple):
@@ -75,16 +87,16 @@ def generation_conf_filename(profile: str | None, generation: int, specialisatio
 
 
 def write_loader_conf(profile: str | None, generation: int, specialisation: str | None) -> None:
-    with open("@efiSysMountPoint@/loader/loader.conf.tmp", 'w') as f:
-        if "@timeout@" != "":
-            f.write("timeout @timeout@\n")
+    with open(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", 'w') as f:
+        if TIMEOUT != "":
+            f.write(f"timeout {TIMEOUT}\n")
         f.write("default %s\n" % generation_conf_filename(profile, generation, specialisation))
-        if not @editor@:
+        if not EDITOR:
             f.write("editor 0\n")
-        f.write("console-mode @consoleMode@\n")
+        f.write(f"console-mode {CONSOLE_MODE}\n")
         f.flush()
         os.fsync(f.fileno())
-    os.rename("@efiSysMountPoint@/loader/loader.conf.tmp", "@efiSysMountPoint@/loader/loader.conf")
+    os.rename(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf.tmp", f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf")
 
 
 def get_bootspec(profile: str | None, generation: int) -> BootSpec:
@@ -95,7 +107,7 @@ def get_bootspec(profile: str | None, generation: int) -> BootSpec:
         bootspec_json = json.load(boot_json_f)
     else:
         boot_json_str = subprocess.check_output([
-        "@bootspecTools@/bin/synthesize",
+        f"{BOOTSPEC_TOOLS}/bin/synthesize",
         "--version",
         "1",
         system_directory,
@@ -116,7 +128,7 @@ def copy_from_file(file: str, dry_run: bool = False) -> str:
     store_dir = os.path.basename(os.path.dirname(store_file_path))
     efi_file_path = "/efi/nixos/%s-%s.efi" % (store_dir, suffix)
     if not dry_run:
-        copy_if_not_exists(store_file_path, "@efiSysMountPoint@%s" % (efi_file_path))
+        copy_if_not_exists(store_file_path, f"{EFI_SYS_MOUNT_POINT}%s" % (efi_file_path))
     return efi_file_path
 
 def write_entry(profile: str | None, generation: int, specialisation: str | None,
@@ -126,13 +138,14 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
     kernel = copy_from_file(bootspec.kernel)
     initrd = copy_from_file(bootspec.initrd)
 
-    title = "@distroName@{profile}{specialisation}".format(
+    title = "{name}{profile}{specialisation}".format(
+        name=DISTRO_NAME,
         profile=" [" + profile + "]" if profile else "",
         specialisation=" (%s)" % specialisation if specialisation else "")
 
     try:
         if bootspec.initrdSecrets is not None:
-            subprocess.check_call([bootspec.initrdSecrets, "@efiSysMountPoint@%s" % (initrd)])
+            subprocess.check_call([bootspec.initrdSecrets, f"{EFI_SYS_MOUNT_POINT}%s" % (initrd)])
     except subprocess.CalledProcessError:
         if current:
             print("failed to create initrd secrets!", file=sys.stderr)
@@ -142,7 +155,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
                   f'for "{title} - Configuration {generation}", an older generation', file=sys.stderr)
             print("note: this is normal after having removed "
                   "or renamed a file in `boot.initrd.secrets`", file=sys.stderr)
-    entry_file = "@efiSysMountPoint@/loader/entries/%s" % (
+    entry_file = f"{EFI_SYS_MOUNT_POINT}/loader/entries/%s" % (
         generation_conf_filename(profile, generation, specialisation))
     tmp_path = "%s.tmp" % (entry_file)
     kernel_params = "init=%s " % bootspec.init
@@ -167,7 +180,7 @@ def write_entry(profile: str | None, generation: int, specialisation: str | None
 
 def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
     gen_list = subprocess.check_output([
-        "@nix@/bin/nix-env",
+        f"{NIX}/bin/nix-env",
         "--list-generations",
         "-p",
         "/nix/var/nix/profiles/%s" % ("system-profiles/" + profile if profile else "system"),
@@ -176,7 +189,7 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
     gen_lines = gen_list.split('\n')
     gen_lines.pop()
 
-    configurationLimit = @configurationLimit@
+    configurationLimit = CONFIGURATION_LIMIT
     configurations = [
         SystemIdentifier(
             profile=profile,
@@ -189,14 +202,14 @@ def get_generations(profile: str | None = None) -> list[SystemIdentifier]:
 
 
 def remove_old_entries(gens: list[SystemIdentifier]) -> None:
-    rex_profile = re.compile(r"^@efiSysMountPoint@/loader/entries/nixos-(.*)-generation-.*\.conf$")
-    rex_generation = re.compile(r"^@efiSysMountPoint@/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
+    rex_profile = re.compile(r"^" + re.escape(EFI_SYS_MOUNT_POINT) + "/loader/entries/nixos-(.*)-generation-.*\.conf$")
+    rex_generation = re.compile(r"^" + re.escape(EFI_SYS_MOUNT_POINT) + "/loader/entries/nixos.*-generation-([0-9]+)(-specialisation-.*)?\.conf$")
     known_paths = []
     for gen in gens:
         bootspec = get_bootspec(gen.profile, gen.generation)
         known_paths.append(copy_from_file(bootspec.kernel, True))
         known_paths.append(copy_from_file(bootspec.initrd, True))
-    for path in glob.iglob("@efiSysMountPoint@/loader/entries/nixos*-generation-[1-9]*.conf"):
+    for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/loader/entries/nixos*-generation-[1-9]*.conf"):
         if rex_profile.match(path):
             prof = rex_profile.sub(r"\1", path)
         else:
@@ -207,7 +220,7 @@ def remove_old_entries(gens: list[SystemIdentifier]) -> None:
             continue
         if not (prof, gen_number, None) in gens:
             os.unlink(path)
-    for path in glob.iglob("@efiSysMountPoint@/efi/nixos/*"):
+    for path in glob.iglob(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/*"):
         if not path in known_paths and not os.path.isdir(path):
             os.unlink(path)
 
@@ -230,7 +243,7 @@ def install_bootloader(args: argparse.Namespace) -> None:
         # Since systemd version 232 a machine ID is required and it might not
         # be there on newly installed systems, so let's generate one so that
         # bootctl can find it and we can also pass it to write_entry() later.
-        cmd = ["@systemd@/bin/systemd-machine-id-setup", "--print"]
+        cmd = [f"{SYSTEMD}/bin/systemd-machine-id-setup", "--print"]
         machine_id = subprocess.run(
           cmd, text=True, check=True, stdout=subprocess.PIPE
         ).stdout.rstrip()
@@ -242,22 +255,22 @@ def install_bootloader(args: argparse.Namespace) -> None:
     # flags to pass to bootctl install/update
     bootctl_flags = []
 
-    if "@canTouchEfiVariables@" != "1":
+    if CAN_TOUCH_EFI_VARIABLES != "1":
         bootctl_flags.append("--no-variables")
 
-    if "@graceful@" == "1":
+    if GRACEFUL == "1":
         bootctl_flags.append("--graceful")
 
     if os.getenv("NIXOS_INSTALL_BOOTLOADER") == "1":
         # bootctl uses fopen() with modes "wxe" and fails if the file exists.
-        if os.path.exists("@efiSysMountPoint@/loader/loader.conf"):
-            os.unlink("@efiSysMountPoint@/loader/loader.conf")
+        if os.path.exists(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf"):
+            os.unlink(f"{EFI_SYS_MOUNT_POINT}/loader/loader.conf")
 
-        subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["install"])
+        subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_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", "--esp-path=@efiSysMountPoint@", "status"], universal_newlines=True)
+        available_out = subprocess.check_output([f"{SYSTEMD}/bin/bootctl", "--version"], universal_newlines=True).split()[2]
+        installed_out = subprocess.check_output([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}", "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.]+[^)]*)\)$",
@@ -276,10 +289,10 @@ def install_bootloader(args: argparse.Namespace) -> None:
 
         if installed_version < available_version:
             print("updating systemd-boot from %s to %s" % (installed_version, available_version))
-            subprocess.check_call(["@systemd@/bin/bootctl", "--esp-path=@efiSysMountPoint@"] + bootctl_flags + ["update"])
+            subprocess.check_call([f"{SYSTEMD}/bin/bootctl", f"--esp-path={EFI_SYS_MOUNT_POINT}"] + bootctl_flags + ["update"])
 
-    os.makedirs("@efiSysMountPoint@/efi/nixos", exist_ok=True)
-    os.makedirs("@efiSysMountPoint@/loader/entries", exist_ok=True)
+    os.makedirs(f"{EFI_SYS_MOUNT_POINT}/efi/nixos", exist_ok=True)
+    os.makedirs(f"{EFI_SYS_MOUNT_POINT}/loader/entries", exist_ok=True)
 
     gens = get_generations()
     for profile in get_profiles():
@@ -302,9 +315,9 @@ def install_bootloader(args: argparse.Namespace) -> None:
             else:
                 raise e
 
-    for root, _, files in os.walk('@efiSysMountPoint@/efi/nixos/.extra-files', topdown=False):
-        relative_root = root.removeprefix("@efiSysMountPoint@/efi/nixos/.extra-files").removeprefix("/")
-        actual_root = os.path.join("@efiSysMountPoint@", relative_root)
+    for root, _, files in os.walk(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files", topdown=False):
+        relative_root = root.removeprefix(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files").removeprefix("/")
+        actual_root = os.path.join(f"{EFI_SYS_MOUNT_POINT}", relative_root)
 
         for file in files:
             actual_file = os.path.join(actual_root, file)
@@ -317,14 +330,14 @@ def install_bootloader(args: argparse.Namespace) -> None:
             os.rmdir(actual_root)
         os.rmdir(root)
 
-    os.makedirs("@efiSysMountPoint@/efi/nixos/.extra-files", exist_ok=True)
+    os.makedirs(f"{EFI_SYS_MOUNT_POINT}/efi/nixos/.extra-files", exist_ok=True)
 
-    subprocess.check_call("@copyExtraFiles@")
+    subprocess.check_call(COPY_EXTRA_FILES)
 
 
 def main() -> None:
-    parser = argparse.ArgumentParser(description='Update @distroName@-related systemd-boot files')
-    parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help='The default @distroName@ config to boot')
+    parser = argparse.ArgumentParser(description=f"Update {DISTRO_NAME}-related systemd-boot files")
+    parser.add_argument('default_config', metavar='DEFAULT-CONFIG', help=f"The default {DISTRO_NAME} config to boot")
     args = parser.parse_args()
 
     try:
@@ -334,9 +347,9 @@ def main() -> None:
         # it can leave the system in an unbootable state, when a crash/outage
         # happens shortly after an update. To decrease the likelihood of this
         # event sync the efi filesystem after each update.
-        rc = libc.syncfs(os.open("@efiSysMountPoint@", os.O_RDONLY))
+        rc = libc.syncfs(os.open(f"{EFI_SYS_MOUNT_POINT}", os.O_RDONLY))
         if rc != 0:
-            print("could not sync @efiSysMountPoint@: {}".format(os.strerror(rc)), file=sys.stderr)
+            print(f"could not sync {EFI_SYS_MOUNT_POINT}: {os.strerror(rc)}", file=sys.stderr)
 
 
 if __name__ == '__main__':
diff --git a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
index 9d55c21077d1..3b140726c2d6 100644
--- a/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
+++ b/nixos/modules/system/boot/loader/systemd-boot/systemd-boot.nix
@@ -81,7 +81,11 @@ in {
 
       type = types.bool;
 
-      description = lib.mdDoc "Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager";
+      description = lib.mdDoc ''
+        Whether to enable the systemd-boot (formerly gummiboot) EFI boot manager.
+        For more information about systemd-boot:
+        https://www.freedesktop.org/wiki/Software/systemd/systemd-boot/
+      '';
     };
 
     editor = mkOption {
diff --git a/nixos/modules/system/boot/resolved.nix b/nixos/modules/system/boot/resolved.nix
index 538f71cc0b9a..c42c88163c56 100644
--- a/nixos/modules/system/boot/resolved.nix
+++ b/nixos/modules/system/boot/resolved.nix
@@ -95,6 +95,29 @@ in
       '';
     };
 
+    services.resolved.dnsovertls = mkOption {
+      default = "false";
+      example = "true";
+      type = types.enum [ "true" "opportunistic" "false" ];
+      description = lib.mdDoc ''
+        If set to
+        - `"true"`:
+            all DNS lookups will be encrypted. This requires
+            that the DNS server supports DNS-over-TLS and
+            has a valid certificate. If the hostname was specified
+            via the `address#hostname` format in {option}`services.resolved.domains`
+            then the specified hostname is used to validate its certificate.
+        - `"opportunistic"`:
+            all DNS lookups will attempt to be encrypted, but will fallback
+            to unecrypted requests if the server does not support DNS-over-TLS.
+            Note that this mode does allow for a malicious party to conduct a
+            downgrade attack by immitating the DNS server and pretending to not
+            support encryption.
+        - `"false"`:
+            all DNS lookups are done unencrypted.
+      '';
+    };
+
     services.resolved.extraConfig = mkOption {
       default = "";
       type = types.lines;
@@ -141,6 +164,7 @@ in
           "Domains=${concatStringsSep " " cfg.domains}"}
         LLMNR=${cfg.llmnr}
         DNSSEC=${cfg.dnssec}
+        DNSOverTLS=${cfg.dnsovertls}
         ${config.services.resolved.extraConfig}
       '';
 
diff --git a/nixos/modules/system/boot/systemd.nix b/nixos/modules/system/boot/systemd.nix
index 46c3f66f02dc..331ca5103ba6 100644
--- a/nixos/modules/system/boot/systemd.nix
+++ b/nixos/modules/system/boot/systemd.nix
@@ -451,20 +451,37 @@ in
         cfg.services
     );
 
-    assertions = concatLists (
-      mapAttrsToList
-        (name: service:
-          map (message: {
-            assertion = false;
-            inherit message;
-          }) (concatLists [
-            (optional ((builtins.elem "network-interfaces.target" service.after) || (builtins.elem "network-interfaces.target" service.wants))
-              "Service '${name}.service' is using the deprecated target network-interfaces.target, which no longer exists. Using network.target is recommended instead."
-            )
-          ])
-        )
-        cfg.services
-    );
+    assertions = let
+      mkOneAssert = typeStr: name: def: {
+        assertion = lib.elem "network-online.target" def.after -> lib.elem "network-online.target" (def.wants ++ def.requires ++ def.bindsTo);
+        message = "${name}.${typeStr} is ordered after 'network-online.target' but doesn't depend on it";
+      };
+      mkAsserts = typeStr: lib.mapAttrsToList (mkOneAssert typeStr);
+      mkMountAsserts = typeStr: map (m: mkOneAssert typeStr m.what m);
+    in mkMerge [
+      (concatLists (
+        mapAttrsToList
+          (name: service:
+            map (message: {
+              assertion = false;
+              inherit message;
+            }) (concatLists [
+              (optional ((builtins.elem "network-interfaces.target" service.after) || (builtins.elem "network-interfaces.target" service.wants))
+                "Service '${name}.service' is using the deprecated target network-interfaces.target, which no longer exists. Using network.target is recommended instead."
+              )
+            ])
+          )
+          cfg.services
+      ))
+      (mkAsserts "target" cfg.targets)
+      (mkAsserts "service" cfg.services)
+      (mkAsserts "socket" cfg.sockets)
+      (mkAsserts "timer" cfg.timers)
+      (mkAsserts "path" cfg.paths)
+      (mkMountAsserts "mount" cfg.mounts)
+      (mkMountAsserts "automount" cfg.automounts)
+      (mkAsserts "slice" cfg.slices)
+    ];
 
     system.build.units = cfg.units;
 
@@ -641,7 +658,6 @@ in
     systemd.services.systemd-udev-settle.restartIfChanged = false; # Causes long delays in nixos-rebuild
     systemd.targets.local-fs.unitConfig.X-StopOnReconfiguration = true;
     systemd.targets.remote-fs.unitConfig.X-StopOnReconfiguration = true;
-    systemd.targets.network-online.wantedBy = [ "multi-user.target" ];
     systemd.services.systemd-importd.environment = proxy_env;
     systemd.services.systemd-pstore.wantedBy = [ "sysinit.target" ]; # see #81138
 
diff --git a/nixos/modules/virtualisation/amazon-image.nix b/nixos/modules/virtualisation/amazon-image.nix
index aa44f2642697..f0d9b95f81f6 100644
--- a/nixos/modules/virtualisation/amazon-image.nix
+++ b/nixos/modules/virtualisation/amazon-image.nix
@@ -71,6 +71,7 @@ in
 
     systemd.services.fetch-ec2-metadata = {
       wantedBy = [ "multi-user.target" ];
+      wants = [ "network-online.target" ];
       after = ["network-online.target"];
       path = [ pkgs.curl ];
       script = builtins.readFile ./ec2-metadata-fetcher.sh;
diff --git a/nixos/modules/virtualisation/oci-containers.nix b/nixos/modules/virtualisation/oci-containers.nix
index a4a40346f093..07ed08ab2f84 100644
--- a/nixos/modules/virtualisation/oci-containers.nix
+++ b/nixos/modules/virtualisation/oci-containers.nix
@@ -267,6 +267,7 @@ let
     };
   in {
     wantedBy = [] ++ optional (container.autoStart) "multi-user.target";
+    wants = lib.optional (container.imageFile == null)  "network-online.target";
     after = lib.optionals (cfg.backend == "docker") [ "docker.service" "docker.socket" ]
             # if imageFile is not set, the service needs the network to download the image from the registry
             ++ lib.optionals (container.imageFile == null) [ "network-online.target" ]
diff --git a/nixos/modules/virtualisation/podman/default.nix b/nixos/modules/virtualisation/podman/default.nix
index ec0b713e58b3..47382f9beab0 100644
--- a/nixos/modules/virtualisation/podman/default.nix
+++ b/nixos/modules/virtualisation/podman/default.nix
@@ -150,26 +150,33 @@ in
 
   };
 
-  config = lib.mkIf cfg.enable
-    {
+  config =
+    let
+      networkConfig = ({
+        dns_enabled = false;
+        driver = "bridge";
+        id = "0000000000000000000000000000000000000000000000000000000000000000";
+        internal = false;
+        ipam_options = { driver = "host-local"; };
+        ipv6_enabled = false;
+        name = "podman";
+        network_interface = "podman0";
+        subnets = [{ gateway = "10.88.0.1"; subnet = "10.88.0.0/16"; }];
+      } // cfg.defaultNetwork.settings);
+      inherit (networkConfig) dns_enabled network_interface;
+    in
+    lib.mkIf cfg.enable {
       environment.systemPackages = [ cfg.package ]
         ++ lib.optional cfg.dockerCompat dockerCompat;
 
       # https://github.com/containers/podman/blob/097cc6eb6dd8e598c0e8676d21267b4edb11e144/docs/tutorials/basic_networking.md#default-network
       environment.etc."containers/networks/podman.json" = lib.mkIf (cfg.defaultNetwork.settings != { }) {
-        source = json.generate "podman.json" ({
-          dns_enabled = false;
-          driver = "bridge";
-          id = "0000000000000000000000000000000000000000000000000000000000000000";
-          internal = false;
-          ipam_options = { driver = "host-local"; };
-          ipv6_enabled = false;
-          name = "podman";
-          network_interface = "podman0";
-          subnets = [{ gateway = "10.88.0.1"; subnet = "10.88.0.0/16"; }];
-        } // cfg.defaultNetwork.settings);
+        source = json.generate "podman.json" networkConfig;
       };
 
+      # containers cannot reach aardvark-dns otherwise
+      networking.firewall.interfaces.${network_interface}.allowedUDPPorts = lib.mkIf dns_enabled [ 53 ];
+
       virtualisation.containers = {
         enable = true; # Enable common /etc/containers configuration
         containersConf.settings = {