about summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/release-notes/rl-2009.xml41
-rw-r--r--nixos/modules/config/networking.nix33
-rw-r--r--nixos/modules/module-list.nix2
-rw-r--r--nixos/modules/services/misc/redmine.nix4
-rw-r--r--nixos/modules/services/networking/networkmanager.nix7
-rw-r--r--nixos/modules/services/security/fprintd.nix1
-rw-r--r--nixos/modules/services/security/usbguard.nix23
-rw-r--r--nixos/modules/services/system/earlyoom.nix23
-rw-r--r--nixos/modules/services/torrent/rtorrent.nix209
-rw-r--r--nixos/modules/services/web-apps/engelsystem.nix186
-rw-r--r--nixos/modules/services/x11/desktop-managers/cde.nix17
-rw-r--r--nixos/modules/tasks/network-interfaces.nix16
-rw-r--r--nixos/modules/virtualisation/cri-o.nix75
-rw-r--r--nixos/tests/all-tests.nix2
-rw-r--r--nixos/tests/engelsystem.nix41
-rw-r--r--nixos/tests/hostname.nix66
16 files changed, 702 insertions, 44 deletions
diff --git a/nixos/doc/manual/release-notes/rl-2009.xml b/nixos/doc/manual/release-notes/rl-2009.xml
index 41f9665a3cd5..e17e8ac24d13 100644
--- a/nixos/doc/manual/release-notes/rl-2009.xml
+++ b/nixos/doc/manual/release-notes/rl-2009.xml
@@ -400,6 +400,47 @@ systemd.services.nginx.serviceConfig.ReadWritePaths = [ "/var/www" ];
       The rkt module has been removed, it was archived by upstream.
     </para>
    </listitem>
+   <listitem>
+    <para>
+      The <link xlink:href="https://bazaar.canonical.com">Bazaar</link> VCS is
+      unmaintained and, as consequence of the Python 2 EOL, the packages
+      <literal>bazaar</literal> and <literal>bazaarTools</literal> were
+      removed. Breezy, the backward compatible fork of Bazaar (see the
+      <link xlink:href="https://www.jelmer.uk/breezy-intro.html">announcement</link>),
+      was packaged as <literal>breezy</literal> and can be used instead.
+    </para>
+    <para>
+      Regarding Nixpkgs, <literal>fetchbzr</literal>,
+      <literal>nix-prefetch-bzr</literal> and Bazaar support in Hydra will
+      continue to work through Breezy.
+    </para>
+   </listitem>
+   <listitem>
+     <para>
+       In addition to the hostname, the fully qualified domain name (FQDN),
+       which consists of <literal>${cfg.hostName}</literal> and
+       <literal>${cfg.domain}</literal> is now added to
+       <literal>/etc/hosts</literal>, to allow local FQDN resolution, as used by the
+       <literal>hostname --fqdn</literal> command and other applications that
+       try to determine the FQDN. These new entries take precedence over entries
+       from the DNS which could cause regressions in some very specific setups.
+       Additionally the hostname is now resolved to <literal>127.0.0.2</literal>
+       instead of <literal>127.0.1.1</literal> to be consistent with what
+       <literal>nss-myhostname</literal> (from systemd) returns.
+       The old behaviour can e.g. be restored by using
+       <literal>networking.hosts = lib.mkForce { "127.0.1.1" = [ config.networking.hostName ]; };</literal>.
+     </para>
+   </listitem>
+   <listitem>
+     <para>
+       The hostname (<literal>networking.hostName</literal>) must now be a valid
+       DNS label (see RFC 1035) and as such must not contain the domain part.
+       This means that the hostname must start with a letter, end with a letter
+       or digit, and have as interior characters only letters, digits, and
+       hyphen. The maximum length is 63 characters. Additionally it is
+       recommended to only use lower-case characters.
+     </para>
+   </listitem>
   </itemizedlist>
  </section>
 
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
index 03944de82497..4cb7d81c9972 100644
--- a/nixos/modules/config/networking.nix
+++ b/nixos/modules/config/networking.nix
@@ -8,9 +8,6 @@ let
 
   cfg = config.networking;
 
-  localhostMapped4 = cfg.hosts ? "127.0.0.1" && elem "localhost" cfg.hosts."127.0.0.1";
-  localhostMapped6 = cfg.hosts ? "::1"       && elem "localhost" cfg.hosts."::1";
-
   localhostMultiple = any (elem "localhost") (attrValues (removeAttrs cfg.hosts [ "127.0.0.1" "::1" ]));
 
 in
@@ -147,12 +144,6 @@ in
   config = {
 
     assertions = [{
-      assertion = localhostMapped4;
-      message = ''`networking.hosts` doesn't map "127.0.0.1" to "localhost"'';
-    } {
-      assertion = !cfg.enableIPv6 || localhostMapped6;
-      message = ''`networking.hosts` doesn't map "::1" to "localhost"'';
-    } {
       assertion = !localhostMultiple;
       message = ''
         `networking.hosts` maps "localhost" to something other than "127.0.0.1"
@@ -161,22 +152,34 @@ in
       '';
     }];
 
-    networking.hosts = {
-      "127.0.0.1" = [ "localhost" ];
-    } // optionalAttrs (cfg.hostName != "") {
-      "127.0.1.1" = [ cfg.hostName ];
+    # These entries are required for "hostname -f" and to resolve both the
+    # hostname and FQDN correctly:
+    networking.hosts = let
+      hostnames = # Note: The FQDN (canonical hostname) has to come first:
+        optional (cfg.hostName != "" && cfg.domain != null) "${cfg.hostName}.${cfg.domain}"
+        ++ optional (cfg.hostName != "") cfg.hostName; # Then the hostname (without the domain)
+    in {
+      "127.0.0.2" = hostnames;
     } // optionalAttrs cfg.enableIPv6 {
-      "::1" = [ "localhost" ];
+      "::1" = hostnames;
     };
 
     networking.hostFiles = let
+      # Note: localhostHosts has to appear first in /etc/hosts so that 127.0.0.1
+      # resolves back to "localhost" (as some applications assume) instead of
+      # the FQDN! By default "networking.hosts" also contains entries for the
+      # FQDN so that e.g. "hostname -f" works correctly.
+      localhostHosts = pkgs.writeText "localhost-hosts" ''
+        127.0.0.1 localhost
+        ${optionalString cfg.enableIPv6 "::1 localhost"}
+      '';
       stringHosts =
         let
           oneToString = set: ip: ip + " " + concatStringsSep " " set.${ip} + "\n";
           allToString = set: concatMapStrings (oneToString set) (attrNames set);
         in pkgs.writeText "string-hosts" (allToString (filterAttrs (_: v: v != []) cfg.hosts));
       extraHosts = pkgs.writeText "extra-hosts" cfg.extraHosts;
-    in mkBefore [ stringHosts extraHosts ];
+    in mkBefore [ localhostHosts stringHosts extraHosts ];
 
     environment.etc =
       { # /etc/services: TCP/UDP port assignments.
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index d5285cfabd78..9f9bf3bc5329 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -817,6 +817,7 @@
   ./services/torrent/magnetico.nix
   ./services/torrent/opentracker.nix
   ./services/torrent/peerflix.nix
+  ./services/torrent/rtorrent.nix
   ./services/torrent/transmission.nix
   ./services/ttys/agetty.nix
   ./services/ttys/gpm.nix
@@ -829,6 +830,7 @@
   ./services/web-apps/cryptpad.nix
   ./services/web-apps/documize.nix
   ./services/web-apps/dokuwiki.nix
+  ./services/web-apps/engelsystem.nix
   ./services/web-apps/frab.nix
   ./services/web-apps/gerrit.nix
   ./services/web-apps/gotify-server.nix
diff --git a/nixos/modules/services/misc/redmine.nix b/nixos/modules/services/misc/redmine.nix
index 1febdba0c8f9..0e71cf925692 100644
--- a/nixos/modules/services/misc/redmine.nix
+++ b/nixos/modules/services/misc/redmine.nix
@@ -26,7 +26,7 @@ let
       scm_mercurial_command: ${pkgs.mercurial}/bin/hg
       scm_git_command: ${pkgs.gitAndTools.git}/bin/git
       scm_cvs_command: ${pkgs.cvs}/bin/cvs
-      scm_bazaar_command: ${pkgs.bazaar}/bin/bzr
+      scm_bazaar_command: ${pkgs.breezy}/bin/bzr
       scm_darcs_command: ${pkgs.darcs}/bin/darcs
 
     ${cfg.extraConfig}
@@ -297,7 +297,7 @@ in
       environment.SCHEMA = "${cfg.stateDir}/cache/schema.db";
       path = with pkgs; [
         imagemagick
-        bazaar
+        breezy
         cvs
         darcs
         gitAndTools.git
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 6f24141b33cf..cc789897b29f 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -449,6 +449,13 @@ in {
 
     systemd.services.ModemManager.aliases = [ "dbus-org.freedesktop.ModemManager1.service" ];
 
+    # override unit as recommended by upstream - see https://github.com/NixOS/nixpkgs/issues/88089
+    # TODO: keep an eye on modem-manager releases as this will eventually be added to the upstream unit
+    systemd.services.ModemManager.serviceConfig.ExecStart = [
+      ""
+      "${pkgs.modemmanager}/sbin/ModemManager --filter-policy=STRICT"
+    ];
+
     systemd.services.NetworkManager-dispatcher = {
       wantedBy = [ "network.target" ];
       restartTriggers = [ configFile ];
diff --git a/nixos/modules/services/security/fprintd.nix b/nixos/modules/services/security/fprintd.nix
index 8ece1ca19013..cbac4ef05b8d 100644
--- a/nixos/modules/services/security/fprintd.nix
+++ b/nixos/modules/services/security/fprintd.nix
@@ -29,7 +29,6 @@ in
         type = types.package;
         default = pkgs.fprintd;
         defaultText = "pkgs.fprintd";
-        example = "pkgs.fprintd-thinkpad";
         description = ''
           fprintd package to use.
         '';
diff --git a/nixos/modules/services/security/usbguard.nix b/nixos/modules/services/security/usbguard.nix
index 4ced5acd9bd9..f4118eb87fc7 100644
--- a/nixos/modules/services/security/usbguard.nix
+++ b/nixos/modules/services/security/usbguard.nix
@@ -207,6 +207,29 @@ in {
         Type = "simple";
         ExecStart = ''${cfg.package}/bin/usbguard-daemon -P -k -c ${daemonConfFile}'';
         Restart = "on-failure";
+
+        AmbientCapabilities = "";
+        CapabilityBoundingSet = "CAP_CHOWN CAP_FOWNER";
+        DeviceAllow = "/dev/null rw";
+        DevicePolicy = "strict";
+        IPAddressDeny = "any";
+        LockPersonality = true;
+        MemoryDenyWriteExecute = true;
+        NoNewPrivileges = true;
+        PrivateDevices = true;
+        PrivateTmp = true;
+        ProtectControlGroups = true;
+        ProtectHome = true;
+        ProtectKernelModules = true;
+        ProtectSystem = true;
+        ReadOnlyPaths = "-/";
+        ReadWritePaths = "-/dev/shm -${dirOf cfg.auditFilePath} -/tmp -${dirOf cfg.ruleFile}";
+        RestrictAddressFamilies = "AF_UNIX AF_NETLINK";
+        RestrictNamespaces = true;
+        RestrictRealtime = true;
+        SystemCallArchitectures = "native";
+        SystemCallFilter = "@system-service";
+        UMask = "0077";
       };
     };
   };
diff --git a/nixos/modules/services/system/earlyoom.nix b/nixos/modules/services/system/earlyoom.nix
index 39d1bf274bd2..c6a001d30eeb 100644
--- a/nixos/modules/services/system/earlyoom.nix
+++ b/nixos/modules/services/system/earlyoom.nix
@@ -67,9 +67,19 @@ in
       notificationsCommand = mkOption {
         type = types.nullOr types.str;
         default = null;
-        example = "sudo -u example_user DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send";
         description = ''
-          Command used to send notifications.
+          This option is deprecated and ignored by earlyoom since 1.6.
+          Use <option>services.earlyoom.enableNotifications</option> instead.
+        '';
+      };
+
+      enableNotifications = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Send notifications about killed processes via the system d-bus.
+          To actually see the notifications in your GUI session, you need to have
+          <literal>systembus-notify</literal> running as your user.
 
           See <link xlink:href="https://github.com/rfjakob/earlyoom#notifications">README</link> for details.
         '';
@@ -87,9 +97,13 @@ in
         message = "Both options in conjunction do not make sense"; }
     ];
 
+    warnings = optional (ecfg.notificationsCommand != null)
+      "`services.earlyoom.notificationsCommand` is deprecated and ignored by earlyoom since 1.6.";
+
     systemd.services.earlyoom = {
       description = "Early OOM Daemon for Linux";
       wantedBy = [ "multi-user.target" ];
+      path = optional ecfg.enableNotifications pkgs.dbus;
       serviceConfig = {
         StandardOutput = "null";
         StandardError = "syslog";
@@ -100,10 +114,11 @@ in
           ${optionalString ecfg.useKernelOOMKiller "-k"} \
           ${optionalString ecfg.ignoreOOMScoreAdjust "-i"} \
           ${optionalString ecfg.enableDebugInfo "-d"} \
-          ${optionalString (ecfg.notificationsCommand != null)
-            "-N ${escapeShellArg ecfg.notificationsCommand}"}
+          ${optionalString ecfg.enableNotifications "-n"}
         '';
       };
     };
+
+    environment.systemPackages = optional ecfg.enableNotifications pkgs.systembus-notify;
   };
 }
diff --git a/nixos/modules/services/torrent/rtorrent.nix b/nixos/modules/services/torrent/rtorrent.nix
new file mode 100644
index 000000000000..be57c03b1721
--- /dev/null
+++ b/nixos/modules/services/torrent/rtorrent.nix
@@ -0,0 +1,209 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rtorrent;
+
+in {
+  options.services.rtorrent = {
+    enable = mkEnableOption "rtorrent";
+
+    dataDir = mkOption {
+      type = types.str;
+      default = "/var/lib/rtorrent";
+      description = ''
+        The directory where rtorrent stores its data files.
+      '';
+    };
+
+    downloadDir = mkOption {
+      type = types.str;
+      default = "${cfg.dataDir}/download";
+      description = ''
+        Where to put downloaded files.
+      '';
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        User account under which rtorrent runs.
+      '';
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "rtorrent";
+      description = ''
+        Group under which rtorrent runs.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.rtorrent;
+      defaultText = "pkgs.rtorrent";
+      description = ''
+        The rtorrent package to use.
+      '';
+    };
+
+    port = mkOption {
+      type = types.port;
+      default = 50000;
+      description = ''
+        The rtorrent port.
+      '';
+    };
+
+    openFirewall = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to open the firewall for the port in <option>services.rtorrent.port</option>.
+      '';
+    };
+
+    rpcSocket = mkOption {
+      type = types.str;
+      readOnly = true;
+      default = "/run/rtorrent/rpc.sock";
+      description = ''
+        RPC socket path.
+      '';
+    };
+
+    configText = mkOption {
+      type = types.lines;
+      default = "";
+      description = ''
+        The content of <filename>rtorrent.rc</filename>. The <link xlink:href="https://rtorrent-docs.readthedocs.io/en/latest/cookbook.html#modernized-configuration-template">modernized configuration template</link> with the values specified in this module will be prepended using mkBefore. You can use mkForce to overwrite the config completly.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    users.groups = mkIf (cfg.group == "rtorrent") {
+      rtorrent = {};
+    };
+
+    users.users = mkIf (cfg.user == "rtorrent") {
+      rtorrent = {
+        group = cfg.group;
+        shell = pkgs.bashInteractive;
+        home = cfg.dataDir;
+        description = "rtorrent Daemon user";
+        isSystemUser = true;
+      };
+    };
+
+    networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall) [ cfg.port ];
+
+    services.rtorrent.configText = mkBefore ''
+      # Instance layout (base paths)
+      method.insert = cfg.basedir, private|const|string, (cat,"${cfg.dataDir}/")
+      method.insert = cfg.watch,   private|const|string, (cat,(cfg.basedir),"watch/")
+      method.insert = cfg.logs,    private|const|string, (cat,(cfg.basedir),"log/")
+      method.insert = cfg.logfile, private|const|string, (cat,(cfg.logs),(system.time),".log")
+      method.insert = cfg.rpcsock, private|const|string, (cat,"${cfg.rpcSocket}")
+
+      # Create instance directories
+      execute.throw = sh, -c, (cat, "mkdir -p ", (cfg.basedir), "/session ", (cfg.watch), " ", (cfg.logs))
+
+      # Listening port for incoming peer traffic (fixed; you can also randomize it)
+      network.port_range.set = ${toString cfg.port}-${toString cfg.port}
+      network.port_random.set = no
+
+      # Tracker-less torrent and UDP tracker support
+      # (conservative settings for 'private' trackers, change for 'public')
+      dht.mode.set = disable
+      protocol.pex.set = no
+      trackers.use_udp.set = no
+
+      # Peer settings
+      throttle.max_uploads.set = 100
+      throttle.max_uploads.global.set = 250
+
+      throttle.min_peers.normal.set = 20
+      throttle.max_peers.normal.set = 60
+      throttle.min_peers.seed.set = 30
+      throttle.max_peers.seed.set = 80
+      trackers.numwant.set = 80
+
+      protocol.encryption.set = allow_incoming,try_outgoing,enable_retry
+
+      # Limits for file handle resources, this is optimized for
+      # an `ulimit` of 1024 (a common default). You MUST leave
+      # a ceiling of handles reserved for rTorrent's internal needs!
+      network.http.max_open.set = 50
+      network.max_open_files.set = 600
+      network.max_open_sockets.set = 3000
+
+      # Memory resource usage (increase if you have a large number of items loaded,
+      # and/or the available resources to spend)
+      pieces.memory.max.set = 1800M
+      network.xmlrpc.size_limit.set = 4M
+
+      # Basic operational settings (no need to change these)
+      session.path.set = (cat, (cfg.basedir), "session/")
+      directory.default.set = "${cfg.downloadDir}"
+      log.execute = (cat, (cfg.logs), "execute.log")
+      ##log.xmlrpc = (cat, (cfg.logs), "xmlrpc.log")
+      execute.nothrow = sh, -c, (cat, "echo >", (session.path), "rtorrent.pid", " ", (system.pid))
+
+      # Other operational settings (check & adapt)
+      encoding.add = utf8
+      system.umask.set = 0027
+      system.cwd.set = (cfg.basedir)
+      network.http.dns_cache_timeout.set = 25
+      schedule2 = monitor_diskspace, 15, 60, ((close_low_diskspace, 1000M))
+
+      # Watch directories (add more as you like, but use unique schedule names)
+      #schedule2 = watch_start, 10, 10, ((load.start, (cat, (cfg.watch), "start/*.torrent")))
+      #schedule2 = watch_load, 11, 10, ((load.normal, (cat, (cfg.watch), "load/*.torrent")))
+
+      # Logging:
+      #   Levels = critical error warn notice info debug
+      #   Groups = connection_* dht_* peer_* rpc_* storage_* thread_* tracker_* torrent_*
+      print = (cat, "Logging to ", (cfg.logfile))
+      log.open_file = "log", (cfg.logfile)
+      log.add_output = "info", "log"
+      ##log.add_output = "tracker_debug", "log"
+
+      # XMLRPC
+      scgi_local = (cfg.rpcsock)
+      schedule = scgi_group,0,0,"execute.nothrow=chown,\":rtorrent\",(cfg.rpcsock)"
+      schedule = scgi_permission,0,0,"execute.nothrow=chmod,\"g+w,o=\",(cfg.rpcsock)"
+    '';
+
+    systemd = {
+      services = {
+        rtorrent = let
+          rtorrentConfigFile = pkgs.writeText "rtorrent.rc" cfg.configText;
+        in {
+          description = "rTorrent system service";
+          after = [ "network.target" ];
+          path = [ cfg.package pkgs.bash ];
+          wantedBy = [ "multi-user.target" ];
+          serviceConfig = {
+            User = cfg.user;
+            Group = cfg.group;
+            Type = "simple";
+            Restart = "on-failure";
+            WorkingDirectory = cfg.dataDir;
+            ExecStartPre=''${pkgs.bash}/bin/bash -c "if test -e ${cfg.dataDir}/session/rtorrent.lock && test -z $(${pkgs.procps}/bin/pidof rtorrent); then rm -f ${cfg.dataDir}/session/rtorrent.lock; fi"'';
+            ExecStart="${cfg.package}/bin/rtorrent -n -o system.daemon.set=true -o import=${rtorrentConfigFile}";
+            RuntimeDirectory = "rtorrent";
+            RuntimeDirectoryMode = 755;
+          };
+        };
+      };
+
+      tmpfiles.rules = [ "d '${cfg.dataDir}' 0750 ${cfg.user} ${cfg.group} -" ];
+    };
+  };
+}
diff --git a/nixos/modules/services/web-apps/engelsystem.nix b/nixos/modules/services/web-apps/engelsystem.nix
new file mode 100644
index 000000000000..899582a20304
--- /dev/null
+++ b/nixos/modules/services/web-apps/engelsystem.nix
@@ -0,0 +1,186 @@
+{ config, lib, pkgs, utils, ... }:
+
+let
+  inherit (lib) mkDefault mkEnableOption mkIf mkOption types literalExample;
+  cfg = config.services.engelsystem;
+in {
+  options = {
+    services.engelsystem = {
+      enable = mkOption {
+        default = false;
+        example = true;
+        description = ''
+          Whether to enable engelsystem, an online tool for coordinating helpers
+          and shifts on large events.
+        '';
+        type = lib.types.bool;
+      };
+
+      domain = mkOption {
+        type = types.str;
+        example = "engelsystem.example.com";
+        description = "Domain to serve on.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        example = literalExample "pkgs.engelsystem";
+        description = "Engelsystem package used for the service.";
+        default = pkgs.engelsystem;
+      };
+
+      createDatabase = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to create a local database automatically.
+          This will override every database setting in <option>services.engelsystem.config</option>.
+        '';
+      };
+    };
+
+    services.engelsystem.config = mkOption {
+      type = types.attrs;
+      default = {
+        database = {
+          host = "localhost";
+          database = "engelsystem";
+          username = "engelsystem";
+        };
+      };
+      example = {
+        maintenance = false;
+        database = {
+          host = "database.example.com";
+          database = "engelsystem";
+          username = "engelsystem";
+          password._secret = "/var/keys/engelsystem/database";
+        };
+        email = {
+          driver = "smtp";
+          host = "smtp.example.com";
+          port = 587;
+          from.address = "engelsystem@example.com";
+          from.name = "example engelsystem";
+          encryption = "tls";
+          username = "engelsystem@example.com";
+          password._secret = "/var/keys/engelsystem/mail";
+        };
+        autoarrive = true;
+        min_password_length = 6;
+        default_locale = "de_DE";
+      };
+      description = ''
+        Options to be added to config.php, as a nix attribute set. Options containing secret data
+        should be set to an attribute set containing the attribute _secret - a string pointing to a
+        file containing the value the option should be set to. See the example to get a better
+        picture of this: in the resulting config.php file, the email.password key will be set to
+        the contents of the /var/keys/engelsystem/mail file.
+
+        See https://engelsystem.de/doc/admin/configuration/ for available options.
+
+        Note that the admin user login credentials cannot be set here - they always default to
+        admin:asdfasdf. Log in and change them immediately.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # create database
+    services.mysql = mkIf cfg.createDatabase {
+      enable = true;
+      package = mkDefault pkgs.mysql;
+      ensureUsers = [{
+        name = "engelsystem";
+        ensurePermissions = { "engelsystem.*" = "ALL PRIVILEGES"; };
+      }];
+      ensureDatabases = [ "engelsystem" ];
+    };
+
+    environment.etc."engelsystem/config.php".source =
+      pkgs.writeText "config.php" ''
+        <?php
+        return json_decode(file_get_contents("/var/lib/engelsystem/config.json"), true);
+      '';
+
+    services.phpfpm.pools.engelsystem = {
+      user = "engelsystem";
+      settings = {
+        "listen.owner" = config.services.nginx.user;
+        "pm" = "dynamic";
+        "pm.max_children" = 32;
+        "pm.max_requests" = 500;
+        "pm.start_servers" = 2;
+        "pm.min_spare_servers" = 2;
+        "pm.max_spare_servers" = 5;
+        "php_admin_value[error_log]" = "stderr";
+        "php_admin_flag[log_errors]" = true;
+        "catch_workers_output" = true;
+      };
+    };
+
+    services.nginx = {
+      enable = true;
+      virtualHosts."${cfg.domain}".locations = {
+        "/" = {
+          root = "${cfg.package}/share/engelsystem/public";
+          extraConfig = ''
+            index index.php;
+            try_files $uri $uri/ /index.php?$args;
+            autoindex off;
+          '';
+        };
+        "~ \\.php$" = {
+          root = "${cfg.package}/share/engelsystem/public";
+          extraConfig = ''
+            fastcgi_pass unix:${config.services.phpfpm.pools.engelsystem.socket};
+            fastcgi_index index.php;
+            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+            include ${config.services.nginx.package}/conf/fastcgi_params;
+            include ${config.services.nginx.package}/conf/fastcgi.conf;
+          '';
+        };
+      };
+    };
+
+    systemd.services."engelsystem-init" = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = { Type = "oneshot"; };
+      script =
+        let
+          genConfigScript = pkgs.writeScript "engelsystem-gen-config.sh"
+            (utils.genJqSecretsReplacementSnippet cfg.config "config.json");
+        in ''
+          umask 077
+          mkdir -p /var/lib/engelsystem/storage/app
+          mkdir -p /var/lib/engelsystem/storage/cache/views
+          cd /var/lib/engelsystem
+          ${genConfigScript}
+          chmod 400 config.json
+          chown -R engelsystem .
+      '';
+    };
+    systemd.services."engelsystem-migrate" = {
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "oneshot";
+        User = "engelsystem";
+        Group = "engelsystem";
+      };
+      script = ''
+        ${cfg.package}/bin/migrate
+      '';
+      after = [ "engelsystem-init.service" "mysql.service" ];
+    };
+    systemd.services."phpfpm-engelsystem".after =
+      [ "engelsystem-migrate.service" ];
+
+    users.users.engelsystem = {
+      isSystemUser = true;
+      createHome = true;
+      home = "/var/lib/engelsystem/storage";
+      group = "engelsystem";
+    };
+    users.groups.engelsystem = { };
+  };
+}
diff --git a/nixos/modules/services/x11/desktop-managers/cde.nix b/nixos/modules/services/x11/desktop-managers/cde.nix
index c1b6d3bf064a..2d9504fb5f1e 100644
--- a/nixos/modules/services/x11/desktop-managers/cde.nix
+++ b/nixos/modules/services/x11/desktop-managers/cde.nix
@@ -8,9 +8,26 @@ let
 in {
   options.services.xserver.desktopManager.cde = {
     enable = mkEnableOption "Common Desktop Environment";
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = with pkgs.xorg; [
+        xclock bitmap xlsfonts xfd xrefresh xload xwininfo xdpyinfo xwd xwud
+      ];
+      example = literalExample ''
+        with pkgs.xorg; [
+          xclock bitmap xlsfonts xfd xrefresh xload xwininfo xdpyinfo xwd xwud
+        ]
+      '';
+      description = ''
+        Extra packages to be installed system wide.
+      '';
+    };
   };
 
   config = mkIf (xcfg.enable && cfg.enable) {
+    environment.systemPackages = cfg.extraPackages;
+
     services.rpcbind.enable = true;
 
     services.xinetd.enable = true;
diff --git a/nixos/modules/tasks/network-interfaces.nix b/nixos/modules/tasks/network-interfaces.nix
index 44677d417ead..12cff6b038f8 100644
--- a/nixos/modules/tasks/network-interfaces.nix
+++ b/nixos/modules/tasks/network-interfaces.nix
@@ -376,10 +376,20 @@ in
 
     networking.hostName = mkOption {
       default = "nixos";
-      type = types.str;
+      # Only allow hostnames without the domain name part (i.e. no FQDNs, see
+      # e.g. "man 5 hostname") and require valid DNS labels (recommended
+      # syntax). Note: We also allow underscores for compatibility/legacy
+      # reasons (as undocumented feature):
+      type = types.strMatching
+        "^[[:alpha:]]([[:alnum:]_-]{0,61}[[:alnum:]])?$";
       description = ''
-        The name of the machine.  Leave it empty if you want to obtain
-        it from a DHCP server (if using DHCP).
+        The name of the machine. Leave it empty if you want to obtain it from a
+        DHCP server (if using DHCP). The hostname must be a valid DNS label (see
+        RFC 1035 section 2.3.1: "Preferred name syntax") and as such must not
+        contain the domain part. This means that the hostname must start with a
+        letter, end with a letter or digit, and have as interior characters only
+        letters, digits, and hyphen. The maximum length is 63 characters.
+        Additionally it is recommended to only use lower-case characters.
       '';
     };
 
diff --git a/nixos/modules/virtualisation/cri-o.nix b/nixos/modules/virtualisation/cri-o.nix
index 2af4214302d6..f267c97b1788 100644
--- a/nixos/modules/virtualisation/cri-o.nix
+++ b/nixos/modules/virtualisation/cri-o.nix
@@ -5,6 +5,8 @@ with lib;
 let
   cfg = config.virtualisation.cri-o;
 
+  crioPackage = (pkgs.cri-o.override { inherit (cfg) extraPackages; });
+
   # Copy configuration files to avoid having the entire sources in the system closure
   copyFile = filePath: pkgs.runCommandNoCC (builtins.unsafeDiscardStringContext (builtins.baseNameOf filePath)) {} ''
     cp ${filePath} $out
@@ -23,55 +25,90 @@ in
     enable = mkEnableOption "Container Runtime Interface for OCI (CRI-O)";
 
     storageDriver = mkOption {
-      type = types.enum ["btrfs" "overlay" "vfs"];
+      type = types.enum [ "btrfs" "overlay" "vfs" ];
       default = "overlay";
       description = "Storage driver to be used";
     };
 
     logLevel = mkOption {
-      type = types.enum ["trace" "debug" "info" "warn" "error" "fatal"];
+      type = types.enum [ "trace" "debug" "info" "warn" "error" "fatal" ];
       default = "info";
       description = "Log level to be used";
     };
 
     pauseImage = mkOption {
-      type = types.str;
-      default = "k8s.gcr.io/pause:3.1";
-      description = "Pause image for pod sandboxes to be used";
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default pause image for pod sandboxes";
+      example = [ "k8s.gcr.io/pause:3.2" ];
     };
 
     pauseCommand = mkOption {
-      type = types.str;
-      default = "/pause";
-      description = "Pause command to be executed";
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default pause command";
+      example = [ "/pause" ];
+    };
+
+    runtime = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Override the default runtime";
+      example = [ "crun" ];
+    };
+
+    extraPackages = mkOption {
+      type = with types; listOf package;
+      default = [ ];
+      example = lib.literalExample ''
+        [
+          pkgs.gvisor
+        ]
+      '';
+      description = ''
+        Extra packages to be installed in the CRI-O wrapper.
+      '';
+    };
+
+    package = lib.mkOption {
+      type = types.package;
+      default = crioPackage;
+      internal = true;
+      description = ''
+        The final CRI-O package (including extra packages).
+      '';
     };
   };
 
   config = mkIf cfg.enable {
-    environment.systemPackages = with pkgs;
-      [ cri-o cri-tools conmon iptables runc utillinux ];
+    environment.systemPackages = [ cfg.package pkgs.cri-tools ];
 
-    environment.etc."crictl.yaml".source = copyFile "${pkgs.cri-o.src}/crictl.yaml";
+    environment.etc."crictl.yaml".source = copyFile "${pkgs.cri-o-unwrapped.src}/crictl.yaml";
 
     environment.etc."crio/crio.conf".text = ''
       [crio]
       storage_driver = "${cfg.storageDriver}"
 
       [crio.image]
-      pause_image = "${cfg.pauseImage}"
-      pause_command = "${cfg.pauseCommand}"
+      ${optionalString (cfg.pauseImage != null) ''pause_image = "${cfg.pauseImage}"''}
+      ${optionalString (cfg.pauseCommand != null) ''pause_command = "${cfg.pauseCommand}"''}
 
       [crio.network]
       plugin_dirs = ["${pkgs.cni-plugins}/bin/"]
-      network_dir = "/etc/cni/net.d/"
 
       [crio.runtime]
-      conmon = "${pkgs.conmon}/bin/conmon"
+      cgroup_manager = "systemd"
       log_level = "${cfg.logLevel}"
-      manage_network_ns_lifecycle = true
+      manage_ns_lifecycle = true
+
+      ${optionalString (cfg.runtime != null) ''
+      default_runtime = "${cfg.runtime}"
+      [crio.runtime.runtimes]
+      [crio.runtime.runtimes.${cfg.runtime}]
+      ''}
     '';
 
-    environment.etc."cni/net.d/10-crio-bridge.conf".source = copyFile "${pkgs.cri-o.src}/contrib/cni/10-crio-bridge.conf";
+    environment.etc."cni/net.d/10-crio-bridge.conf".source = copyFile "${pkgs.cri-o-unwrapped.src}/contrib/cni/10-crio-bridge.conf";
 
     # Enable common /etc/containers configuration
     virtualisation.containers.enable = true;
@@ -81,10 +118,10 @@ in
       documentation = [ "https://github.com/cri-o/cri-o" ];
       wantedBy = [ "multi-user.target" ];
       after = [ "network.target" ];
-      path = [ pkgs.utillinux pkgs.runc pkgs.iptables ];
+      path = [ cfg.package ];
       serviceConfig = {
         Type = "notify";
-        ExecStart = "${pkgs.cri-o}/bin/crio";
+        ExecStart = "${cfg.package}/bin/crio";
         ExecReload = "/bin/kill -s HUP $MAINPID";
         TasksMax = "infinity";
         LimitNOFILE = "1048576";
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index af619ac99a32..47ca015632c7 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -87,6 +87,7 @@ in
   ecryptfs = handleTest ./ecryptfs.nix {};
   ejabberd = handleTest ./xmpp/ejabberd.nix {};
   elk = handleTestOn ["x86_64-linux"] ./elk.nix {};
+  engelsystem = handleTest ./engelsystem.nix {};
   enlightenment = handleTest ./enlightenment.nix {};
   env = handleTest ./env.nix {};
   etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
@@ -133,6 +134,7 @@ in
   hitch = handleTest ./hitch {};
   hocker-fetchdocker = handleTest ./hocker-fetchdocker {};
   home-assistant = handleTest ./home-assistant.nix {};
+  hostname = handleTest ./hostname.nix {};
   hound = handleTest ./hound.nix {};
   hydra = handleTest ./hydra {};
   hydra-db-migration = handleTest ./hydra/db-migration.nix {};
diff --git a/nixos/tests/engelsystem.nix b/nixos/tests/engelsystem.nix
new file mode 100644
index 000000000000..39c10718093f
--- /dev/null
+++ b/nixos/tests/engelsystem.nix
@@ -0,0 +1,41 @@
+import ./make-test-python.nix (
+  { pkgs, lib, ... }:
+  {
+    name = "engelsystem";
+    meta = with pkgs.stdenv.lib.maintainers; {
+      maintainers = [ talyz ];
+    };
+
+    nodes.engelsystem =
+      { ... }:
+      {
+        services.engelsystem = {
+          enable = true;
+          domain = "engelsystem";
+          createDatabase = true;
+        };
+        networking.firewall.allowedTCPPorts = [ 80 443 ];
+        environment.systemPackages = with pkgs; [
+          xmlstarlet
+          libxml2
+        ];
+      };
+
+    testScript = ''
+      engelsystem.start()
+      engelsystem.wait_for_unit("phpfpm-engelsystem.service")
+      engelsystem.wait_until_succeeds("curl engelsystem/login -sS -f")
+      engelsystem.succeed(
+          "curl engelsystem/login -sS -f -c cookie | xmllint -html -xmlout - >login"
+      )
+      engelsystem.succeed(
+          "xml sel -T -t -m \"html/head/meta[@name='csrf-token']\" -v @content login >token"
+      )
+      engelsystem.succeed(
+          "curl engelsystem/login -sS -f -b cookie -F 'login=admin' -F 'password=asdfasdf' -F '_token=<token' -L | xmllint -html -xmlout - >news"
+      )
+      engelsystem.succeed(
+          "test 'News - Engelsystem' = \"$(xml sel -T -t -c html/head/title news)\""
+      )
+    '';
+  })
diff --git a/nixos/tests/hostname.nix b/nixos/tests/hostname.nix
new file mode 100644
index 000000000000..3b87303d73e7
--- /dev/null
+++ b/nixos/tests/hostname.nix
@@ -0,0 +1,66 @@
+{ system ? builtins.currentSystem,
+  config ? {},
+  pkgs ? import ../.. { inherit system config; }
+}:
+
+with import ../lib/testing-python.nix { inherit system pkgs; };
+with pkgs.lib;
+
+let
+  makeHostNameTest = hostName: domain:
+    let
+      fqdn = hostName + (optionalString (domain != null) ".${domain}");
+    in
+      makeTest {
+        name = "hostname-${fqdn}";
+        meta = with pkgs.stdenv.lib.maintainers; {
+          maintainers = [ primeos blitz ];
+        };
+
+        machine = { lib, ... }: {
+          networking.hostName = hostName;
+          networking.domain = domain;
+
+          environment.systemPackages = with pkgs; [
+            inetutils
+          ];
+        };
+
+        testScript = ''
+          start_all()
+
+          machine = ${hostName}
+
+          machine.wait_for_unit("network-online.target")
+
+          # The FQDN, domain name, and hostname detection should work as expected:
+          assert "${fqdn}" == machine.succeed("hostname --fqdn").strip()
+          assert "${optionalString (domain != null) domain}" == machine.succeed("dnsdomainname").strip()
+          assert (
+              "${hostName}"
+              == machine.succeed(
+                  'hostnamectl status | grep "Static hostname" | cut -d: -f2'
+              ).strip()
+          )
+
+          # 127.0.0.1 and ::1 should resolve back to "localhost":
+          assert (
+              "localhost" == machine.succeed("getent hosts 127.0.0.1 | awk '{print $2}'").strip()
+          )
+          assert "localhost" == machine.succeed("getent hosts ::1 | awk '{print $2}'").strip()
+
+          # 127.0.0.2 should resolve back to the FQDN and hostname:
+          fqdn_and_host_name = "${optionalString (domain != null) "${hostName}.${domain} "}${hostName}"
+          assert (
+              fqdn_and_host_name
+              == machine.succeed("getent hosts 127.0.0.2 | awk '{print $2,$3}'").strip()
+          )
+        '';
+      };
+
+in
+{
+  noExplicitDomain = makeHostNameTest "ahost" null;
+
+  explicitDomain = makeHostNameTest "ahost" "adomain";
+}