about summary refs log tree commit diff
path: root/modules/server
diff options
context:
space:
mode:
Diffstat (limited to 'modules/server')
-rw-r--r--modules/server/acme/default.nix8
-rw-r--r--modules/server/bitfolk/default.nix15
-rw-r--r--modules/server/cgit/default.nix133
-rw-r--r--modules/server/default.nix13
-rw-r--r--modules/server/dns/default.nix7
-rw-r--r--modules/server/ftp/default.nix49
-rw-r--r--modules/server/git-http-backend/default.nix106
-rw-r--r--modules/server/git/default.nix73
-rw-r--r--modules/server/git/nixpkgs/default.nix58
-rw-r--r--modules/server/irc/default.nix5
-rw-r--r--modules/server/irc/soju/default.nix47
-rw-r--r--modules/server/irc/znc/default.nix31
-rw-r--r--modules/server/mail/default.nix79
-rw-r--r--modules/server/mail/public-inbox/default.nix62
-rw-r--r--modules/server/nginx/default.nix22
-rw-r--r--modules/server/nixpk.gs/acme/default.nix7
-rw-r--r--modules/server/nixpk.gs/default.nix5
-rw-r--r--modules/server/nixpk.gs/nginx/default.nix13
-rw-r--r--modules/server/nixpk.gs/nginx/index.html12
-rw-r--r--modules/server/nixpk.gs/pr-tracker/default.nix28
-rw-r--r--modules/server/spectrum/acme/default.nix7
-rw-r--r--modules/server/spectrum/cgit/default.nix57
-rw-r--r--modules/server/spectrum/default.nix14
-rw-r--r--modules/server/spectrum/git-http-backend/default.nix11
-rw-r--r--modules/server/spectrum/git/default.nix110
-rw-r--r--modules/server/spectrum/nginx/default.nix41
-rw-r--r--modules/server/spectrum/nginx/robots.txt5
-rw-r--r--modules/server/spectrum/patch-refs/default.nix46
-rw-r--r--modules/server/spectrum/patch-refs/mda.elb36
-rw-r--r--modules/server/spectrum/postfix/default.nix71
-rw-r--r--modules/server/spectrum/public-inbox/default.nix70
-rw-r--r--modules/server/spectrum/spectrumbot/default.nix5
-rw-r--r--modules/server/spectrum/spectrumbot/irccat/default.nix53
-rw-r--r--modules/server/spectrum/spectrumbot/postfix/default.nix39
-rw-r--r--modules/server/spectrum/spectrumbot/postfix/mda.elb26
-rw-r--r--modules/server/spectrum/vultr-mon/default.nix24
-rw-r--r--modules/server/tor/default.nix19
-rw-r--r--modules/server/xmpp/default.nix30
38 files changed, 1437 insertions, 0 deletions
diff --git a/modules/server/acme/default.nix b/modules/server/acme/default.nix
new file mode 100644
index 000000000000..38f92c865ea2
--- /dev/null
+++ b/modules/server/acme/default.nix
@@ -0,0 +1,8 @@
+{ ... }:
+
+{
+  security.acme.acceptTerms = true;
+
+  # TODO: email to root?
+  security.acme.defaults.email = "hi@alyssa.is";
+}
diff --git a/modules/server/bitfolk/default.nix b/modules/server/bitfolk/default.nix
new file mode 100644
index 000000000000..d5b54109fa05
--- /dev/null
+++ b/modules/server/bitfolk/default.nix
@@ -0,0 +1,15 @@
+{ ... }:
+
+{
+  boot.initrd.availableKernelModules = [ "xen_blkfront" ];
+
+  systemd.enableEmergencyMode = false;
+
+  # Bitfolk provides its own GRUB, so we need to give it a grub.cfg
+  # but don't need to actually install GRUB anywhere.
+  boot.loader.grub.enable = true;
+  boot.loader.grub.device = "nodev";
+
+  networking.dhcpcd.enable = false;
+}
+
diff --git a/modules/server/cgit/default.nix b/modules/server/cgit/default.nix
new file mode 100644
index 000000000000..aba0d1b54c5d
--- /dev/null
+++ b/modules/server/cgit/default.nix
@@ -0,0 +1,133 @@
+{ lib, pkgs, config, ... }:
+
+let
+  inherit (builtins) split;
+  inherit (lib) flip foldr groupBy head literalExpression mapAttrs
+    mapAttrs' mapAttrsToList mdDoc mkOption nameValuePair optionalAttrs types;
+
+  cfg = config.services.cgit-qyliss;
+
+  instancesByVhost = groupBy ({ value, ... }: value.vhost)
+    (mapAttrsToList nameValuePair cfg.instances);
+
+  vhostConfigs = mapAttrs (vhost: instances:
+    foldr (l: r: l // r) {} (map ({ name, value }: let
+      unslashedPath = head (split "/+$" value.path);
+      # We'll be dealing almost exclusively with paths ending in /,
+      # since otherwise Nginx likes to do simple prefix matching.
+      path = "${unslashedPath}/";
+    in {
+      locations = {
+        ${path} = {
+          alias = "${value.package}/cgit/";
+          tryFiles = "$uri @${name}-cgit";
+        };
+        "@${name}-cgit" = {
+          proxyPass = "http://unix:/run/cgit/${name}.sock";
+        };
+      } // optionalAttrs (unslashedPath != "") {
+        ${unslashedPath} = {
+          return = "301 ${path}";
+        };
+      };
+
+      extraConfig = ''
+        if ($http_user_agent = "my-tiny-bot") {
+          return 429;
+        }
+        if ($http_user_agent = "thesis-research-bot") {
+          return 429;
+        }
+      '';
+    }) instances)
+  ) instancesByVhost;
+in
+
+{
+  options.services.cgit-qyliss = {
+    instances = mkOption {
+      type = types.attrsOf (types.submodule {
+        options = {
+          vhost = mkOption {
+            type = types.str;
+            example = "spectrum-os.org";
+            description = mdDoc "Nginx vhost for the cgit";
+          };
+
+          path = mkOption {
+            type = types.strMatching "/(.*[^/])?";
+            default = "/";
+            example = "/git";
+            description = mdDoc ''
+              Path to be appended to all cgit URLs.
+
+              Leading slashes are mandatory; trailing slashes are forbidden.
+            '';
+          };
+
+          package = mkOption {
+            type = types.package;
+            default = pkgs.cgit;
+            defaultText = literalExpression "pkgs.cgit";
+            description = mdDoc "cgit package to use";
+          };
+
+          config = mkOption {
+            type = types.package;
+            description = mdDoc ''
+              Configuration file for cgit.  See
+              <citerefentry><refentrytitle>cgitrc</refentrytitle>
+              <manvolnum>5</manvolnum></citerefentry>.
+            '';
+          };
+        };
+      });
+      default = {};
+      description = mdDoc "List of cgit instances to run";
+    };
+  };
+
+  config = {
+    services.nginx.virtualHosts = vhostConfigs;
+
+    systemd.services = flip mapAttrs' cfg.instances (name: instance: {
+      name = "lighttpd-${name}@";
+      value = {
+        unitConfig.CollectMode = "inactive-or-failed";
+        serviceConfig.StandardInput = "socket";
+        serviceConfig.StandardOutput = "socket";
+        serviceConfig.StandardError = "journal";
+        serviceConfig.DynamicUser = true;
+        serviceConfig.Type = "oneshot";
+        serviceConfig.TimeoutSec = "30";
+        serviceConfig.ExecStart = "${lib.getExe pkgs.lighttpd} -1 -f ${pkgs.writeText "lighttpd-${name}.conf" ''
+          server.modules = ( "mod_alias", "mod_setenv", "mod_cgi" )
+
+          server.document-root = "/var/empty"
+
+          alias.url = (
+            "${if instance.path == "/" then "" else instance.path}" =>
+              "${instance.package}/cgit/cgit.cgi"
+          )
+
+          cgi.assign = (
+            "cgit.cgi" => "${instance.package}/cgit/cgit.cgi"
+          )
+
+          setenv.add-environment = (
+            "CGIT_CONFIG" => "${instance.config}"
+          )
+        ''}";
+      };
+    });
+
+    systemd.sockets = flip mapAttrs' cfg.instances (name: instance: {
+      name = "lighttpd-${name}";
+      value = {
+        wantedBy = [ "sockets.target" ];
+        socketConfig.ListenStream = "/run/cgit/${name}.sock";
+        socketConfig.Accept = "yes";
+      };
+    });
+  };
+}
diff --git a/modules/server/default.nix b/modules/server/default.nix
new file mode 100644
index 000000000000..f59ea9662667
--- /dev/null
+++ b/modules/server/default.nix
@@ -0,0 +1,13 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ../nix ../ssh ../users ];
+
+  security.sudo.wheelNeedsPassword = false;
+
+  networking.firewall.logRefusedConnections = false;
+
+  i18n.defaultLocale = "C.UTF-8";
+
+  environment.systemPackages = with pkgs; [ htop ncdu ];
+}
diff --git a/modules/server/dns/default.nix b/modules/server/dns/default.nix
new file mode 100644
index 000000000000..ae36f06a2f80
--- /dev/null
+++ b/modules/server/dns/default.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  networking.nameservers = [ "127.0.0.1" ];
+
+  services.unbound.enable = true;
+}
diff --git a/modules/server/ftp/default.nix b/modules/server/ftp/default.nix
new file mode 100644
index 000000000000..5fbf3f82877c
--- /dev/null
+++ b/modules/server/ftp/default.nix
@@ -0,0 +1,49 @@
+{ pkgs, lib, config, ... }:
+
+with lib;
+
+let
+  cfg = config.ftp;
+in
+
+{
+  options = {
+    ftp = {
+      files = mkOption {
+        default = {};
+        type = with types; attrsOf path;
+        description = mdDoc ''
+          Files to serve on https://ftp.qyliss.net/
+        '';
+        example = literalExample ''
+          {
+            "foo/bar.txt" = pkgs.writeText "bar.txt" ''''
+              Hello, world!
+            '''';
+          }
+        '';
+      };
+    };
+  };
+
+  config = {
+    services.nginx.virtualHosts."ftp.qyliss.net" = {
+      forceSSL = true;
+      useACMEHost = "qyliss.net";
+
+      root = pkgs.runCommand "ftp.qyliss.net" {} ''
+        mkdir $out
+        ${concatStrings (mapAttrsToList (httpPath: diskPath: ''
+          mkdir -p "$out/$(dirname ${escapeShellArg httpPath})"
+          ln -s ${escapeShellArg diskPath} $out/${escapeShellArg httpPath}
+        '') cfg.files)}
+      '';
+
+      extraConfig = ''
+        autoindex on;
+      '';
+    };
+
+    security.acme.certs."qyliss.net".extraDomainNames = [ "ftp.qyliss.net" ];
+  };
+}
diff --git a/modules/server/git-http-backend/default.nix b/modules/server/git-http-backend/default.nix
new file mode 100644
index 000000000000..32e20e603e61
--- /dev/null
+++ b/modules/server/git-http-backend/default.nix
@@ -0,0 +1,106 @@
+{ lib, pkgs, config, ... }:
+
+let
+  inherit (builtins) split;
+  inherit (lib) flip foldr groupBy head literalExpression mapAttrs mapAttrs'
+    mapAttrsToList mdDoc mkOption nameValuePair optionalAttrs types;
+
+  cfg = config.services.git-http-backend;
+
+  instancesByVhost = groupBy ({ value, ... }: value.vhost)
+    (mapAttrsToList nameValuePair cfg.instances);
+
+  vhostConfigs = mapAttrs (vhost: instances:
+    foldr (l: r: l // r) {} (map ({ name, value }: let
+      path = head (split "/+$" value.path);
+      pathRegex =
+        "^${path}/.*?(\.git)?/(HEAD|info/refs|git-(upload|receive)-pack)$";
+    in {
+      locations = {
+        "~ ${pathRegex}" = {
+          proxyPass = "http://unix:/run/cgiserver/git-http-backend/${name}.sock";
+
+          extraConfig = ''
+            client_max_body_size 0;
+            proxy_read_timeout 3600;
+            proxy_send_timeout 3600;
+          '';
+        };
+      };
+    }) instances)
+  ) instancesByVhost;
+in
+
+{
+  options.services.git-http-backend = {
+    package = mkOption {
+      type = types.package;
+      default = pkgs.gitMinimal;
+      description = mdDoc "git package to use";
+    };
+
+    instances = mkOption {
+      type = types.attrsOf (types.submodule {
+        options = {
+          vhost = mkOption {
+            type = types.str;
+            example = "spectrum-os.org";
+            description = mdDoc "Nginx vhost for the git server";
+          };
+
+          path = mkOption {
+            type = types.strMatching "/(.*[^/])?";
+            default = "/";
+            example = "/git";
+            description = mdDoc ''
+              Path to be prepended to all clone URLs.
+
+              Leading slashes are mandatory; trailing slashes are forbidden.
+            '';
+          };
+
+          cgiserver = mkOption {
+            type = types.package;
+            default = pkgs.cgiserver;
+            defaultText = literalExpression "pkgs.cgiserver";
+            description = mdDoc "cgiserver package to use";
+          };
+
+          projectRoot = mkOption {
+            type = types.strMatching "/(.*[^/])?";
+            example = "/var/www/git";
+            description = mdDoc ''
+              Directory in which to look for git repositories.
+
+              Leading slashes are mandatory; trailing slashes are forbidden.
+            '';
+          };
+        };
+      });
+      default = {};
+      description = mdDoc "List of git-http-backend instances to run";
+    };
+  };
+
+  config = {
+    services.nginx.virtualHosts = vhostConfigs;
+
+    systemd.services = flip mapAttrs' cfg.instances (name: instance: {
+      name = "git-http-backend-${name}";
+      value = {
+        environment.GIT_HTTP_EXPORT_ALL = "";
+        environment.GIT_PROJECT_ROOT = instance.projectRoot;
+        serviceConfig.DynamicUser = true;
+        serviceConfig.ExecStart = "${instance.cgiserver}/bin/cgiserver -r ${instance.path} ${cfg.package}/bin/git-http-backend";
+      };
+    });
+
+    systemd.sockets = flip mapAttrs' cfg.instances (name: instance: {
+      name = "git-http-backend-${name}";
+      value = {
+        wantedBy = [ "sockets.target" ];
+        socketConfig.ListenStream = "/run/cgiserver/git-http-backend/${name}.sock";
+      };
+    });
+  };
+}
diff --git a/modules/server/git/default.nix b/modules/server/git/default.nix
new file mode 100644
index 000000000000..523715a363d9
--- /dev/null
+++ b/modules/server/git/default.nix
@@ -0,0 +1,73 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: 2022 Alyssa Ross <hi@alyssa.is>
+# SPDX-License-Identifier: OSL-3.0
+
+# Adapted from https://src.unfathomable.blue/nixos-config/tree/modules/declarative-git.nix
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.declarative-git;
+
+  repoOpts = { config, ... }: {
+    options = {
+      branch = mkOption {
+        default = "main";
+        description = mdDoc "Branch to be the repository's HEAD";
+        type = types.str;
+      };
+
+      description = mkOption {
+        description = mdDoc "Description of the repository.";
+        type = types.str;
+      };
+
+      config = mkOption {
+        description = mdDoc "Git configuration for the repository.";
+        type = types.attrs;
+        default = {};
+      };
+
+      hooks = mkOption {
+        description = mdDoc "Git hooks for the repository.";
+        type = with types; attrsOf (listOf path);
+        default = {};
+      };
+
+      owner = mkOption {
+        description = mdDoc "Name of the user to own the git repository.";
+        type = types.str;
+        default = "-";
+      };
+
+      group = mkOption {
+        description = mdDoc "Name of the group for the git repository.";
+        type = types.str;
+        default = "-";
+      };
+    };
+  };
+in {
+  options.declarative-git = {
+    repositories = mkOption {
+      description = mdDoc "Repositories to manage declaratively.";
+      type = types.attrsOf (types.submodule repoOpts);
+      default = {};
+    };
+
+    hooks = mkOption {
+      description = mdDoc "Git hooks to apply to all declarative repositories.";
+      type = with types; attrsOf (listOf path);
+      default = {};
+    };
+  };
+
+  config.systemd.tmpfiles.packages = mapAttrsToList (path: config:
+    pkgs.declarative-git-repository {
+      inherit path;
+      inherit (config) branch config description owner group;
+      hooks = zipAttrsWith (_: concatLists) [ cfg.hooks config.hooks ];
+    }) cfg.repositories;
+}
diff --git a/modules/server/git/nixpkgs/default.nix b/modules/server/git/nixpkgs/default.nix
new file mode 100644
index 000000000000..95788318a334
--- /dev/null
+++ b/modules/server/git/nixpkgs/default.nix
@@ -0,0 +1,58 @@
+{ lib, pkgs, ... }:
+
+let
+  inherit (pkgs) writeText;
+  toGitConfig = lib.generators.toINI { listsAsDuplicateKeys = true; };
+in
+
+{
+  users.groups.nixpkgs = {};
+
+  environment.etc.gitconfig.text = ''
+    [safe]
+    	directory = /var/lib/git/nixpkgs.git
+  '';
+
+  systemd.tmpfiles.rules = [
+    "L+ /var/lib/git/nixpkgs.git/HEAD - - - - refs/heads/master"
+    "L+ /var/lib/git/nixpkgs.git/config - - - - ${writeText "config" (toGitConfig {
+      core.repositoryformatversion = 0;
+      core.filemode = true;
+      core.bare = true;
+      core.sharedRepository = "world";
+      "remote \"origin\"" = {
+        url = "https://github.com/NixOS/nixpkgs";
+        fetch = [
+          "+refs/heads/master:refs/remotes/origin/master"
+          "+refs/heads/staging:refs/remotes/origin/staging"
+          "+refs/heads/staging-*:refs/remotes/origin/staging-*"
+          "+refs/heads/nixos-*:refs/remotes/origin/nixos-*"
+          "+refs/heads/nixpkgs-unstable:refs/remotes/origin/nixpkgs-unstable"
+          "+refs/heads/nixpkgs-*-darwin:refs/remotes/origin/nixpkgs-*-darwin"
+          "+refs/heads/release-*:refs/remotes/origin/release-*"
+        ];
+      };
+    })}"
+    "d /var/lib/git/nixpkgs.git 2775 - nixpkgs"
+    "d /var/lib/git/nixpkgs.git/refs 2775 - nixpkgs"
+    "d /var/lib/git/nixpkgs.git/objects 2775 - nixpkgs"
+    "d /var/lib/git/nixpkgs.git/objects/pack 2775 - nixpkgs"
+  ];
+
+  systemd.services.git-fetch-nixpkgs = {
+    after = [ "network-online.target" ];
+    requires = [ "network-online.target" ];
+    serviceConfig.DynamicUser = true;
+    serviceConfig.Group = "nixpkgs";
+    serviceConfig.ReadWritePaths = "/var/lib/git/nixpkgs.git";
+    serviceConfig.ExecStart = "${pkgs.gitMinimal}/bin/git --git-dir /var/lib/git/nixpkgs.git fetch";
+    serviceConfig.Type = "oneshot";
+    serviceConfig.UMask = "0002";
+  };
+
+  systemd.timers.git-fetch-nixpkgs = {
+    wantedBy = [ "timers.target" ];
+    timerConfig.OnActiveSec = 0;
+    timerConfig.OnUnitActiveSec = 300;
+  };
+}
diff --git a/modules/server/irc/default.nix b/modules/server/irc/default.nix
new file mode 100644
index 000000000000..81a039ae420b
--- /dev/null
+++ b/modules/server/irc/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  imports = [ ./soju ./znc ];
+}
diff --git a/modules/server/irc/soju/default.nix b/modules/server/irc/soju/default.nix
new file mode 100644
index 000000000000..8e8a1dce502b
--- /dev/null
+++ b/modules/server/irc/soju/default.nix
@@ -0,0 +1,47 @@
+{ config, lib, ... }:
+
+{
+  networking.firewall.allowedTCPPorts = [ 6698 ];
+
+  services.postgresql.enable = true;
+  services.postgresql.ensureDatabases = [ "soju" ];
+  services.postgresql.ensureUsers = [
+    {
+      name = "soju";
+      ensureDBOwnership = true;
+    }
+  ];
+
+  services.soju.enable = true;
+  services.soju.hostName = "${config.networking.hostName}.${config.networking.domain}";
+  services.soju.extraConfig = ''
+    db postgres "dbname=soju host=/run/postgresql sslmode=disable"
+    message-store db
+  '';
+  services.soju.listen = [
+    "unix:///run/soju/soju.sock"
+    "unix+admin://"
+  ];
+
+  services.nginx.streamConfig = ''
+    server {
+      listen [::]:6698 ssl ipv6only=off;
+      ssl_certificate /var/lib/acme/${config.networking.domain}/fullchain.pem;
+      ssl_certificate_key /var/lib/acme/${config.networking.domain}/key.pem;
+      proxy_pass unix:/run/soju/soju.sock;
+    }
+  '';
+
+  systemd.services.soju.serviceConfig.DynamicUser = lib.mkForce false;
+  systemd.services.soju.serviceConfig.Group = "soju";
+  systemd.services.soju.serviceConfig.RuntimeDirectory = "soju";
+  systemd.services.soju.serviceConfig.UMask = "0007";
+  systemd.services.soju.serviceConfig.User = "soju";
+
+  users.users.nginx.extraGroups = [ "soju" ];
+  users.users.soju = {
+    isNormalUser = true;
+    group = "soju";
+  };
+  users.groups.soju = {};
+}
diff --git a/modules/server/irc/znc/default.nix b/modules/server/irc/znc/default.nix
new file mode 100644
index 000000000000..65ba8d087d83
--- /dev/null
+++ b/modules/server/irc/znc/default.nix
@@ -0,0 +1,31 @@
+{ config, pkgs, ... }:
+
+{
+  services.znc.enable = true;
+  services.znc.useLegacyConfig = false;
+  services.znc.modulePackages = with pkgs; [ zncModules.playback ];
+
+  services.nginx.virtualHosts."znc.${config.networking.domain}" = {
+    forceSSL = true;
+    useACMEHost = "qyliss.net";
+
+    locations = {
+      "/" = {
+        proxyPass = "http://127.0.0.1:6667/";
+      };
+    };
+  };
+
+  services.nginx.streamConfig = ''
+    server {
+      listen [::]:6697 ssl ipv6only=off;
+      ssl_certificate /var/lib/acme/${config.networking.domain}/fullchain.pem;
+      ssl_certificate_key /var/lib/acme/${config.networking.domain}/key.pem;
+      proxy_pass 127.0.0.1:6667;
+    }
+  '';
+
+  security.acme.certs."qyliss.net".extraDomainNames = [ "znc.qyliss.net" ];
+
+  networking.firewall.allowedTCPPorts = [ 6697 ];
+}
diff --git a/modules/server/mail/default.nix b/modules/server/mail/default.nix
new file mode 100644
index 000000000000..cebc16dfabe1
--- /dev/null
+++ b/modules/server/mail/default.nix
@@ -0,0 +1,79 @@
+{ lib, pkgs, config, ... }:
+
+let
+  inherit (pkgs) runCommand;
+
+  mailmanCfg = config.services.mailman;
+in
+
+{
+  services.postgresql.enable = true;
+  services.postgresql.ensureDatabases = [ "mailman" ];
+  services.postgresql.ensureUsers = [
+    {
+      name = "mailman";
+      ensureDBOwnership = true;
+    }
+  ];
+
+  services.mailman.enable = true;
+
+  services.mailman.siteOwner = "postmaster@spectrum-os.org";
+  services.mailman.webHosts = [ "spectrum-os.org" ];
+  services.mailman.hyperkitty.enable = true;
+  services.mailman.hyperkitty.baseUrl = "http://localhost:18507/lists/hyperkitty/";
+  services.mailman.settings.database.class = "mailman.database.postgresql.PostgreSQLDatabase";
+  services.mailman.settings.database.url = "postgresql:///mailman";
+  services.mailman.extraConfig = ''
+
+    [antispam]
+    header_checks:
+      X-Spam-Flag: YES
+
+    [logging.template]
+    level: debug
+  '';
+
+  services.mailman.webSettings.ADMINS = [ [ "Alyssa Ross" "hi@alyssa.is" ] ];
+  services.mailman.webSettings.ALLOWED_HOSTS = [ "localhost" "127.0.0.1" "spectrum-os.org" ];
+  services.mailman.webSettings.INSTALLED_APPS = [
+    "hyperkitty"
+    "postorius"
+    "django_mailman3"
+    "django.contrib.admin"
+    "django.contrib.auth"
+    "django.contrib.contenttypes"
+    "django.contrib.sessions"
+    "django.contrib.sites"
+    "django.contrib.messages"
+    "django.contrib.staticfiles"
+    "rest_framework"
+    "django_gravatar"
+    "compressor"
+    "haystack"
+    "django_extensions"
+    "django_q"
+    "allauth"
+    "allauth.account"
+    "allauth.socialaccount"
+  ];
+  services.mailman.webSettings.USE_X_FORWARDED_HOST = true;
+  services.mailman.webSettings.SECURE_PROXY_SSL_HEADER = [ "HTTP_X_FORWARDED_SCHEME" "https" ];
+  services.mailman.webSettings.SESSION_COOKIE_SECURE = true;
+  services.mailman.webSettings.SECURE_CONTENT_TYPE_NOSNIFF = true;
+  services.mailman.webSettings.SECURE_BROWSER_XSS_FILTER = true;
+  services.mailman.webSettings.CSRF_COOKIE_SECURE = true;
+  services.mailman.webSettings.CSRF_COOKIE_HTTPONLY = true;
+  services.mailman.webSettings.LANGUAGE_CODE = "en-gb";
+  services.mailman.webSettings.STATIC_URL = "/lists/static/";
+  services.mailman.webSettings.DEFAULT_FROM_EMAIL = "postmaster@spectrum-os.org";
+  services.mailman.webSettings.SERVER_EMAIL = "postmaster@spectrum-os.org";
+  services.mailman.webSettings.SOCIALACCOUNT_PROVIDERS = {};
+  services.mailman.webSettings.COMPRESS_CSS_HASHING_METHOD = "content";
+  services.mailman.webSettings.FILTER_VHOST = true;
+
+  systemd.services.mailman.after = [ "postgresql.service" ];
+
+  services.mailman.serve.enable = true;
+  services.mailman.serve.virtualRoot = "/lists";
+}
diff --git a/modules/server/mail/public-inbox/default.nix b/modules/server/mail/public-inbox/default.nix
new file mode 100644
index 000000000000..d533f98d03b9
--- /dev/null
+++ b/modules/server/mail/public-inbox/default.nix
@@ -0,0 +1,62 @@
+{ config, pkgs, lib, ... }:
+
+let
+  public-inbox = config.services.public-inbox.package;
+
+  public-inbox-src = pkgs.stdenv.mkDerivation {
+    name = "public-inbox-${public-inbox.version}-qyliss.tar.gz";
+
+    inherit (public-inbox) src patches;
+
+    doBuild = false;
+
+    installPhase = ''
+      cd $NIX_BUILD_TOP
+      mv $sourceRoot public-inbox-${public-inbox.version}-qyliss
+      tar cf $out public-inbox-${public-inbox.version}-qyliss
+    '';
+  };
+
+  hash = with lib;
+    # Safe because we're just using the hash as a file name, and don't
+    # need the file name itself to have a dependency on the src.
+    builtins.unsafeDiscardStringContext
+      (head (splitString "-"
+        (last (splitString "/" public-inbox-src.outPath))));
+
+  tarballName = "public-inbox-${public-inbox.version}-qyliss-${hash}.tar.gz";
+in
+
+{
+  services.public-inbox.enable = true;
+  services.public-inbox.mda.enable = true;
+  services.public-inbox.http.enable = true;
+  services.public-inbox.nntp.enable = true;
+
+  services.public-inbox.mda.args = [ "--no-precheck" ];
+  services.public-inbox.http.port = "/run/public-inbox-httpd.sock";
+  services.public-inbox.postfix.enable = true;
+  services.public-inbox.settings.publicinbox.wwwlisting = "match=domain";
+
+  services.public-inbox.nntp.port = null;
+  systemd.sockets.public-inbox-nntpd.listenStreams = [ "0.0.0.0:119" "0.0.0.0:563" ];
+  systemd.services.public-inbox-nntpd.serviceConfig.SupplementaryGroups = [ "public-inbox" "acme" ];
+
+  services.public-inbox.settings.publicinbox.css =
+    [ "href=https://spectrum-os.org/lists/archives/public-inbox.css" ];
+
+  services.public-inbox.settings.publicinbox.sourceinfo =
+    let
+      url = "https://ftp.qyliss.net/public-inbox/${tarballName}";
+    in (pkgs.writeText "public-inbox-source-info.html" ''
+      <a href="${url}" download>${url}</a>
+    '').outPath;
+
+  ftp.files."public-inbox/${tarballName}" = public-inbox-src;
+
+  services.spamassassin.enable = true;
+  environment.etc."mail/spamassassin/public-inbox.pre".source =
+    "${public-inbox.sa_config}/root/etc/spamassassin/public-inbox.pre";
+
+  networking.firewall.allowedTCPPorts = [ 119 563 ];
+}
diff --git a/modules/server/nginx/default.nix b/modules/server/nginx/default.nix
new file mode 100644
index 000000000000..d60b1aa30c56
--- /dev/null
+++ b/modules/server/nginx/default.nix
@@ -0,0 +1,22 @@
+{ pkgs, ... }:
+
+{
+  services.nginx.enable = true;
+  services.nginx.package = pkgs.nginxMainline;
+
+  services.nginx.recommendedOptimisation = true;
+  services.nginx.recommendedTlsSettings = true;
+  services.nginx.recommendedGzipSettings = true;
+  services.nginx.recommendedProxySettings = true;
+
+  services.nginx.commonHttpConfig = ''
+    log_format privacy '[$time_local] $request_method '
+                       '$scheme://$host$request_uri $status $body_bytes_sent '
+                       '($upstream_response_time seconds)';
+
+    # systemd catches syslog, and access_log doesn't support stdout/stderr.
+    access_log syslog:server=unix:/dev/log privacy;
+  '';
+
+  networking.firewall.allowedTCPPorts = [ 80 443 ];
+}
diff --git a/modules/server/nixpk.gs/acme/default.nix b/modules/server/nixpk.gs/acme/default.nix
new file mode 100644
index 000000000000..4c3c8f446602
--- /dev/null
+++ b/modules/server/nixpk.gs/acme/default.nix
@@ -0,0 +1,7 @@
+{ config, lib, ... }:
+
+{
+  security.acme.certs."nixpk.gs" = {
+    webroot = "/var/lib/acme/acme-challenge";
+  };
+}
diff --git a/modules/server/nixpk.gs/default.nix b/modules/server/nixpk.gs/default.nix
new file mode 100644
index 000000000000..7ed0e4b4f7d4
--- /dev/null
+++ b/modules/server/nixpk.gs/default.nix
@@ -0,0 +1,5 @@
+{ ... }:
+
+{
+  imports = [ ./acme ./nginx ./pr-tracker ];
+}
diff --git a/modules/server/nixpk.gs/nginx/default.nix b/modules/server/nixpk.gs/nginx/default.nix
new file mode 100644
index 000000000000..cd02a70be0c7
--- /dev/null
+++ b/modules/server/nixpk.gs/nginx/default.nix
@@ -0,0 +1,13 @@
+{ pkgs, ... }:
+
+{
+  services.nginx.virtualHosts."nixpk.gs" = {
+    forceSSL = true;
+    useACMEHost = "nixpk.gs";
+
+    locations."/".root = pkgs.runCommand "index.html" {} ''
+      mkdir -p $out
+      cp ${./index.html} $out/index.html
+    '';
+  };
+}
diff --git a/modules/server/nixpk.gs/nginx/index.html b/modules/server/nixpk.gs/nginx/index.html
new file mode 100644
index 000000000000..0c4e94022447
--- /dev/null
+++ b/modules/server/nixpk.gs/nginx/index.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html lang="en">
+  <title>nixpk.gs</title>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width,initial-scale=1">
+
+  <h1>nixpk.gs</h1>
+
+  <ul>
+    <li><a href="/pr-tracker.html">Pull request tracker</a>
+  </ul>
+</html>
diff --git a/modules/server/nixpk.gs/pr-tracker/default.nix b/modules/server/nixpk.gs/pr-tracker/default.nix
new file mode 100644
index 000000000000..e3b00c433455
--- /dev/null
+++ b/modules/server/nixpk.gs/pr-tracker/default.nix
@@ -0,0 +1,28 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ../../git/nixpkgs ];
+
+  services.nginx.virtualHosts."nixpk.gs".locations."/pr-tracker.html" = {
+    proxyPass = "http://unix:/run/pr-tracker.sock:/pr-tracker.html";
+    extraConfig = ''
+      proxy_http_version 1.1;
+    '';
+  };
+
+  systemd.services.pr-tracker = {
+    requires = [ "pr-tracker.socket" ];
+    serviceConfig.ExecStart = "${pkgs.pr-tracker}/bin/pr-tracker --path /var/lib/git/nixpkgs.git --remote origin --user-agent 'pr-tracker by alyssais' --source-url https://git.qyliss.net/pr-tracker --mount pr-tracker.html";
+    serviceConfig.StandardInput = "file:/etc/pr-tracker/token";
+    serviceConfig.DynamicUser = true;
+    serviceConfig.SupplementaryGroups = "nixpkgs";
+    serviceConfig.UMask = "0002";
+    serviceConfig.ReadWritePaths = "/var/lib/git/nixpkgs.git";
+  };
+
+  systemd.sockets.pr-tracker = {
+    wantedBy = [ "sockets.target" ];
+    before = [ "nginx.service" ];
+    socketConfig.ListenStream = "/run/pr-tracker.sock";
+  };
+}
diff --git a/modules/server/spectrum/acme/default.nix b/modules/server/spectrum/acme/default.nix
new file mode 100644
index 000000000000..6a60f52d2456
--- /dev/null
+++ b/modules/server/spectrum/acme/default.nix
@@ -0,0 +1,7 @@
+{ ... }:
+
+{
+  security.acme.certs."spectrum-os.org" = {
+    webroot = "/var/lib/acme/acme-challenge";
+  };
+}
diff --git a/modules/server/spectrum/cgit/default.nix b/modules/server/spectrum/cgit/default.nix
new file mode 100644
index 000000000000..e691d438a840
--- /dev/null
+++ b/modules/server/spectrum/cgit/default.nix
@@ -0,0 +1,57 @@
+{ pkgs, ... }:
+
+let
+
+  spectrumReadme = pkgs.writeText "about.html" ''
+    <article>
+
+    <h1>Contributing to Spectrum</h1>
+
+    <p>
+    Want to contribute to Spectrum?  We'd love to have you.
+    Have a look at the <a href="/contributing.html">online
+    documentation</a>.
+
+    </article>
+  '';
+
+  sourceFilter = pkgs.runCommand "source-filter" {
+    nativeBuildInputs = with pkgs; with python3.pkgs; [ wrapPython ];
+  } ''
+    mkdir -p $out/bin
+    sed s/pastie/friendly/g >$out/bin/syntax-highlighting.py \
+       <${pkgs.cgit-pink}/lib/cgit/filters/.syntax-highlighting.py-wrapped
+    chmod +x $out/bin/syntax-highlighting.py
+    wrapPythonPrograms
+  '';
+in
+
+{
+  imports = [ ../../cgit ];
+
+  services.cgit-qyliss.instances.spectrum = {
+    package = pkgs.cgit-pink;
+    vhost = "spectrum-os.org";
+    path = "/git";
+    config = pkgs.writeText "cgit.conf" ''
+      clone-prefix=https://spectrum-os.org/git
+      css=/git/cgit.css
+      enable-blame=1
+      enable-commit-graph=1
+      enable-follow-links=1
+      enable-git-config=1
+      enable-index-owner=0
+      favicon=https://spectrum-os.org/logo/logo_html.svg
+      logo=
+      remove-suffix=1
+      root-desc=Web interface for Spectrum source code
+      root-readme=${spectrumReadme}
+      root-title=Spectrum Git Repository Browser
+      snapshots=all
+      about-filter=${pkgs.cgit-pink}/lib/cgit/filters/about-formatting.sh
+      source-filter=${sourceFilter}/bin/syntax-highlighting.py
+
+      scan-path=/home/spectrum/git
+    '';
+  };
+}
diff --git a/modules/server/spectrum/default.nix b/modules/server/spectrum/default.nix
new file mode 100644
index 000000000000..d6c2eaa57d0e
--- /dev/null
+++ b/modules/server/spectrum/default.nix
@@ -0,0 +1,14 @@
+{ ... }:
+
+{
+  imports = [
+    ./acme ./cgit ./git ./git-http-backend ./nginx ./patch-refs ./postfix
+    ./public-inbox ./spectrumbot ./vultr-mon
+  ];
+
+  nix.settings.substituters = [ "https://cache.dataaturservice.se/spectrum/" ];
+  nix.settings.trusted-public-keys = [
+    "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
+    "spectrum-os.org-1:rnnSumz3+Dbs5uewPlwZSTP0k3g/5SRG4hD7Wbr9YuQ="
+  ];
+}
diff --git a/modules/server/spectrum/git-http-backend/default.nix b/modules/server/spectrum/git-http-backend/default.nix
new file mode 100644
index 000000000000..e7a3b003f190
--- /dev/null
+++ b/modules/server/spectrum/git-http-backend/default.nix
@@ -0,0 +1,11 @@
+{ ... }:
+
+{
+  imports = [ ../../git-http-backend ];
+
+  services.git-http-backend.instances.spectrum = {
+    vhost = "spectrum-os.org";
+    path = "/git";
+    projectRoot = "/home/spectrum/git";
+  };
+}
diff --git a/modules/server/spectrum/git/default.nix b/modules/server/spectrum/git/default.nix
new file mode 100644
index 000000000000..beb61b78dd89
--- /dev/null
+++ b/modules/server/spectrum/git/default.nix
@@ -0,0 +1,110 @@
+{ pkgs, ... }:
+
+{
+  imports = [ ../../git ];
+
+  declarative-git.repositories."/home/spectrum/git/crosvm.git" = {
+    branch = "master";
+    description = "Downstream crosvm tree for Spectrum";
+    group = "spectrum";
+    config.cgit.section = "obsolete";
+  };
+
+  declarative-git.repositories."/home/spectrum/git/doc.git" = {
+    branch = "master";
+    description = "Old manuals for Spectrum";
+    hooks.post-update = [
+      (pkgs.writeShellScript "post-update.sh" ''
+        nix-build --tarball-ttl 0 --out-link built --cores 1 -j1 -E "
+          let src = builtins.fetchGit ./.;
+          in (import src).overrideAttrs ({ ... }: { inherit src; })
+        "
+      '')
+    ];
+    group = "spectrum";
+    config.cgit.section = "obsolete";
+  };
+
+  declarative-git.repositories."/home/spectrum/git/nixpkgs.git" = {
+    branch = "rootfs";
+    description = "Downstream nixpkgs tree for Spectrum";
+    group = "spectrum";
+    config.cgit.defBranch = "rootfs";
+    config.cgit.section = "obsolete";
+    config.core.sharedrepository = "0644";
+  };
+
+  declarative-git.repositories."/home/spectrum/git/mktuntap.git" = {
+    branch = "master";
+    description = "Utility program for creating TUN and TAP devices on file descriptors";
+    group = "spectrum";
+    config.cgit.readme = ":README";
+    config.core.sharedrepository = "0644";
+    config.receive.denyNonFastforwards = true;
+  };
+
+  declarative-git.repositories."/home/spectrum/git/spectrum.git" = {
+    description = "A compartmentalized operating system";
+    group = "spectrum";
+    config.cgit.defBranch = "main";
+    hooks.post-receive = with pkgs; [
+      (writeShellScript "send-email.sh" ''
+        set -ueo pipefail
+        export PATH=${lib.makeBinPath [ coreutils curl gitMinimal gnused mailutils ]}
+
+        repo_url=https://spectrum-os.org/git/spectrum
+        inbox_url=https://spectrum-os.org/lists/archives/spectrum-devel
+
+        while read oldrev newrev refname; do
+            [ "$refname" = "refs/heads/main" ] || continue
+
+            git log --reverse --format=%H "$oldrev..$newrev" | while read commit; do
+                message_id="$(git log -1 --format=%B "$commit" |
+                    git interpret-trailers --parse |
+                    sed -n 's/^Message-Id: <\(.*\)>$/\1/Ip' | head -n 1)"
+
+                [ -n "$message_id" ] || continue
+
+                url="$inbox_url/$message_id/raw"
+                path="$(mktemp)"
+                curl -LSfso "$path" "$url"
+                mail -E "file $path" -E "reply" -E "quit" <<EOF
+        This patch has been committed as $commit,
+        which can be viewed online at
+        $repo_url/commit/?id=$commit.
+
+        This is an automated message.  Send comments/questions/requests to:
+        Alyssa Ross <hi@alyssa.is>
+        EOF
+                rm "$path"
+            done
+        done
+      '')
+      (writeShellScript "build-documentation.sh" ''
+        nix-build --tarball-ttl 0 --out-link /home/spectrum/Documentation -E '
+          import "''${builtins.fetchGit { url = ./.; ref = "main"; }}/Documentation" {}
+        '
+      '')
+    ];
+  };
+
+  declarative-git.repositories."/home/spectrum/git/ucspi-vsock.git" = {
+    branch = "master";
+    description = "UCSPI-1996 implementation for Linux AF_VSOCK sockets";
+    group = "spectrum";
+    config.cgit.section = "obsolete";
+  };
+
+  declarative-git.repositories."/home/spectrum/git/www.git" = {
+    branch = "master";
+    description = "Static source files for the Spectrum website";
+    group = "spectrum";
+    config.cgit.readme = ":README";
+    config.core.bare = false;
+    config.core.logallrefupdates = true;
+    config.core.sharedrepository = 1;
+    config.core.worktree = "../../www";
+    config.receive.denyCurrentBranch = "updateInstead";
+    config.receive.denyNonFastforwards = true;
+  };
+}
diff --git a/modules/server/spectrum/nginx/default.nix b/modules/server/spectrum/nginx/default.nix
new file mode 100644
index 000000000000..edb4a8f855f1
--- /dev/null
+++ b/modules/server/spectrum/nginx/default.nix
@@ -0,0 +1,41 @@
+{ lib, ... }:
+
+let
+  inherit (lib) head tail;
+
+  redirectDomains = [
+    "spectrum-os.com"
+    "spectrumos.org"
+    "www.spectrum-os.com"
+    "www.spectrum-os.org"
+    "www.spectrumos.org"
+  ];
+in
+
+{
+  services.nginx.virtualHosts."spectrum-redirects" = {
+    serverName = head redirectDomains;
+    serverAliases = tail redirectDomains;
+    addSSL = true;
+    useACMEHost = "spectrum-os.org";
+    globalRedirect = "spectrum-os.org";
+  };
+
+  services.nginx.virtualHosts."spectrum-os.org".locations."= /participating.html".return =
+    "301 /doc/contributing/communication.html";
+
+  services.nginx.virtualHosts."spectrum-os.org".locations."= /doc".return =
+    "301 /doc/";
+  services.nginx.virtualHosts."spectrum-os.org".locations."/doc/".alias =
+    "/home/spectrum/Documentation/";
+
+  # TODO: some sort of robots.txt generation module might be nice.
+  services.nginx.virtualHosts."spectrum-os.org".locations."= /robots.txt" = {
+    alias = ./robots.txt;
+  };
+
+  security.acme.certs."spectrum-os.org".extraDomainNames = redirectDomains;
+
+  # The Spectrum website lives in /home/spectrum/www
+  systemd.services.nginx.serviceConfig.ProtectHome = false;
+}
diff --git a/modules/server/spectrum/nginx/robots.txt b/modules/server/spectrum/nginx/robots.txt
new file mode 100644
index 000000000000..b2d1429a929b
--- /dev/null
+++ b/modules/server/spectrum/nginx/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+# Wildcards are not supported in the robots.txt spec,
+# but let's hope they work anyway.
+Disallow: /git/*/snapshot/
+Crawl-delay: 5
diff --git a/modules/server/spectrum/patch-refs/default.nix b/modules/server/spectrum/patch-refs/default.nix
new file mode 100644
index 000000000000..8e608c5201d2
--- /dev/null
+++ b/modules/server/spectrum/patch-refs/default.nix
@@ -0,0 +1,46 @@
+{ lib, pkgs, ... }:
+
+{
+  users.users.patch-refs = {
+    description = "spectrum-devel patch monitor";
+    group = "spectrum";
+    isSystemUser = true;
+  };
+
+  services.postfix.virtual = ''
+    patch-refs@spectrum-os.org patch-refs@spectrum-os.org
+  '';
+
+  services.postfix.transport = ''
+    patch-refs@spectrum-os.org patch-refs:
+  '';
+
+  services.postfix.masterConfig.patch-refs = {
+    type = "unix";
+    command = "pipe";
+    privileged = true;
+    args = [
+      "flags=X"
+      "user=patch-refs"
+      "argv=${with pkgs; toString [
+        "${execline}/bin/export" "PATH"
+        (lib.makeBinPath [
+          b4 coreutils findutils gitMinimal strace
+
+          (mblaze.overrideAttrs ({ patches ? [], ... }: {
+            patches = patches ++ [
+              (fetchpatch {
+                url = "https://inbox.vuxu.org/mblaze/20220523170921.2623516-1-hi@alyssa.is/raw";
+                sha256 = "1fwnr6277fjdrv0lvjrzyxjd1p94c6jg2nl6cd4lh9aizmfbjiq0";
+              })
+            ];
+          }))
+        ])
+        "${execline}/bin/execlineb"
+        "-S1"
+        (copyPathToStore ./mda.elb)
+        "$client_address"
+      ]}"
+    ];
+  };
+}
diff --git a/modules/server/spectrum/patch-refs/mda.elb b/modules/server/spectrum/patch-refs/mda.elb
new file mode 100644
index 000000000000..c613d0529f88
--- /dev/null
+++ b/modules/server/spectrum/patch-refs/mda.elb
@@ -0,0 +1,36 @@
+foreground { echo "Mail from " $1 }
+if -x 77 { test $1 = IPv6:::1 }
+
+backtick message_id { mhdr -h Message-Id - }
+backtick dir { mktemp -d }
+
+multisubstitute {
+  importas -i message_id message_id
+  importas -i dir dir
+  define origin /home/spectrum/git/spectrum.git
+}
+
+foreground {
+  if { mkdir ${dir}/git }
+  cd ${dir}/git
+  export GIT_CONFIG_COUNT 2
+  export GIT_CONFIG_KEY_0 am.messageid
+  export GIT_CONFIG_VALUE_0 true
+  export GIT_CONFIG_KEY_1 b4.midmask
+  export GIT_CONFIG_VALUE_1 https://spectrum-os.org/lists/archives/spectrum-test/%s
+  export XDG_CACHE_HOME ${dir}/cache
+  export XDG_DATA_HOME ${dir}/data
+  if { git clone -n --single-branch --reference $origin $origin . }
+  if -x 75 { b4 shazam -CH $message_id }
+  pipeline {
+    git log -z --format=%H:refs/patches/%(trailers:key=Message-Id,valueonly)
+      HEAD..FETCH_HEAD
+  }
+  pipeline { tr -d <> }
+  redirfd -w 2 /tmp/err
+  xargs -tr0
+  git push origin --dry-run
+}
+importas -iu exit ?
+if { rm -rf $dir }
+exit $exit
diff --git a/modules/server/spectrum/postfix/default.nix b/modules/server/spectrum/postfix/default.nix
new file mode 100644
index 000000000000..978cb47726e6
--- /dev/null
+++ b/modules/server/spectrum/postfix/default.nix
@@ -0,0 +1,71 @@
+{ pkgs, ... }:
+
+{
+  services.postfix.enable = true;
+  services.postfix.enableSubmission = true;
+  services.postfix.hostname = "atuin.qyliss.net";
+  services.postfix.config.smtp_tls_loglevel = "1";
+  services.postfix.config.smtpd_forbid_bare_newline = true;
+  services.postfix.config.disable_mime_output_conversion = true;
+  services.postfix.sslCert = "/var/lib/acme/spectrum-os.org/fullchain.pem";
+  services.postfix.sslKey = "/var/lib/acme/spectrum-os.org/key.pem";
+  services.postfix.rootAlias = "hi@alyssa.is";
+  services.postfix.relayDomains = [ "hash:/var/lib/mailman/data/postfix_domains" ];
+  services.postfix.config.transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+  services.postfix.localRecipients = []; # empty array causes NixOS to add $alias_maps
+  services.postfix.config.mailbox_command = "${pkgs.coreutils}/bin/false";
+  services.postfix.config.local_recipient_maps =
+    [ "proxy:unix:passwd.byname" "hash:/var/lib/mailman/data/postfix_lmtp" ];
+
+  services.postfix.destination =
+    [ "atuin.qyliss.net" "qyliss.net" "spectrumos.org" "spectrum-os.org" ];
+  services.postfix.extraAliases = ''
+    abuse: root
+    noc: root
+    security: root
+    hostmaster: root
+    usenet: root
+    news: root
+    webmaster: root
+    www: root
+    uucp: root
+    ftp: root
+  '';
+
+  services.postfix.enableHeaderChecks = true;
+
+  # Local mail can be submitted without being filtered through SpamAssassin.
+  services.postfix.masterConfig."::1:smtp" = {
+    type = "inet";
+    private = false;
+    command = "smtpd";
+  };
+
+  services.postfix.masterConfig.smtp_inet.args =
+    [ "-o" "content_filter=spamassassin" ];
+
+  services.postfix.masterConfig.spamassassin = {
+    privileged = true;
+    chroot = false;
+    command = "pipe";
+    args = [
+      "user=postfix-spamc"
+      "argv=${pkgs.spamassassin}/bin/spamc"
+      "-f"
+      "-e"
+      "/run/wrappers/bin/sendmail"
+      "-oi"
+      "-f"
+      "\${sender}"
+      "\${recipient}"
+    ];
+  };
+
+  networking.firewall.allowedTCPPorts = [ 25 ];
+
+  users.groups.postfix-spamc = {};
+  users.users.postfix-spamc = {
+    group = "postfix-spamc";
+    isSystemUser = true;
+  };
+}
diff --git a/modules/server/spectrum/public-inbox/default.nix b/modules/server/spectrum/public-inbox/default.nix
new file mode 100644
index 000000000000..2c5aed09631b
--- /dev/null
+++ b/modules/server/spectrum/public-inbox/default.nix
@@ -0,0 +1,70 @@
+{ config, lib, ... }:
+
+let
+  repos = [ "crosvm" "doc" "mktuntap" "nixpkgs" "spectrum" "ucspi-vsock" "www" ];
+in
+
+{
+  imports = [ ../../mail/public-inbox ];
+
+  services.public-inbox.http.mounts =
+    [ "https://spectrum-os.org/lists/archives" ];
+  services.public-inbox.nntp.cert =
+    "/var/lib/acme/spectrum-os.org/fullchain.pem";
+  services.public-inbox.nntp.key = "/var/lib/acme/spectrum-os.org/key.pem";
+  services.public-inbox.settings.publicinbox.nntpserver =
+    [ "nntps://spectrum-os.org" "nntp://spectrum-os.org" ];
+
+  systemd.services.public-inbox-httpd.serviceConfig.ProtectHome = "tmpfs";
+  systemd.services.public-inbox-httpd.serviceConfig.BindReadOnlyPaths =
+    map (c: c.dir) (lib.attrValues config.services.public-inbox.settings.coderepo);
+
+  services.public-inbox.settings.coderepo = lib.genAttrs repos (name: {
+    dir = "/home/spectrum/git/${name}.git";
+    cgitUrl = "https://spectrum-os.org/git/${name}";
+  });
+
+  services.public-inbox.inboxes.spectrum-announce = {
+    address = [
+      "public-inbox+spectrum-announce@spectrum-os.org"
+      "announce@spectrum-os.org"
+    ];
+    description = "announcements from the spectrum developers";
+    url = "https://spectrum-os.org/lists/archives/spectrum-announce";
+    newsgroup = "inbox.comp.spectrum.announce";
+  };
+
+  services.public-inbox.inboxes.spectrum-discuss = {
+    address = [
+      "public-inbox+spectrum-discuss@spectrum-os.org"
+      "discuss@spectrum-os.org"
+    ];
+    description = "general high-level discussion about spectrum";
+    filter = "PublicInbox::Filter::Mirror";
+    url = "https://spectrum-os.org/lists/archives/spectrum-discuss";
+    newsgroup = "inbox.comp.spectrum.discuss";
+  };
+
+  services.public-inbox.inboxes.spectrum-devel = {
+    address = [
+      "public-inbox+spectrum-devel@spectrum-os.org"
+      "devel@spectrum-os.org"
+    ];
+    description = "patches and low-level development discussion";
+    filter = "PublicInbox::Filter::Mirror";
+    url = "https://spectrum-os.org/lists/archives/spectrum-devel";
+    newsgroup = "inbox.comp.spectrum.devel";
+    coderepo = repos;
+  };
+
+  services.public-inbox.inboxes.spectrum-test = {
+    address = [
+      "public-inbox+spectrum-test@spectrum-os.org"
+      "test@spectrum-os.org"
+    ];
+    description = "test list for spectrum infrastructure";
+    url = "https://spectrum-os.org/lists/archives/spectrum-test";
+    newsgroup = "inbox.comp.spectrum.test";
+    hide = [ "www" ];
+  };
+}
diff --git a/modules/server/spectrum/spectrumbot/default.nix b/modules/server/spectrum/spectrumbot/default.nix
new file mode 100644
index 000000000000..bef02077a3af
--- /dev/null
+++ b/modules/server/spectrum/spectrumbot/default.nix
@@ -0,0 +1,5 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [ ./irccat ./postfix ];
+}
diff --git a/modules/server/spectrum/spectrumbot/irccat/default.nix b/modules/server/spectrum/spectrumbot/irccat/default.nix
new file mode 100644
index 000000000000..f4efd3828703
--- /dev/null
+++ b/modules/server/spectrum/spectrumbot/irccat/default.nix
@@ -0,0 +1,53 @@
+{ config, pkgs, ... }:
+
+{
+  environment.etc."irccat.json".text = builtins.toJSON {
+    tcp.listen = "[::1]:18770";
+
+    irc.server = "irc.libera.chat:6697";
+    irc.tls = true;
+    irc.nick = "spectrumbot";
+    irc.realname = "#spectrum bot";
+    irc.channels = [ "#spectrum" ];
+    irc.keys = {};
+
+    irc.sasl_external = true;
+    irc.tls_client_cert = "/var/lib/irccat/tls.pem";
+
+    commands = {};
+  };
+
+  systemd.services.irccat = {
+    after = [ "network-online.target" ];
+    requires = [ "network-online.target" ];
+    restartTriggers = [ config.environment.etc."irccat.json".source ];
+    serviceConfig.StateDirectory = "irccat";
+    serviceConfig.StateDirectoryMode = "0700";
+    serviceConfig.ExecStart = "${pkgs.irccat}/bin/irccat";
+    serviceConfig.Restart = "always";
+    serviceConfig.RestartSec = 60;
+    wantedBy = [ "multi-user.target" ];
+
+    serviceConfig.CapabilityBoundingSet = "";
+    serviceConfig.DynamicUser = true;
+    serviceConfig.LockPersonality = true;
+    serviceConfig.MemoryDenyWriteExecute = true;
+    serviceConfig.PrivateDevices = true;
+    serviceConfig.PrivateUsers = true;
+    serviceConfig.ProcSubset = "pid";
+    serviceConfig.ProtectClock = true;
+    serviceConfig.ProtectControlGroups = true;
+    serviceConfig.ProtectHome = true;
+    serviceConfig.ProtectHostname = true;
+    serviceConfig.ProtectKernelLogs = true;
+    serviceConfig.ProtectKernelModules = true;
+    serviceConfig.ProtectKernelTunables = true;
+    serviceConfig.ProtectProc = "invisible";
+    serviceConfig.RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
+    serviceConfig.RestrictNamespaces = true;
+    serviceConfig.RestrictRealtime = true;
+    serviceConfig.SystemCallArchitectures = "native";
+    serviceConfig.SystemCallFilter = [ "@system-service" "~@privileged" ];
+    serviceConfig.UMask = "0077";
+  };
+}
diff --git a/modules/server/spectrum/spectrumbot/postfix/default.nix b/modules/server/spectrum/spectrumbot/postfix/default.nix
new file mode 100644
index 000000000000..3430107a59be
--- /dev/null
+++ b/modules/server/spectrum/spectrumbot/postfix/default.nix
@@ -0,0 +1,39 @@
+{ lib, pkgs, ... }:
+
+{
+  users.groups.irccat-mail = {};
+  users.users.irccat-mail = {
+    isSystemUser = true;
+    group = "irccat-mail";
+  };
+
+  services.postfix.virtual = ''
+    irccat@spectrum-os.org irccat@spectrum-os.org
+  '';
+
+  services.postfix.transport = ''
+    irccat@spectrum-os.org irccat:
+  '';
+
+  services.postfix.masterConfig.irccat = {
+    type = "unix";
+    maxproc = 1;
+    command = "pipe";
+    privileged = true;
+    args = [
+      "flags=X"
+      "user=irccat-mail"
+      "argv=${with pkgs; toString [
+        "${execline}/bin/export" "PATH"
+        (lib.makeBinPath [ coreutils gnused libressl.nc mblaze ])
+        "${execline}/bin/execlineb"
+        "-S1"
+        (copyPathToStore ./mda.elb)
+        "$client_address"
+      ]}"
+    ];
+  };
+
+  systemd.services.postfix.wants = [ "irccat.service" ];
+}
+
diff --git a/modules/server/spectrum/spectrumbot/postfix/mda.elb b/modules/server/spectrum/spectrumbot/postfix/mda.elb
new file mode 100644
index 000000000000..05b111cf59a5
--- /dev/null
+++ b/modules/server/spectrum/spectrumbot/postfix/mda.elb
@@ -0,0 +1,26 @@
+backtick -E path { mktemp }
+if { redirfd -w 1 $path cat }
+foreground { echo "Mail from " $1 }
+if -x 77 { test $1 = IPv6:::1 }
+
+foreground {
+  pipeline -w { nc -N ::1 18770 }
+  pipeline -w { if { tr -d "\n" } echo }
+  backtick list {
+    pipeline { mhdr -h List-Id $path }
+    sed "s/.*<\\([^.>]*\\)[.>].*/\\1/"
+  }
+  if { printf "📨 #ORANGE" }
+  if { printenv list }
+  if { printf "@ #GREEN" }
+  if { maddr -dh from $path }
+  if { printf " #NORMAL#BOLD" }
+  if { mhdr -h Subject $path }
+  if { printf " #NORMAL#BLUE#UNDERLINEhttps://spectrum-os.org/lists/archives/spectrum-" }
+  if { printenv list }
+  pipeline { mhdr -h Message-Id $path }
+  sed "s,.*<\\([^>]*\\)>.*,/\\1/,"
+}
+importas -iu exit ?
+if { rm $path }
+exit $exit
diff --git a/modules/server/spectrum/vultr-mon/default.nix b/modules/server/spectrum/vultr-mon/default.nix
new file mode 100644
index 000000000000..50890d68ccbb
--- /dev/null
+++ b/modules/server/spectrum/vultr-mon/default.nix
@@ -0,0 +1,24 @@
+{ pkgs, ... }:
+
+{
+  systemd.services.vultr-mon = {
+    after = [ "network-online.target" ];
+    requires = [ "network-online.target" ];
+    path = with pkgs; [ coreutils curl findutils jq ];
+    script = ''
+      api_base=https://api.vultr.com/v2
+      curl -fsLSH @/var/lib/vultr-mon/key $api_base/instances |
+          jq -r '.instances[] | select(.date_created < $date) | .id' --arg date "$(date -uIseconds -d '24 hours ago')" |
+          xargs -rtd '\n' -I% curl -fsLSX DELETE -H @/var/lib/vultr-mon/key $api_base/instances/%
+    '';
+    serviceConfig.DynamicUser = true;
+    serviceConfig.StateDirectory = "vultr-mon";
+    serviceConfig.Type = "oneshot";
+  };
+
+  systemd.timers.vultr-mon = {
+    wantedBy = [ "timers.target" ];
+    timerConfig.OnActiveSec = 0;
+    timerConfig.OnUnitActiveSec = 3600;
+  };
+}
diff --git a/modules/server/tor/default.nix b/modules/server/tor/default.nix
new file mode 100644
index 000000000000..3e93309835b6
--- /dev/null
+++ b/modules/server/tor/default.nix
@@ -0,0 +1,19 @@
+{ lib, config, ... }:
+
+{
+  networking.firewall.allowedTCPPorts = [ 143 ];
+
+  services.tor.enable = true;
+
+  services.tor.settings.AccountingMax =
+    lib.mkDefault (throw "Set tor accountingMax!!");
+
+  services.tor.settings.AccountingStart =
+    lib.mkDefault (throw "Set tor accountingStart!!");
+
+  services.tor.relay.enable = true;
+  services.tor.relay.role = "relay";
+  services.tor.settings.ContactInfo = "hi@alyssa.is";
+  services.tor.settings.Nickname = lib.mkDefault config.networking.hostName;
+  services.tor.settings.ORPort = [ 143 ];
+}
diff --git a/modules/server/xmpp/default.nix b/modules/server/xmpp/default.nix
new file mode 100644
index 000000000000..dcd3dba8000f
--- /dev/null
+++ b/modules/server/xmpp/default.nix
@@ -0,0 +1,30 @@
+{ pkgs, ... }:
+
+{
+  networking.firewall.allowedTCPPorts = [ 5222 5269 5281 ];
+
+  security.acme.certs."qyliss.net".extraDomainNames =
+    [ "muc.qyliss.net" "upload.qyliss.net" ];
+
+  services.prosody.enable = true;
+  services.prosody.modules.http_files = true;
+  services.prosody.modules.mam = true;
+  services.prosody.s2sSecureAuth = true;
+  services.prosody.muc = [
+    { domain = "muc.qyliss.net"; }
+  ];
+  services.prosody.package = pkgs.prosody.override {
+    withCommunityModules = [ "smacks" "csi" ];
+  };
+  services.prosody.ssl.key = "/var/lib/acme/qyliss.net/key.pem";
+  services.prosody.ssl.cert = "/var/lib/acme/qyliss.net/fullchain.pem";
+  services.prosody.uploadHttp.domain = "upload.qyliss.net";
+  services.prosody.virtualHosts."qyliss.net" = {
+    domain = "qyliss.net";
+    enabled = true;
+    ssl.key = "/var/lib/acme/qyliss.net/key.pem";
+    ssl.cert = "/var/lib/acme/qyliss.net/fullchain.pem";
+  };
+
+  users.users.prosody.extraGroups = [ "acme" ];
+}