about summary refs log tree commit diff
path: root/ops/nixos
diff options
context:
space:
mode:
Diffstat (limited to 'ops/nixos')
-rw-r--r--ops/nixos/.gitignore3
-rw-r--r--ops/nixos/.skip-subtree1
-rw-r--r--ops/nixos/README.md7
-rw-r--r--ops/nixos/all-systems.nix15
-rw-r--r--ops/nixos/clbot.nix75
-rw-r--r--ops/nixos/default.nix58
-rw-r--r--ops/nixos/depot.nix16
-rw-r--r--ops/nixos/irccat.nix49
-rw-r--r--ops/nixos/monorepo-gerrit.nix123
-rw-r--r--ops/nixos/panettone.nix104
-rw-r--r--ops/nixos/paroxysm.nix27
-rw-r--r--ops/nixos/quassel.nix76
-rw-r--r--ops/nixos/smtprelay.nix53
-rw-r--r--ops/nixos/sourcegraph.nix52
-rw-r--r--ops/nixos/tvl-slapd/default.nix89
-rw-r--r--ops/nixos/tvl-sso/default.nix24
-rw-r--r--ops/nixos/v4l2loopback.nix12
-rw-r--r--ops/nixos/whitby/OWNERS6
-rw-r--r--ops/nixos/whitby/README.md5
-rw-r--r--ops/nixos/whitby/default.nix456
-rw-r--r--ops/nixos/www/b.tvl.fyi.nix21
-rw-r--r--ops/nixos/www/base.nix36
-rw-r--r--ops/nixos/www/cl.tvl.fyi.nix24
-rw-r--r--ops/nixos/www/code.tvl.fyi.nix27
-rw-r--r--ops/nixos/www/cs.tvl.fyi.nix30
-rw-r--r--ops/nixos/www/login.tvl.fyi.nix24
-rw-r--r--ops/nixos/www/tazj.in.nix36
-rw-r--r--ops/nixos/www/todo.tvl.fyi.nix24
-rw-r--r--ops/nixos/www/tvl.fyi.nix30
-rw-r--r--ops/nixos/www/wigglydonke.rs.nix15
30 files changed, 1518 insertions, 0 deletions
diff --git a/ops/nixos/.gitignore b/ops/nixos/.gitignore
new file mode 100644
index 000000000000..773fa1667019
--- /dev/null
+++ b/ops/nixos/.gitignore
@@ -0,0 +1,3 @@
+hardware-configuration.nix
+local-configuration.nix
+result
diff --git a/ops/nixos/.skip-subtree b/ops/nixos/.skip-subtree
new file mode 100644
index 000000000000..09520f8c831f
--- /dev/null
+++ b/ops/nixos/.skip-subtree
@@ -0,0 +1 @@
+NixOS modules are not readTree compatible.
diff --git a/ops/nixos/README.md b/ops/nixos/README.md
new file mode 100644
index 000000000000..595b4c3344c6
--- /dev/null
+++ b/ops/nixos/README.md
@@ -0,0 +1,7 @@
+NixOS modules
+=============
+
+This folder contains various NixOS modules shared by our NixOS
+configurations.
+
+It is not read by `readTree`.
diff --git a/ops/nixos/all-systems.nix b/ops/nixos/all-systems.nix
new file mode 100644
index 000000000000..d1bf39746252
--- /dev/null
+++ b/ops/nixos/all-systems.nix
@@ -0,0 +1,15 @@
+{ depot, ... }:
+
+(with depot.ops.nixos; [
+  whitby
+]) ++
+
+(with depot.users.tazjin.nixos; [
+  camden
+  frog
+]) ++
+
+(with depot.users.glittershark.system.system; [
+  chupacabra
+  yeren
+])
diff --git a/ops/nixos/clbot.nix b/ops/nixos/clbot.nix
new file mode 100644
index 000000000000..0c45badd2b3e
--- /dev/null
+++ b/ops/nixos/clbot.nix
@@ -0,0 +1,75 @@
+# Module that configures CLBot, our Gerrit->IRC info bridge.
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (builtins) attrValues concatStringsSep mapAttrs readFile;
+  inherit (pkgs) runCommandNoCC;
+
+  inherit (lib)
+    listToAttrs
+    mkEnableOption
+    mkIf
+    mkOption
+    removeSuffix
+    types;
+
+  description = "Bot to forward CL notifications";
+  cfg = config.services.depot.clbot;
+
+  mkFlags = flags:
+    concatStringsSep " "
+      (attrValues (mapAttrs (key: value: "-${key} \"${toString value}\"") flags));
+
+  # Escapes a unit name for use in systemd
+  systemdEscape = name: removeSuffix "\n" (readFile (runCommandNoCC "unit-name" {} ''
+    ${pkgs.systemd}/bin/systemd-escape '${name}' >> $out
+  ''));
+
+  mkUnit = flags: channel: {
+    name = "clbot-${systemdEscape channel}";
+    value = {
+      description = "${description} to ${channel}";
+      wantedBy = [ "multi-user.target" ];
+
+      script = "${config.depot.fun.clbot}/bin/clbot ${mkFlags (cfg.flags // {
+        irc_channel = channel;
+      })} -alsologtostderr";
+
+      serviceConfig = {
+        User = "clbot";
+        EnvironmentFile = "/etc/secrets/clbot";
+        Restart = "always";
+      };
+    };
+  };
+in {
+  options.services.depot.clbot = {
+    enable = mkEnableOption description;
+
+    flags = mkOption {
+      type = types.attrsOf types.str;
+      description = "Key value pairs for command line flags";
+    };
+
+    channels = mkOption {
+      type = with types; listOf str;
+      description = "Channels in which to post (generates one unit per channel)";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    # This does not use DynamicUser because we need to make some files
+    # (notably the SSH private key) readable by this user outside of
+    # the module.
+    users = {
+      groups.clbot = {};
+
+      users.clbot = {
+        group = "clbot";
+        isNormalUser = false;
+      };
+    };
+
+    systemd.services = listToAttrs (map (mkUnit cfg.flags) cfg.channels);
+  };
+}
diff --git a/ops/nixos/default.nix b/ops/nixos/default.nix
new file mode 100644
index 000000000000..312d762f246b
--- /dev/null
+++ b/ops/nixos/default.nix
@@ -0,0 +1,58 @@
+# Most of the Nix expressions in this folder are NixOS modules, which
+# are not readTree compatible.
+#
+# Some things (such as system configurations) are, and we import them
+# here manually.
+#
+# TODO(tazjin): Find a more elegant solution for the whole module
+# situation.
+{ lib, pkgs, depot, ... }@args:
+
+let
+  inherit (lib) findFirst isAttrs;
+in
+
+rec {
+  whitby = import ./whitby/default.nix args;
+
+  # System installation
+
+  allSystems = import ./all-systems.nix args;
+
+  nixosFor = configuration: depot.third_party.nixos {
+    configuration = {
+      inherit depot;
+      imports = [
+        configuration
+        "${depot.depotPath}/ops/nixos/depot.nix"
+      ];
+    };
+  };
+
+  findSystem = hostname:
+    (findFirst
+      (system: system.config.networking.hostName == hostname)
+      (throw "${hostname} is not a known NixOS host")
+      (map nixosFor allSystems));
+
+  rebuild-system = pkgs.writeShellScriptBin "rebuild-system" ''
+    set -ue
+    if [[ $EUID -ne 0 ]]; then
+      echo "Oh no! Only root is allowed to rebuild the system!" >&2
+      exit 1
+    fi
+
+    echo "Rebuilding NixOS for $HOSTNAME"
+    system=$(nix-build -E "((import ${toString depot.depotPath} {}).ops.nixos.findSystem \"$HOSTNAME\").system" --no-out-link --show-trace)
+
+    nix-env -p /nix/var/nix/profiles/system --set $system
+    $system/bin/switch-to-configuration switch
+  '';
+
+  # Systems that should be built in CI
+  #
+  # TODO(tazjin): Refactor the whole systems setup, it's a bit
+  # inconsistent at the moment.
+  whitbySystem = (nixosFor whitby).system;
+  meta.targets = [ "whitbySystem" ];
+}
diff --git a/ops/nixos/depot.nix b/ops/nixos/depot.nix
new file mode 100644
index 000000000000..2c1b71a2da9b
--- /dev/null
+++ b/ops/nixos/depot.nix
@@ -0,0 +1,16 @@
+# This module makes it possible to get at the depot from "proper"
+# NixOS modules.
+#
+# It needs to be included and configured in each system like this:
+#
+# {
+#   imports = [ "${depot.depotPath}/ops/nixos/depot.nix" ];
+#   inherit depot;
+# }
+{ lib, ... }:
+
+{
+  options.depot = with lib; mkOption {
+    description = "tazjin's imported monorepo";
+  };
+}
diff --git a/ops/nixos/irccat.nix b/ops/nixos/irccat.nix
new file mode 100644
index 000000000000..68735e4ce54f
--- /dev/null
+++ b/ops/nixos/irccat.nix
@@ -0,0 +1,49 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.irccat;
+  description = "irccat - forward messages to IRC";
+
+  # irccat expects to read its configuration from the *current
+  # directory*, and its configuration contains secrets.
+  #
+  # To make this work we construct the JSON configuration file and
+  # then recursively merge it with an on-disk secret using jq on
+  # service launch.
+  configJson = pkgs.writeText "irccat.json" (builtins.toJSON cfg.config);
+  configMerge = pkgs.writeShellScript "merge-irccat-config" ''
+    if [ ! -f "/etc/secrets/irccat.json" ]; then
+      echo "irccat secrets file is missing"
+      exit 1
+    fi
+
+    # jq's * is the recursive merge operator
+    ${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${configJson} /etc/secrets/irccat.json \
+      > /var/lib/irccat/irccat.json
+  '';
+in {
+  options.services.depot.irccat = {
+    enable = lib.mkEnableOption description;
+
+    config = lib.mkOption {
+      type = lib.types.attrs; # varying value types
+      description = "Configuration structure (unchecked!)";
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.irccat = {
+      inherit description;
+      preStart = "${configMerge}";
+      script = "${config.depot.third_party.irccat}/bin/irccat";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "irccat";
+        WorkingDirectory = "/var/lib/irccat";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/ops/nixos/monorepo-gerrit.nix b/ops/nixos/monorepo-gerrit.nix
new file mode 100644
index 000000000000..eda64766f4b2
--- /dev/null
+++ b/ops/nixos/monorepo-gerrit.nix
@@ -0,0 +1,123 @@
+# Gerrit configuration for the TVL monorepo
+{ pkgs, config, lib, ... }:
+
+let
+  cfg = config.services.gerrit;
+  gerritHooks = pkgs.runCommandNoCC "gerrit-hooks" {} ''
+    mkdir -p $out
+    ln -s ${config.depot.ops.besadii}/bin/besadii $out/ref-updated
+  '';
+in {
+  services.gerrit = {
+    enable = true;
+    listenAddress = "[::]:4778"; # 4778 - grrt
+    serverId = "4fdfa107-4df9-4596-8e0a-1d2bbdd96e36";
+    builtinPlugins = [
+      "download-commands"
+      "hooks"
+    ];
+
+    plugins = with config.depot.third_party.gerrit_plugins; [
+      checks
+      owners
+    ];
+
+    package = config.depot.third_party.gerrit;
+
+    jvmHeapLimit = "4g";
+
+    settings = {
+      core.packedGitLimit = "100m";
+      log.jsonLogging = true;
+      log.textLogging = false;
+      sshd.advertisedAddress = "code.tvl.fyi:29418";
+      hooks.path = "${gerritHooks}";
+      cache.web_sessions.maxAge = "3 months";
+      plugins.allowRemoteAdmin = false;
+      change.enableAttentionSet = true;
+      change.enableAssignee = false;
+
+      # Configures gerrit for being reverse-proxied by nginx as per
+      # https://gerrit-review.googlesource.com/Documentation/config-reverseproxy.html
+      gerrit = {
+        canonicalWebUrl = "https://cl.tvl.fyi";
+        docUrl = "/Documentation";
+      };
+
+      httpd.listenUrl = "proxy-https://${cfg.listenAddress}";
+
+      download.command = [
+        "checkout"
+        "cherry_pick"
+        "format_patch"
+        "pull"
+      ];
+
+      # Configure for Sourcegraph.
+      gitweb = {
+        type = "custom";
+        url = "https://cs.tvl.fyi";
+        linkname = "Sourcegraph";
+        project = "/depot";
+        revision = "/depot/-/commit/\${commit}";
+        branch = "/depot@\${branch}";
+        tag = "/depot@\${tag}";
+        roottree = "/depot@\${commit}";
+        file = "/depot@\${commit}/-/blob/\${file}";
+        filehistory = "/depot@\${commit}/-/blob/\${file}#&tab=history";
+      };
+
+      # Auto-link panettone bug links
+      commentlink.panettone = {
+        match = "b/(\\\\d+)";
+        html = "<a href=\"https://b.tvl.fyi/issues/$1\">b/$1</a>";
+      };
+
+      # Configures integration with the locally running OpenLDAP
+      auth.type = "LDAP";
+      ldap = {
+        server = "ldap://localhost";
+        accountBase = "ou=users,dc=tvl,dc=fyi";
+        accountPattern = "(&(objectClass=organizationalPerson)(cn=\${username}))";
+        accountFullName = "displayName";
+        accountEmailAddress = "mail";
+        accountSshUserName = "cn";
+        groupBase = "ou=groups,dc=tvl,dc=fyi";
+
+        # TODO(tazjin): Assuming this is what we'll be doing ...
+        groupMemberPattern = "(&(objectClass=group)(member=\${dn}))";
+      };
+
+      # Email sending (emails are relayed via the tazj.in domain's
+      # GSuite currently).
+      #
+      # Note that sendemail.smtpPass is stored in
+      # $site_path/etc/secure.config and is *not* controlled by Nix.
+      #
+      # Receiving email is not currently supported.
+      sendemail = {
+        enable = true;
+        html = false;
+        connectTimeout = "10sec";
+        from = "TVL Code Review <tvlbot@tazj.in>";
+        includeDiff = true;
+        smtpEncryption = "none";
+        smtpServer = "localhost";
+        smtpServerPort = 2525;
+      };
+    };
+  };
+
+  systemd.services.gerrit = {
+    serviceConfig = {
+      # There seems to be no easy way to get `DynamicUser` to play
+      # well with other services (e.g. by using SupplementaryGroups,
+      # which seem to have no effect) so we force the DynamicUser
+      # setting for the Gerrit service to be disabled and reuse the
+      # existing 'git' user.
+      DynamicUser = lib.mkForce false;
+      User = "git";
+      Group = "git";
+    };
+  };
+}
diff --git a/ops/nixos/panettone.nix b/ops/nixos/panettone.nix
new file mode 100644
index 000000000000..50826743577d
--- /dev/null
+++ b/ops/nixos/panettone.nix
@@ -0,0 +1,104 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.panettone;
+  depot = config.depot;
+in {
+  options.services.depot.panettone = with lib; {
+    enable = mkEnableOption "Panettone issue tracker";
+
+    port = mkOption {
+      description = "Port on which Panettone should listen";
+      type = types.int;
+      default = 7268;
+    };
+
+    dbHost = mkOption {
+      description = "Postgresql host to connect to for Panettone";
+      type = types.str;
+      default = "localhost";
+    };
+
+    dbName = mkOption {
+      description = "Name of the database for Panettone";
+      type = types.str;
+      default = "panettone";
+    };
+
+    dbUser = mkOption {
+      description = "Name of the database user for Panettone";
+      type = types.str;
+      default = "panettone";
+    };
+
+    secretsFile = mkOption {
+      description = ''
+        Path to a file containing secrets, in the format accepted
+        by systemd's EnvironmentFile
+      '';
+      type = types.str;
+    };
+
+    irccatHost = mkOption {
+      description = "Hostname for the irccat instance";
+      type = types.str;
+      default = "localhost";
+    };
+
+    irccatPort = mkOption {
+      description = "Port for the irccat instance";
+      type = types.int;
+      default = 4722;
+    };
+
+    irccatChannel = mkOption {
+      description = "IRC channels to post to via irccat";
+      type = types.str;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    assertions = [{
+      assertion =
+        cfg.dbHost != "localhost" || config.services.postgresql.enable;
+      message = "Panettone requires a postgresql database";
+    } {
+      assertion =
+        cfg.dbHost != "localhost" || config.services.postgresql.enableTCPIP;
+      message = "Panettone can only connect to the postgresql database over TCP";
+    } {
+      assertion =
+        cfg.dbHost != "localhost" || (lib.any
+          (user: user.name == cfg.dbUser)
+          config.services.postgresql.ensureUsers);
+      message = "Panettone requires a database user";
+    } {
+      assertion =
+        cfg.dbHost != "localhost" || (lib.any
+          (db: db == cfg.dbName)
+          config.services.postgresql.ensureDatabases);
+      message = "Panettone requires a database";
+    }];
+
+    systemd.services.panettone = {
+      wantedBy = [ "multi-user.target" ];
+      script = "${depot.web.panettone}/bin/panettone";
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        EnvironmentFile = cfg.secretsFile;
+      };
+
+      environment = {
+        PANETTONE_PORT = toString cfg.port;
+        PGHOST = "localhost";
+        PGUSER = cfg.dbUser;
+        PGDATABASE = cfg.dbName;
+        IRCCATHOST = cfg.irccatHost;
+        IRCCATPORT = toString cfg.irccatPort;
+        ISSUECHANNEL = cfg.irccatChannel;
+      };
+    };
+  };
+}
diff --git a/ops/nixos/paroxysm.nix b/ops/nixos/paroxysm.nix
new file mode 100644
index 000000000000..24c5377a577a
--- /dev/null
+++ b/ops/nixos/paroxysm.nix
@@ -0,0 +1,27 @@
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.paroxysm;
+  description = "TVL's majestic IRC bot";
+in {
+  options.services.depot.paroxysm.enable = lib.mkEnableOption description;
+
+  config = lib.mkIf cfg.enable {
+    systemd.services.paroxysm = {
+      inherit description;
+      script = "${config.depot.fun.paroxysm}/bin/paroxysm";
+      wantedBy = [ "multi-user.target" ];
+
+      environment = {
+        PARX_DATABASE_URL = "postgresql://tvldb:tvldb@localhost/tvldb";
+        PARX_IRC_CONFIG_PATH = "/var/lib/paroxysm/irc.toml";
+      };
+
+      serviceConfig = {
+        DynamicUser = true;
+        StateDirectory = "paroxysm";
+        Restart = "always";
+      };
+    };
+  };
+}
diff --git a/ops/nixos/quassel.nix b/ops/nixos/quassel.nix
new file mode 100644
index 000000000000..df26a3945532
--- /dev/null
+++ b/ops/nixos/quassel.nix
@@ -0,0 +1,76 @@
+# A more modern module for running Quassel.
+{ config, lib, pkgs, ... }:
+
+let
+  cfg = config.services.depot.quassel;
+  quasselDaemon = pkgs.quassel.override {
+    monolithic = false;
+    enableDaemon = true;
+    withKDE = false;
+  };
+in {
+  options.services.depot.quassel = with lib; {
+    enable = mkEnableOption "Quassel IRC daemon";
+
+    acmeHost = mkOption {
+      description = "ACME host to use for the Quassel TLS certificate";
+      type = lib.types.str;
+    };
+
+    bindAddresses = mkOption {
+      description = "Addresses Quassel will bind to/listen on";
+      default = [ "127.0.0.1" ];
+    };
+
+    logLevel = mkOption {
+      description = "Log level for Quassel Core";
+      default = "Info";
+      type = lib.types.enum [
+        "Debug"
+        "Info"
+        "Warning"
+        "Error"
+      ];
+    };
+
+    port = mkOption {
+      default = 6698;
+      description = ''
+        The port number the Quassel daemon will be listening to.
+      '';
+    };
+  };
+
+  config = with lib; mkIf cfg.enable {
+    systemd.services.quassel = {
+      description = "Quassel IRC daemon";
+      wantedBy = [ "multi-user.target" ];
+
+      script = concatStringsSep " " [
+        "${quasselDaemon}/bin/quasselcore"
+        "--listen=${concatStringsSep "," cfg.bindAddresses}"
+        "--port=${toString cfg.port}"
+        "--configdir=/var/lib/quassel"
+        "--require-ssl"
+        "--ssl-cert=/var/lib/acme/${cfg.acmeHost}/full.pem"
+        "--loglevel=${cfg.logLevel}"
+      ];
+
+      serviceConfig = {
+        Restart = "always";
+        User = "quassel";
+        Group = "quassel";
+        StateDirectory = "quassel";
+      };
+    };
+
+    users = {
+      users.quassel = {
+        isNormalUser = false;
+        group = "quassel";
+      };
+
+      groups.quassel = {};
+    };
+  };
+}
diff --git a/ops/nixos/smtprelay.nix b/ops/nixos/smtprelay.nix
new file mode 100644
index 000000000000..044902b15a9a
--- /dev/null
+++ b/ops/nixos/smtprelay.nix
@@ -0,0 +1,53 @@
+# NixOS module for configuring the simple SMTP relay.
+{ pkgs, config, lib, ... }:
+
+let
+  inherit (builtins) attrValues mapAttrs;
+  inherit (lib)
+    concatStringsSep
+    mkEnableOption
+    mkIf
+    mkOption
+    types
+;
+
+  cfg = config.services.depot.smtprelay;
+  description = "Simple SMTP relay";
+
+  # Configuration values that are always overridden. In particular,
+  # `config` is specified to always load $StateDirectory/secure.config
+  # (so that passwords can be loaded from there) and logging is pinned
+  # to stdout for journald compatibility.
+  overrideArgs = {
+    logfile = "";
+    config = "/var/lib/smtprelay/secure.config";
+  };
+
+  # Creates the command line argument string for the service.
+  prepareArgs = args:
+    concatStringsSep " "
+      (attrValues (mapAttrs (key: value: "-${key} '${toString value}'")
+                            (args // overrideArgs)));
+in {
+  options.services.depot.smtprelay = {
+    enable = mkEnableOption description;
+    args = mkOption {
+      type = types.attrsOf types.str;
+      description = "Key value pairs for command line arguments";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.smtprelay = {
+      inherit description;
+      script = "${config.depot.third_party.smtprelay}/bin/smtprelay ${prepareArgs cfg.args}";
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        Restart = "always";
+        StateDirectory = "smtprelay";
+        DynamicUser = true;
+      };
+    };
+  };
+}
diff --git a/ops/nixos/sourcegraph.nix b/ops/nixos/sourcegraph.nix
new file mode 100644
index 000000000000..43dc275ee148
--- /dev/null
+++ b/ops/nixos/sourcegraph.nix
@@ -0,0 +1,52 @@
+# Run sourcegraph, including its entire machinery, in a container.
+# Running it outside of a container is a futile endeavour for now.
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.depot.sourcegraph;
+  depot = config.depot;
+in {
+  options.services.depot.sourcegraph = with lib; {
+    enable = mkEnableOption "SourceGraph code search engine";
+
+    port = mkOption {
+      description = "Port on which SourceGraph should listen";
+      type = types.int;
+      default = 3463;
+    };
+
+    cheddarPort = mkOption {
+      description = "Port on which cheddar should listen";
+      type = types.int;
+      default = 4238;
+    };
+  };
+
+  config = lib.mkIf cfg.enable {
+    # Run a cheddar syntax highlighting server
+    systemd.services.cheddar-server = {
+      wantedBy = [ "multi-user.target" ];
+      script = "${depot.tools.cheddar}/bin/cheddar --listen 0.0.0.0:${toString cfg.cheddarPort} --sourcegraph-server";
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+      };
+    };
+
+    virtualisation.oci-containers.containers.sourcegraph = {
+      image = "sourcegraph/server:3.18.0";
+
+      ports = [
+        "127.0.0.1:${toString cfg.port}:7080"
+      ];
+
+      volumes = [
+        "/var/lib/sourcegraph/etc:/etc/sourcegraph"
+        "/var/lib/sourcegraph/data:/var/opt/sourcegraph"
+      ];
+
+      environment.SRC_SYNTECT_SERVER = "http://172.17.0.1:${toString cfg.cheddarPort}";
+    };
+  };
+}
diff --git a/ops/nixos/tvl-slapd/default.nix b/ops/nixos/tvl-slapd/default.nix
new file mode 100644
index 000000000000..d32bc96b832d
--- /dev/null
+++ b/ops/nixos/tvl-slapd/default.nix
@@ -0,0 +1,89 @@
+# Configures an OpenLDAP instance for TVL
+#
+# TODO(tazjin): Configure ldaps://
+{ config, lib, pkgs, ... }:
+
+with config.depot.nix.yants;
+
+let
+  user = struct {
+    username = string;
+    email = string;
+    password = string;
+    displayName = option string;
+  };
+
+  toLdif = defun [ user string ] (u: ''
+    dn: cn=${u.username},ou=users,dc=tvl,dc=fyi
+    objectClass: organizationalPerson
+    objectClass: inetOrgPerson
+    sn: ${u.username}
+    cn: ${u.username}
+    displayName: ${u.displayName or u.username}
+    mail: ${u.email}
+    userPassword: ${u.password}
+  '');
+
+  inherit (config.depot.ops) users;
+
+in {
+  # Use our patched OpenLDAP derivation which enables stronger password hashing.
+  #
+  # Unfortunately the module for OpenLDAP has no package option, so we
+  # need to override it system-wide. Be aware that this triggers a
+  # *large* number of rebuilds of packages such as GPG and Python.
+  nixpkgs.overlays = [
+    (_: _: {
+      inherit (config.depot.third_party) openldap;
+    })
+  ];
+
+  services.openldap = {
+    enable = true;
+    dataDir = "/var/lib/openldap";
+    database = "mdb";
+    suffix = "dc=tvl,dc=fyi";
+    rootdn = "cn=admin,dc=tvl,dc=fyi";
+    rootpw = "{ARGON2}$argon2id$v=19$m=65536,t=2,p=1$OfcgkOQ96VQ3aJj7NfA9vQ$oS6HQOkYl/bUYg4SejpltQYy7kvqx/RUxvoR4zo1vXU";
+
+    settings.children = {
+      "olcDatabase={1}mdb".attrs = {
+        objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
+        olcDatabase = "{1}mdb";
+        olcSuffix = "dc=tvl,dc=fyi";
+        olcAccess = "to *  by * read";
+      };
+
+      "cn=module{0}".attrs = {
+        objectClass = "olcModuleList";
+        olcModuleLoad = "pw-argon2";
+      };
+    };
+
+    # Contents are immutable at runtime, and adding user accounts etc.
+    # is done statically in the LDIF-formatted contents in this folder.
+    declarativeContents."dc=tvl,dc=fyi" = ''
+      dn: dc=tvl,dc=fyi
+      dc: tvl
+      o: TVL LDAP server
+      description: Root entry for tvl.fyi
+      objectClass: top
+      objectClass: dcObject
+      objectClass: organization
+
+      dn: ou=users,dc=tvl,dc=fyi
+      ou: users
+      description: All users in TVL
+      objectClass: top
+      objectClass: organizationalUnit
+
+      dn: ou=groups,dc=tvl,dc=fyi
+      ou: groups
+      description: All groups in TVL
+      objectClass: top
+      objectClass: organizationalUnit
+
+      ${lib.concatStringsSep "\n" (map toLdif users)}
+    '';
+  };
+}
diff --git a/ops/nixos/tvl-sso/default.nix b/ops/nixos/tvl-sso/default.nix
new file mode 100644
index 000000000000..8590918e575a
--- /dev/null
+++ b/ops/nixos/tvl-sso/default.nix
@@ -0,0 +1,24 @@
+# Configures an Apereo CAS instance for TVL SSO
+{ config, ... }:
+
+let
+  inherit (config.depot.third_party) apereo-cas;
+in {
+  config = {
+    environment.systemPackages = [ apereo-cas ];
+    systemd.services.apereo-cas = {
+      description = "Apereo CAS Single Sign On server";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      serviceConfig = {
+        User = "apereo-cas";
+        Group = "apereo-cas";
+        ExecStart = "${apereo-cas}/bin/cas";
+        EnvironmentFile = "/etc/cas/secrets";
+        Restart = "always";
+      };
+    };
+    users.users.apereo-cas = {};
+    users.groups.apereo-cas = {};
+  };
+}
diff --git a/ops/nixos/v4l2loopback.nix b/ops/nixos/v4l2loopback.nix
new file mode 100644
index 000000000000..636b2ff6cf27
--- /dev/null
+++ b/ops/nixos/v4l2loopback.nix
@@ -0,0 +1,12 @@
+{ config, lib, pkgs, ... }:
+
+{
+  boot = {
+    extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
+    kernelModules = [ "v4l2loopback" ];
+    extraModprobeConfig = ''
+      options v4l2loopback exclusive_caps=1
+    '';
+  };
+}
+
diff --git a/ops/nixos/whitby/OWNERS b/ops/nixos/whitby/OWNERS
new file mode 100644
index 000000000000..b1b749e871e1
--- /dev/null
+++ b/ops/nixos/whitby/OWNERS
@@ -0,0 +1,6 @@
+inherited: false
+
+# Want in on this list? Try paying!
+owners:
+  - lukegb
+  - tazjin
diff --git a/ops/nixos/whitby/README.md b/ops/nixos/whitby/README.md
new file mode 100644
index 000000000000..55287c541256
--- /dev/null
+++ b/ops/nixos/whitby/README.md
@@ -0,0 +1,5 @@
+whitby
+======
+
+`whitby.tvl.fyi` is our dedicated server providing continuous
+integration services and other random nonsense.
diff --git a/ops/nixos/whitby/default.nix b/ops/nixos/whitby/default.nix
new file mode 100644
index 000000000000..e839b8d077c4
--- /dev/null
+++ b/ops/nixos/whitby/default.nix
@@ -0,0 +1,456 @@
+{ depot, lib, ... }:
+
+let
+  inherit (builtins) listToAttrs;
+  inherit (lib) range;
+
+  nixpkgs = import depot.third_party.nixpkgsSrc {};
+
+  # All Buildkite hooks are actually besadii, but it's being invoked
+  # with different names.
+  buildkiteHooks = depot.third_party.runCommandNoCC "buildkite-hooks" {} ''
+    mkdir -p $out/bin
+    ln -s ${depot.ops.besadii}/bin/besadii $out/bin/post-command
+  '';
+in lib.fix(self: {
+  inherit depot;
+  imports = [
+    "${depot.depotPath}/ops/nixos/clbot.nix"
+    "${depot.depotPath}/ops/nixos/depot.nix"
+    "${depot.depotPath}/ops/nixos/irccat.nix"
+    "${depot.depotPath}/ops/nixos/monorepo-gerrit.nix"
+    "${depot.depotPath}/ops/nixos/panettone.nix"
+    "${depot.depotPath}/ops/nixos/paroxysm.nix"
+    "${depot.depotPath}/ops/nixos/smtprelay.nix"
+    "${depot.depotPath}/ops/nixos/sourcegraph.nix"
+    "${depot.depotPath}/ops/nixos/tvl-slapd/default.nix"
+    "${depot.depotPath}/ops/nixos/tvl-sso/default.nix"
+    "${depot.depotPath}/ops/nixos/www/b.tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/cl.tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/code.tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/cs.tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/login.tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/tazj.in.nix"
+    "${depot.depotPath}/ops/nixos/www/todo.tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/tvl.fyi.nix"
+    "${depot.depotPath}/ops/nixos/www/wigglydonke.rs.nix"
+    "${depot.third_party.nixpkgsSrc}/nixos/modules/services/web-apps/gerrit.nix"
+  ];
+
+  hardware = {
+    enableRedistributableFirmware = true;
+    cpu.amd.updateMicrocode = true;
+  };
+
+  boot = {
+    tmpOnTmpfs = true;
+    kernelModules = [ "kvm-amd" ];
+    supportedFilesystems = [ "zfs" ];
+
+    initrd = {
+      availableKernelModules = [
+        "igb" "xhci_pci" "nvme" "ahci" "usbhid" "usb_storage" "sr_mod"
+      ];
+
+      # Enable SSH in the initrd so that we can enter disk encryption
+      # passwords remotely.
+      network = {
+        enable = true;
+        ssh = {
+          enable = true;
+          port = 2222;
+          authorizedKeys =
+            depot.users.tazjin.keys.all
+            ++ depot.users.lukegb.keys.all
+            ++ [ depot.users.glittershark.keys.whitby ];
+
+          hostKeys = [
+            /etc/secrets/initrd_host_ed25519_key
+          ];
+        };
+
+        # this will launch the zfs password prompt on login and kill the
+        # other prompt
+        postCommands = ''
+          echo "zfs load-key -a && killall zfs" >> /root/.profile
+        '';
+      };
+    };
+
+    loader.grub = {
+      enable = true;
+      version = 2;
+      efiSupport = true;
+      efiInstallAsRemovable = true;
+      device = "/dev/disk/by-id/nvme-SAMSUNG_MZQLB1T9HAJR-00007_S439NA0N201620";
+    };
+
+    zfs.requestEncryptionCredentials = true;
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "zroot/root";
+      fsType = "zfs";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/073E-7FBD";
+      fsType = "vfat";
+    };
+
+    "/nix" = {
+      device = "zroot/nix";
+      fsType = "zfs";
+    };
+
+    "/home" = {
+      device = "zroot/home";
+      fsType = "zfs";
+    };
+  };
+
+  networking = {
+    # Glass is boring, but Luke doesn't like Wapping - the Prospect of
+    # Whitby, however, is quite a pleasant establishment.
+    hostName = "whitby";
+    domain = "tvl.fyi";
+    hostId = "b38ca543";
+    useDHCP = false;
+
+    # Don't use Hetzner's DNS servers.
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+
+    defaultGateway6 = {
+      address = "fe80::1";
+      interface = "enp196s0";
+    };
+
+    firewall.allowedTCPPorts = [ 22 80 443 4238 29418 ];
+
+    interfaces.enp196s0.useDHCP = true;
+    interfaces.enp196s0.ipv6.addresses = [
+      {
+        address = "2a01:04f8:0242:5b21::feed:edef:beef";
+        prefixLength = 64;
+      }
+    ];
+  };
+
+  # Generate an immutable /etc/resolv.conf from the nameserver settings
+  # above (otherwise DHCP overwrites it):
+  environment.etc."resolv.conf" = with lib; {
+    source = depot.third_party.writeText "resolv.conf" ''
+      ${concatStringsSep "\n" (map (ns: "nameserver ${ns}") self.networking.nameservers)}
+      options edns0
+    '';
+  };
+
+  # Disable background git gc system-wide, as it has a tendency to break CI.
+  environment.etc."gitconfig".source = depot.third_party.writeText "gitconfig" ''
+    [gc]
+    autoDetach = false
+  '';
+
+  time.timeZone = "UTC";
+
+  nix = {
+    nrBuildUsers = 256;
+    maxJobs = lib.mkDefault 64;
+    extraOptions = ''
+      secret-key-files = /etc/secrets/nix-cache-privkey
+    '';
+
+    trustedUsers = [
+      "grfn"
+      "lukegb"
+      "tazjin"
+    ];
+
+    sshServe = {
+      enable = true;
+      keys = with depot.users;
+        tazjin.keys.all
+        ++ lukegb.keys.all
+        ++ [ glittershark.keys.whitby ];
+    };
+  };
+
+  programs.mtr.enable = true;
+  programs.mosh.enable = true;
+  services.openssh = {
+    enable = true;
+    passwordAuthentication = false;
+    challengeResponseAuthentication = false;
+  };
+
+  # Run a handful of Buildkite agents to support parallel builds.
+  services.buildkite-agents = listToAttrs (map (n: rec {
+    name = "whitby-${toString n}";
+    value = {
+      inherit name;
+      enable = true;
+      tokenPath = "/etc/secrets/buildkite-agent-token";
+      hooks.post-command = "${buildkiteHooks}/bin/post-command";
+    };
+  }) (range 1 32));
+
+  # Start a local SMTP relay to Gmail (used by gerrit)
+  services.depot.smtprelay = {
+    enable = true;
+    args = {
+      listen = ":2525";
+      remote_host = "smtp.gmail.com:587";
+      remote_auth = "plain";
+      remote_user = "tvlbot@tazj.in";
+    };
+  };
+
+  # Start the Gerrit->IRC bot
+  services.depot.clbot = {
+    enable = true;
+
+    # Almost all configuration values are already correct (well, duh),
+    # see //fun/clbot for details.
+    flags = {
+      gerrit_host = "cl.tvl.fyi:29418";
+      gerrit_ssh_auth_username = "clbot";
+      gerrit_ssh_auth_key = "/etc/secrets/clbot-key";
+      irc_server = "znc.lukegb.com:6697";
+
+      notify_branches = "canon,refs/meta/config";
+      notify_repo = "depot";
+
+      # This secret is read from an environment variable, which is
+      # populated from /etc/secrets/clbot
+      irc_pass = "$CLBOT_PASS";
+    };
+
+    channels = [
+      "##tvl"
+      "##tvl-dev"
+    ];
+  };
+
+  services.depot = {
+    # Run a SourceGraph code search instance
+    sourcegraph.enable = true;
+
+    # Run the Panettone issue tracker
+    panettone = {
+      enable = true;
+      dbUser = "panettone";
+      dbName = "panettone";
+      secretsFile = "/etc/secrets/panettone";
+      irccatChannel = "##tvl,##tvl-dev";
+    };
+
+    # Run the first cursed bot (quote bot)
+    paroxysm.enable = true;
+
+    # Run irccat to forward messages to IRC
+    irccat = {
+      enable = true;
+      config = {
+        tcp.listen = ":4722"; # "ircc"
+        irc = {
+          server = "chat.freenode.net:6697";
+          tls = true;
+          nick = "tvlbot";
+          realname = "TVL Bot";
+          channels = [
+            "##tvl"
+            "##tvl-dev"
+          ];
+        };
+      };
+    };
+  };
+
+  services.postgresql = {
+    enable = true;
+    enableTCPIP = true;
+
+    authentication = lib.mkForce ''
+      local all all trust
+      host all all 127.0.0.1/32 password
+      host all all ::1/128 password
+      hostnossl all all 127.0.0.1/32 password
+      hostnossl all all ::1/128  password
+    '';
+
+    ensureDatabases = [
+      "panettone"
+    ];
+
+    ensureUsers = [{
+      name = "panettone";
+      ensurePermissions = {
+        "DATABASE panettone" = "ALL PRIVILEGES";
+      };
+    }];
+  };
+
+  services.postgresqlBackup = {
+    enable = true;
+    databases = [
+      "tvldb"
+      "panettone"
+    ];
+  };
+
+  environment.systemPackages = with nixpkgs; [
+    bb
+    curl
+    emacs-nox
+    git
+    htop
+    nano
+    rxvt_unicode.terminfo
+    vim
+    zfs
+    zfstools
+  ];
+
+  # Run cgit for the depot. The onion here is nginx(thttpd(cgit)).
+  systemd.services.cgit = {
+    wantedBy = [ "multi-user.target" ];
+    script = "${depot.web.cgit-taz}/bin/cgit-launch";
+
+    serviceConfig = {
+      Restart = "on-failure";
+      User = "git";
+      Group = "git";
+    };
+  };
+
+  # Regularly back up whitby to Google Cloud Storage.
+  systemd.services.restic = {
+    description = "Backups to Google Cloud Storage";
+    script = "${nixpkgs.restic}/bin/restic backup /var/lib/gerrit /var/backup/postgresql";
+
+    environment = {
+      GOOGLE_PROJECT_ID = "tazjins-infrastructure";
+      GOOGLE_APPLICATION_CREDENTIALS = "/var/backup/restic/gcp-key.json";
+      RESTIC_REPOSITORY = "gs:tvl-fyi-backups:/whitby";
+      RESTIC_PASSWORD_FILE = "/var/backup/restic/secret";
+      RESTIC_CACHE_DIR = "/var/backup/restic/cache";
+      RESTIC_EXCLUDE_FILE = builtins.toFile "exclude-files" ''
+        /var/lib/gerrit/tmp
+      '';
+    };
+  };
+
+  systemd.timers.restic = {
+    wantedBy = [ "multi-user.target" ];
+    timerConfig.OnCalendar = "hourly";
+  };
+
+  services.journaldriver = {
+    enable = true;
+    googleCloudProject = "tvl-fyi";
+    logStream = "whitby";
+    applicationCredentials = "/var/lib/journaldriver/key.json";
+  };
+
+  security.sudo.extraRules = [
+    {
+      groups = ["wheel"];
+      commands = [{ command = "ALL"; options = ["NOPASSWD"]; }];
+    }
+  ];
+
+  users = {
+    users.root.openssh.authorizedKeys.keys = [
+      depot.users.tazjin.keys.frog
+    ];
+
+    users.tazjin = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      shell = nixpkgs.fish;
+      openssh.authorizedKeys.keys = depot.users.tazjin.keys.all;
+    };
+
+    users.lukegb = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      openssh.authorizedKeys.keys = depot.users.lukegb.keys.all;
+    };
+
+    users.grfn = {
+      isNormalUser = true;
+      extraGroups = [ "git" "wheel" ];
+      openssh.authorizedKeys.keys = [
+        depot.users.glittershark.keys.whitby
+      ];
+    };
+
+    users.isomer = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.isomer.keys.all;
+    };
+
+    users.riking = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.riking.keys.u2f ++ depot.users.riking.keys.passworded;
+    };
+
+    users.edef = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.edef.keys.all;
+    };
+
+    users.qyliss = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.qyliss.keys.all;
+    };
+
+    users.eta = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.eta.keys.whitby;
+    };
+
+    users.cynthia = {
+      isNormalUser = true; # I'm normal OwO :3
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.cynthia.keys.all;
+    };
+
+    users.firefly = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.firefly.keys.whitby;
+    };
+
+    users.sterni = {
+      isNormalUser = true;
+      extraGroups = [ "git" ];
+      openssh.authorizedKeys.keys = depot.users.sterni.keys.all;
+    };
+
+    # Set up a user & group for git shenanigans
+    groups.git = {};
+    users.git = {
+      group = "git";
+      isNormalUser = false;
+      createHome = true;
+      home = "/var/lib/git";
+    };
+  };
+
+  security.acme = {
+    acceptTerms = true;
+    email = "certs@tvl.fyi";
+  };
+
+  system.stateVersion = "20.03";
+})
diff --git a/ops/nixos/www/b.tvl.fyi.nix b/ops/nixos/www/b.tvl.fyi.nix
new file mode 100644
index 000000000000..3d8a4068aa11
--- /dev/null
+++ b/ops/nixos/www/b.tvl.fyi.nix
@@ -0,0 +1,21 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."b.tvl.fyi" = {
+      serverName = "b.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:${toString config.services.depot.panettone.port};
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/base.nix b/ops/nixos/www/base.nix
new file mode 100644
index 000000000000..4b956cd95ef1
--- /dev/null
+++ b/ops/nixos/www/base.nix
@@ -0,0 +1,36 @@
+{ config, pkgs, ... }:
+
+{
+  config = {
+    services.nginx = {
+      enable = true;
+      enableReload = true;
+
+      recommendedTlsSettings = true;
+      recommendedGzipSettings = true;
+      recommendedProxySettings = true;
+    };
+
+    # NixOS 20.03 broke nginx and I can't be bothered to debug it
+    # anymore, all solution attempts have failed, so here's a
+    # brute-force fix.
+    #
+    # TODO(tazjin): Find a link to the upstream issue and see if
+    # they've sorted it after ~20.09
+    systemd.services.fix-nginx = {
+      script = "${pkgs.coreutils}/bin/chown -f -R nginx: /var/spool/nginx /var/cache/nginx";
+
+      serviceConfig = {
+        User = "root";
+        Type = "oneshot";
+      };
+    };
+
+    systemd.timers.fix-nginx = {
+      wantedBy = [ "multi-user.target" ];
+      timerConfig = {
+        OnCalendar = "minutely";
+      };
+    };
+  };
+}
diff --git a/ops/nixos/www/cl.tvl.fyi.nix b/ops/nixos/www/cl.tvl.fyi.nix
new file mode 100644
index 000000000000..bcaab85f0227
--- /dev/null
+++ b/ops/nixos/www/cl.tvl.fyi.nix
@@ -0,0 +1,24 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts.gerrit = {
+      serverName = "cl.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:4778;
+          proxy_set_header  X-Forwarded-For $remote_addr;
+          # The :443 suffix is a workaround for https://b.tvl.fyi/issues/88.
+          proxy_set_header  Host $host:443;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/code.tvl.fyi.nix b/ops/nixos/www/code.tvl.fyi.nix
new file mode 100644
index 000000000000..5ee33f39ca92
--- /dev/null
+++ b/ops/nixos/www/code.tvl.fyi.nix
@@ -0,0 +1,27 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts.cgit = {
+      serverName = "code.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        # Static assets must always hit the root.
+        location ~ ^/(favicon\.ico|cgit\.(css|png))$ {
+           proxy_pass http://localhost:2448;
+        }
+
+        # Everything else hits the depot directly.
+        location / {
+            proxy_pass http://localhost:2448/cgit.cgi/depot/;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/cs.tvl.fyi.nix b/ops/nixos/www/cs.tvl.fyi.nix
new file mode 100644
index 000000000000..ed2adcbf8294
--- /dev/null
+++ b/ops/nixos/www/cs.tvl.fyi.nix
@@ -0,0 +1,30 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."cs.tvl.fyi" = {
+      serverName = "cs.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location = / {
+          return 301 https://cs.tvl.fyi/depot;
+        }
+
+        location / {
+          proxy_set_header X-Sg-Auth "Anonymous";
+          proxy_pass http://localhost:${toString config.services.depot.sourcegraph.port};
+        }
+
+        location /users/Anonymous/settings {
+          return 301 https://cs.tvl.fyi;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/login.tvl.fyi.nix b/ops/nixos/www/login.tvl.fyi.nix
new file mode 100644
index 000000000000..05b7cee25338
--- /dev/null
+++ b/ops/nixos/www/login.tvl.fyi.nix
@@ -0,0 +1,24 @@
+{ ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."login.tvl.fyi" = {
+      serverName = "login.tvl.fyi";
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        location / {
+          proxy_pass http://localhost:8443;
+          proxy_set_header X-Forwarded-For $remote_addr;
+          proxy_set_header X-Forwarded-Proto https;
+          proxy_set_header Host $host;
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/tazj.in.nix b/ops/nixos/www/tazj.in.nix
new file mode 100644
index 000000000000..c33c9560deac
--- /dev/null
+++ b/ops/nixos/www/tazj.in.nix
@@ -0,0 +1,36 @@
+# serve tazjin's website & blog
+{ config, lib, pkgs, ... }:
+
+let depot = config.depot;
+in {
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tazj.in" = {
+      enableACME = true;
+      forceSSL = true;
+      root = depot.users.tazjin.homepage;
+
+      extraConfig = ''
+        ${depot.users.tazjin.blog.oldRedirects}
+        location /blog/ {
+          alias ${depot.users.tazjin.blog.rendered}/;
+
+          if ($request_uri ~ ^/(.*)\.html$) {
+            return 302 /$1;
+          }
+
+          try_files $uri $uri.html $uri/ =404;
+        }
+      '';
+    };
+
+    services.nginx.virtualHosts."git.tazj.in" = {
+      enableACME = true;
+      forceSSL = true;
+      extraConfig = "return 301 https://code.tvl.fyi$request_uri;";
+    };
+  };
+}
diff --git a/ops/nixos/www/todo.tvl.fyi.nix b/ops/nixos/www/todo.tvl.fyi.nix
new file mode 100644
index 000000000000..0820d136d295
--- /dev/null
+++ b/ops/nixos/www/todo.tvl.fyi.nix
@@ -0,0 +1,24 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."todo.tvl.fyi" = {
+      serverName = "todo.tvl.fyi";
+      root = config.depot.web.todolist;
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+        location ~* \.(webp|woff2)$ {
+          add_header Cache-Control "public, max-age=31536000";
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/tvl.fyi.nix b/ops/nixos/www/tvl.fyi.nix
new file mode 100644
index 000000000000..9c2bf0274f23
--- /dev/null
+++ b/ops/nixos/www/tvl.fyi.nix
@@ -0,0 +1,30 @@
+{ config, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."tvl.fyi" = {
+      serverName = "tvl.fyi";
+      root = config.depot.web.tvl;
+      enableACME = true;
+      forceSSL = true;
+
+      extraConfig = ''
+        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+
+        rewrite ^/builds/?$ https://buildkite.com/tvl/depot/ last;
+
+        rewrite ^/monorepo-doc/?$ https://docs.google.com/document/d/1nnyByXcH0F6GOmEezNOUa2RFelpeRpDToBLYD_CtjWE/edit?usp=sharing last;
+
+        rewrite ^/irc/?$ ircs://chat.freenode.net:6697/##tvl last;
+
+        location ~* \.(webp|woff2)$ {
+          add_header Cache-Control "public, max-age=31536000";
+        }
+      '';
+    };
+  };
+}
diff --git a/ops/nixos/www/wigglydonke.rs.nix b/ops/nixos/www/wigglydonke.rs.nix
new file mode 100644
index 000000000000..0774eaea7c70
--- /dev/null
+++ b/ops/nixos/www/wigglydonke.rs.nix
@@ -0,0 +1,15 @@
+{ config, lib, pkgs, ... }:
+
+{
+  imports = [
+    ./base.nix
+  ];
+
+  config = {
+    services.nginx.virtualHosts."wigglydonke.rs" = {
+      enableACME = true;
+      forceSSL = true;
+      root = "${config.depot.depotPath}/users/glittershark/wigglydonke.rs";
+    };
+  };
+}