summary refs log tree commit diff
path: root/fleet/hosts
diff options
context:
space:
mode:
authorV <v@unfathomable.blue>2021-06-09 15:43:16 +0200
committerV <v@unfathomable.blue>2021-08-17 03:09:34 +0200
commitec0965e2672899d25a5a3a8c072de3ea734076a2 (patch)
treeddf53e6cc5ae47fa1a925f7a7d6414ba03718a84 /fleet/hosts
parentdb7c54f92f386a94db8af7a12626d2657b4dd640 (diff)
fleet: init
Co-authored-by: edef <edef@unfathomable.blue>
Change-Id: I36d2c4cca542ed91630b1b832f3c7a7b97b33c65
Diffstat (limited to 'fleet/hosts')
-rw-r--r--fleet/hosts/trieste/build-from-git.nix16
-rw-r--r--fleet/hosts/trieste/cgit/default.nix107
-rw-r--r--fleet/hosts/trieste/cgit/ripple.svg8
-rw-r--r--fleet/hosts/trieste/cgit/un.svg6
-rw-r--r--fleet/hosts/trieste/cgit/unicon.svg6
-rw-r--r--fleet/hosts/trieste/default.nix52
-rw-r--r--fleet/hosts/trieste/git.nix47
-rw-r--r--fleet/hosts/trieste/lists.nix58
-rw-r--r--fleet/hosts/trieste/mail.nix14
-rw-r--r--fleet/hosts/trieste/web.nix32
-rw-r--r--fleet/hosts/vityaz/default.nix112
-rw-r--r--fleet/hosts/vityaz/git.nix67
-rw-r--r--fleet/hosts/vityaz/mail.nix58
-rw-r--r--fleet/hosts/vityaz/mumble.nix21
14 files changed, 604 insertions, 0 deletions
diff --git a/fleet/hosts/trieste/build-from-git.nix b/fleet/hosts/trieste/build-from-git.nix
new file mode 100644
index 0000000..f04ef48
--- /dev/null
+++ b/fleet/hosts/trieste/build-from-git.nix
@@ -0,0 +1,16 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ repo, pkgs ? import <nixpkgs> {} }:
+
+pkgs.callPackage (builtins.fetchGit {
+  url = repo;
+  # While Nix will happily just fetch from HEAD if you only pass in a
+  # path, it will also cache the result for an hour, making it totally
+  # unsuitable for what we're doing. lib.commitIdFromGitRepo, on the
+  # other hand, is implemented purely in Nix and does not cache lookups
+  # from one invocation to the next. This lets us "impurely" fetch from
+  # HEAD while enjoying the niceties of using builtins.fetchGit with a
+  # specific commit hash.
+  rev = pkgs.lib.commitIdFromGitRepo repo;
+}).outPath {}
diff --git a/fleet/hosts/trieste/cgit/default.nix b/fleet/hosts/trieste/cgit/default.nix
new file mode 100644
index 0000000..23e8ab6
--- /dev/null
+++ b/fleet/hosts/trieste/cgit/default.nix
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ lib, pkgs, ... }:
+
+with lib;
+
+let
+  cgit-webroot = pkgs.runCommand "cgit-webroot" {
+    extraStyles = ''
+      div#cgit table#header td.logo {
+        width: 64px;
+      }
+
+      #summary {
+        max-width: 72ch;
+        margin: auto;
+        font-size: initial;
+      }
+    '';
+    passAsFile = [ "extraStyles" ];
+  } ''
+    ${pkgs.minify}/bin/minify --type css ${pkgs.cgit}/cgit/cgit.css $extraStylesPath -o $out/cgit.css
+    cp ${./un.svg} $out/un.svg  # TODO(V): remove this variant, apply padding to the Sigil using CSS
+    cp ${./unicon.svg} $out/unicon.svg  # This is the same as un.svg, but without any padding
+    cp ${./ripple.svg} $out/ripple.svg  # This is referenced in git.nix (as config.cgit.logo, for Ripple)
+    cp ${pkgs.cgit}/cgit/robots.txt $out
+  '';
+
+  cgit-about-filter = pkgs.writeShellScript "cgit-about-filter" ''
+    # Asciidoctor's embedded mode defaults to eliding the top-level heading, for some reason.
+    # Fortunately we can change this behaviour using the showtitle attribute.
+    # See also: https://github.com/asciidoctor/asciidoctor/issues/1149
+    ${pkgs.asciidoctor}/bin/asciidoctor -e -a showtitle -
+  '';
+
+  cgit-config = pkgs.writeText "cgit-config" ''
+    # TODO(V): sort these sanely
+    root-title=unfathomable software
+    root-desc=
+    # TODO(V): root-readme? what should go in here, contribution info? info about the server? info about the branch conventions?
+    enable-index-owner=0
+
+    logo=/un.svg
+    favicon=/unicon.svg
+    # TODO(V): footer=https://src.unfathomable.blue/nixos-config/commit/?id={commit}
+    mimetype-file=${pkgs.mime-types}/etc/mime.types
+    # TODO(V): repository-sort=age?
+    # TODO(V): robots=none? (same as noindex, nofollow)
+    readme=:README.adoc
+    clone-prefix=https://src.unfathomable.blue
+    agefile=info/last-modified
+    about-filter=${cgit-about-filter}
+    # TODO(edef): commit-filter, for bug tracker links
+    source-filter=${pkgs.cgit}/lib/cgit/filters/syntax-highlighting.py
+    # TODO(edef): add snapshots once we start releasing things
+    # TODO(V): branch-sort=age?
+    enable-git-config=1
+
+    # Has to go last.
+    # Options set after this won't be applied due to how they're evaluated.
+    scan-path=/var/lib/git
+    # TODO(V): section-from-path?
+    # TODO(V): repository-specific logos
+    # TODO(V): other repository-specific options
+  '';
+in {
+  services.cgiserver.instances.cgit = {
+    description = "Lightweight Git web interface";
+    application = "${pkgs.cgit}/cgit/cgit.cgi";
+    environment.CGIT_CONFIG = "${cgit-config}";
+    serviceConfig.SupplementaryGroups = [ "git" ];
+    # TODO(V): Hardening options
+  };
+
+  # TODO(V): set up git-http-backend. Disable enable-http-clone when we've done that?
+  services.caddy.config = ''
+    src.unfathomable.blue {
+      import common
+
+      root * ${cgit-webroot}
+      @exists file
+
+      route {
+        file_server @exists
+        reverse_proxy unix//run/cgit/cgit.sock
+      }
+    }
+  '';
+
+  declarative.git.hooks.post-receive = [
+    # Regenerate the static pack and ref indices used by the dumb git protocol
+    # TODO(V): Remove this once we set up git-http-backend
+    (pkgs.writeShellScript "update-server-info" ''
+      git update-server-info
+    '')
+
+    # Update the last-modified timestamp that cgit uses to measure freshness
+    (pkgs.writeShellScript "update-agefile" ''
+      git for-each-ref \
+        --sort=-creatordate --count=1 \
+        --format='%(creatordate:iso)' \
+        >info/last-modified
+    '')
+  ];
+}
diff --git a/fleet/hosts/trieste/cgit/ripple.svg b/fleet/hosts/trieste/cgit/ripple.svg
new file mode 100644
index 0000000..243059f
--- /dev/null
+++ b/fleet/hosts/trieste/cgit/ripple.svg
@@ -0,0 +1,8 @@
+<!-- SPDX-FileCopyrightText: V <v@unfathomable.blue> -->
+<!-- SPDX-License-Identifier: LicenseRef-NONE -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
+  <circle cx="32" cy="32" r="8"/>
+  <path d="M17.28 38.2l3.5-2.03A12 12 0 0120 32a12 12 0 0110-11.82v-4.04A16 16 0 0016 32a16 16 0 001.28 6.2zM44.71 41.65l-3.5-2.02A12 12 0 0132 44a12 12 0 01-9.21-4.37l-3.5 2.02A16 16 0 0032 48a16 16 0 0012.71-6.35zM34 16.13v4.04A12 12 0 0144 32a12 12 0 01-.78 4.17l3.5 2.02A16 16 0 0048 32a16 16 0 00-14-15.87z" color="#000"/>
+  <path d="M10.3 42.22l3.51-2.03A20 20 0 0112 32a20 20 0 0118-19.84V8.14A24 24 0 008 32a24 24 0 002.3 10.22zM51.67 45.67l-3.44-1.99A20 20 0 0132 52a20 20 0 01-16.23-8.32l-3.44 1.99A24 24 0 0032 56a24 24 0 0019.67-10.33zM34 8.09v4.01A20 20 0 0152 32a20 20 0 01-1.81 8.2l3.5 2.02A24 24 0 0056 32 24 24 0 0034 8.09z" color="#000"/>
+</svg>
diff --git a/fleet/hosts/trieste/cgit/un.svg b/fleet/hosts/trieste/cgit/un.svg
new file mode 100644
index 0000000..a6201bf
--- /dev/null
+++ b/fleet/hosts/trieste/cgit/un.svg
@@ -0,0 +1,6 @@
+<!-- SPDX-FileCopyrightText: V <v@unfathomable.blue> -->
+<!-- SPDX-License-Identifier: LicenseRef-NONE -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
+  <path d="M29.5 47.2v-4.67c-1.26 1.85-2.63 3.23-4.25 4.13c-1.6.89-3.45 1.33-5.57 1.33-3.5 0-6.16-1.09-7.98-3.26-1.8-2.18-2.72-5.36-2.72-9.55V16.74H14v18.25c0 2.89.56 5.05 1.69 6.5 1.12 1.44 2.8 2.15 5.06 2.15 2.7 0 4.83-.86 6.39-2.58 1.58-1.72 2.37-4.07 2.37-7.05V16.74h5.03v4.74c1.2-1.84 2.6-3.2 4.22-4.11 1.63-.91 3.5-1.36 5.63-1.36 3.5 0 6.15 1.09 7.94 3.26 1.8 2.16 2.7 5.34 2.7 9.55v18.39H50V28.99c0-2.89-.56-5.05-1.69-6.48-1.12-1.43-2.8-2.15-5.06-2.15-2.7 0-4.83.86-6.39 2.58-1.56 1.73-2.34 4.08-2.34 7.05v17.22H29.5"/>
+</svg>
diff --git a/fleet/hosts/trieste/cgit/unicon.svg b/fleet/hosts/trieste/cgit/unicon.svg
new file mode 100644
index 0000000..4753d6b
--- /dev/null
+++ b/fleet/hosts/trieste/cgit/unicon.svg
@@ -0,0 +1,6 @@
+<!-- SPDX-FileCopyrightText: V <v@unfathomable.blue> -->
+<!-- SPDX-License-Identifier: LicenseRef-NONE -->
+
+<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
+  <path d="M28.519 53.125v-6.49c-1.752 2.57-3.656 4.489-5.907 5.74-2.224 1.236-4.795 1.848-7.741 1.848-4.864 0-8.561-1.515-11.09-4.53C1.278 46.662 0 42.242 0 36.42V10.792h6.977v25.363c0 4.017.778 7.019 2.349 9.034 1.556 2.001 3.89 2.988 7.032 2.988 3.752 0 6.713-1.195 8.88-3.585 2.197-2.39 3.294-5.657 3.294-9.799V10.792h6.991v6.587c1.668-2.557 3.614-4.447 5.865-5.712 2.265-1.264 4.864-1.89 7.825-1.89 4.864 0 8.547 1.515 11.035 4.53C62.749 17.31 64 21.73 64 27.58V53.14h-6.99V27.817c0-4.017-.779-7.019-2.35-9.006-1.556-1.988-3.89-2.988-7.032-2.988-3.752 0-6.712 1.195-8.88 3.585-2.169 2.405-3.253 5.67-3.253 9.799v23.932H28.52"/>
+</svg>
diff --git a/fleet/hosts/trieste/default.nix b/fleet/hosts/trieste/default.nix
new file mode 100644
index 0000000..08dce1f
--- /dev/null
+++ b/fleet/hosts/trieste/default.nix
@@ -0,0 +1,52 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    ./cgit
+    ./git.nix
+    ./lists.nix
+    ./mail.nix
+    ./web.nix
+  ];
+
+  boot.initrd.network.ssh.authorizedKeys = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM3xBRi/sOVJnurXf1McDrODEhU4hCrKZewrUlDmu1Sl v@january"
+    "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+  ];
+
+  # TODO(V): Write a proper description for this
+  # It's b/c the default hosts file is borked
+  # And we need the addresses here b/c for some reason the
+  # stub resolver doesn't return the domain name in PTR records
+  networking.hostFiles = mkForce [
+    (pkgs.writeText "hosts" ''
+      168.119.127.252 trieste.unfathomable.blue
+      2a01:4f8:c2c:b2ae::1:f93f trieste.unfathomable.blue
+    '')
+  ];
+
+  networking.defaultGateway6.address = "fe80::1";
+  networking.interfaces.ens3.ipv6.addresses = singleton {
+    address = "2a01:4f8:c2c:b2ae::1:f93f";
+    prefixLength = 64;
+  };
+
+  services.caddy.config = ''
+    trieste.unfathomable.blue {
+      import common
+      redir / https://en.wikipedia.org/wiki/Trieste_(bathyscaphe)
+      error 404
+    }
+  '';
+
+  users.users.root.openssh.authorizedKeys.keys = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILjTET0rm61NIM8C8t95YY8PYGhuieEchTznaaIm/3IK v@january"
+    "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+  ];
+}
diff --git a/fleet/hosts/trieste/git.nix b/fleet/hosts/trieste/git.nix
new file mode 100644
index 0000000..f4d4e0b
--- /dev/null
+++ b/fleet/hosts/trieste/git.nix
@@ -0,0 +1,47 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ pkgs, ... }:
+
+let
+  root = "/var/lib/git";
+in {
+  users.users.git = {
+    isSystemUser = true;
+    group = "git";
+
+    # This lets us address remote repositories like `trieste:foo`.
+    home = root;
+
+    # TODO(V): Remove the override once https://github.com/NixOS/nixpkgs/pull/128062 has made its way into stable.
+    shell = pkgs.git // { shellPath = "/bin/git-shell"; };
+
+    openssh.authorizedKeys.keys = [
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDXELHAMjO/BzoBFgTW9ln3td2WnXw9VGF3zpMBiswsx git@vityaz"
+    ];
+  };
+
+  users.groups.git = {};
+
+  systemd.tmpfiles.rules = [
+    "d ${root} 0750 git git"
+  ];
+
+  declarative.git.repositories = {
+    ripple = {
+      description = "A build system for the next decade";
+      config.cgit = {
+        # This is added to the webroot in cgit.nix. It would be nice if we could do that modularly.
+        # Another option is to simply hotlink https://ripple.unfathomable.blue/icon.svg
+        # Yet another option is to keep the SVG in Git, and link to the raw file from trunk.
+        logo = "/ripple.svg";
+
+        homepage = "https://ripple.unfathomable.blue/";
+      };
+    };
+
+    ripple-website.description = "Source code for https://ripple.unfathomable.blue/";
+    nixos-config.description = "NixOS configuration for Unfathomable infrastructure";
+  };
+}
diff --git a/fleet/hosts/trieste/lists.nix b/fleet/hosts/trieste/lists.nix
new file mode 100644
index 0000000..a4e9a69
--- /dev/null
+++ b/fleet/hosts/trieste/lists.nix
@@ -0,0 +1,58 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+  # Block HTML e-mail
+  # FIXME(V): This is global, and will affect anyone sending HTML mail to e.g. postmaster@
+  # We should fix this, and limit it to just the list: this is possible using http://mlmmj.org/docs/readme-access/
+  # Unfortunately this doesn't let us pick an error message, though. So maybe not.
+  services.postfix = {
+    enableHeaderChecks = true;
+    headerChecks = [
+      {
+        pattern = ''/^Content-Type: text\/html/'';  # This feels kind of brittle, but should work in 99% of cases.
+        action = "REJECT HTML e-mail is not allowed on this list. See https://useplaintext.email/ for more information.";
+      }
+    ];
+  };
+
+  services.mlmmj = {
+    enablePostfix = true;
+    enablePublicInbox = true;
+
+    control.customheaders = [ "X-Clacks-Overhead: GNU Terry Pratchett" ];
+
+    lists."lists.unfathomable.blue" = {
+      ripple-announce = {
+        description = "Progress updates and other major announcements about Ripple";
+        moderators = [
+          "v@unfathomable.blue"
+          "edef@unfathomable.blue"
+        ];
+        # FIXME(V): This doesn't have quite the effect I was looking for.
+        # It submits non-moderator posts for review, rather than outright rejecting them as I'd wanted.
+        # Perhaps this is good, though, as it allows guest posts?
+        # Downside is there's no immediate rejection, so the user is left with the impression that their mail disappeared…
+        # Maybe http://mlmmj.org/docs/readme-access/ would be more appropriate?
+        control.modonlypost = true;
+      };
+      ripple-devel.description = "Technical discourse and patches for Ripple";
+      ripple-discuss.description = "General discussion about Ripple";
+      # TODO(V): ripple-commits, read-only commit notifications
+    };
+  };
+
+  # By default, the index 404s with the rather confusing message "no inboxes, yet", even when there are inboxes configured.
+  services.public-inbox.settings.publicinbox.wwwlisting = "all";
+
+  services.caddy.config = ''
+    lists.unfathomable.blue {
+      import common
+      reverse_proxy unix//run/public-inbox/httpd.sock
+    }
+  '';
+}
diff --git a/fleet/hosts/trieste/mail.nix b/fleet/hosts/trieste/mail.nix
new file mode 100644
index 0000000..a9258d2
--- /dev/null
+++ b/fleet/hosts/trieste/mail.nix
@@ -0,0 +1,14 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ pkgs, ... }:
+
+{
+  services.postfix = {
+    # Disable delivery to local users
+    localRecipients = [];
+
+    # Forward administrative mail to vityaz
+    postmasterAlias = "postmaster@unfathomable.blue";
+  };
+}
diff --git a/fleet/hosts/trieste/web.nix b/fleet/hosts/trieste/web.nix
new file mode 100644
index 0000000..d32fc44
--- /dev/null
+++ b/fleet/hosts/trieste/web.nix
@@ -0,0 +1,32 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ pkgs, ... }:
+
+{
+  systemd.tmpfiles.rules = [
+    "d /var/lib/www - git git"
+  ];
+
+  declarative.git.repositories.ripple-website.hooks.post-receive = [
+    (pkgs.writeShellScript "update-ripple-website" ''
+      nix-build ${./build-from-git.nix} \
+        --argstr repo /var/lib/git/ripple-website \
+        -o /var/lib/www/ripple
+    '')
+  ];
+
+  services.caddy.config = ''
+    unfathomable.blue {
+      import common
+      respond / "the depths await"
+      error 404
+    }
+
+    ripple.unfathomable.blue {
+      import common
+      root * /var/lib/www/ripple
+      file_server
+    }
+  '';
+}
diff --git a/fleet/hosts/vityaz/default.nix b/fleet/hosts/vityaz/default.nix
new file mode 100644
index 0000000..18a4c03
--- /dev/null
+++ b/fleet/hosts/vityaz/default.nix
@@ -0,0 +1,112 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+  imports = [
+    ./git.nix
+    ./mail.nix
+    ./mumble.nix
+  ];
+
+  boot.initrd.network.ssh.authorizedKeys = [
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGJ8Ms9z95InM7oGJLuo7DdDPh3r5xKnglvBSZ7FTTZ8 v@january"
+    "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+  ];
+
+  # TODO(V): Write a proper description for this
+  # It's b/c the default hosts file is borked
+  # And we need the addresses here b/c for some reason the
+  # stub resolver doesn't return the domain name in PTR records
+  networking.hostFiles = mkForce [
+    (pkgs.writeText "hosts" ''
+      157.90.172.8 vityaz.unfathomable.blue
+      2a01:4f8:1c0c:46a9::1:f93f vityaz.unfathomable.blue
+    '')
+  ];
+
+  networking.defaultGateway6.address = "fe80::1";
+  networking.interfaces.ens3.ipv6.addresses = singleton {
+    address = "2a01:4f8:1c0c:46a9::1:f93f";
+    prefixLength = 64;
+  };
+
+  networking.wireguard.interfaces.wg0 = {
+    ips = [ "10.102.120.0" ];
+    listenPort = 51820;
+    privateKeyFile = "/etc/wireguard/0.key";
+    generatePrivateKeyFile = true;
+
+    peers = mapAttrsToList (address: publicKey: {
+      inherit publicKey;
+      allowedIPs = [ "10.102.120.${address}/32" ];
+    }) {
+      "1" = "z6JrEDvTyIB7cPh4RzeyAihNl+pzgHxv08TMyeynQX4=";  # january
+      "2" = "KSigo7Ny3TTOSPBYDOCVm+K92/pIfgawlfAxK/UBfxA=";  # jaguar
+      "3" = "1EcmBoRykRep8IagzhtJ4zZU0r7gx5W7nZFh2m1wSE8=";  # OnePlus 5T
+      "4" = "TqKlPfBk1McfYNk6S7ZtSj/GnyisGWneozQrh0eh1C8=";  # wallaby
+      "5" = "kuEkbQ+6mOGwkNkOHqpnxM/TI3gpc2sQ6L15UxsOMDI=";  # M1
+    };
+
+    preSetup = ''
+      ${pkgs.iptables}/bin/iptables -A FORWARD -i wg0 -o wg0 -s 10.102.120.0/24 -d 10.102.120.0/24 -j ACCEPT
+    '';
+
+    postShutdown = ''
+      ${pkgs.iptables}/bin/iptables -D FORWARD -i wg0 -o wg0 -s 10.102.120.0/24 -d 10.102.120.0/24 -j ACCEPT
+    '';
+  };
+
+  networking.firewall.interfaces.ens3.allowedUDPPorts = [ config.networking.wireguard.interfaces.wg0.listenPort ];
+
+  networking.firewall.extraCommands = ''
+    iptables -P FORWARD DROP
+  '';
+
+  boot.kernel.sysctl."net.ipv4.conf.wg0.forwarding" = true;
+
+  services.caddy.config = ''
+    vityaz.unfathomable.blue {
+      import common
+      redir / https://en.wikipedia.org/wiki/Vityaz-D_Autonomous_Underwater_Vehicle
+      error 404
+    }
+  '';
+
+  users.users = {
+    root = {
+      openssh.authorizedKeys.keys = [
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDz+gGXZUvQiLcDgvon28dErFsbii2cVXJ5wVlsUgaBZ v@january"
+        "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+      ];
+    };
+
+    v = {
+      isNormalUser = true;
+      description = "V";
+
+      openssh.authorizedKeys.keys = [
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILKMEXEIK2PIRkXYb3RCVN15q9DhKsQlbMhHa5BxQyuz v@january"
+      ];
+
+      packages = with pkgs; [
+      ];
+    };
+
+    edef = {
+      isNormalUser = true;
+      description = "edef";
+
+      openssh.authorizedKeys.keys = [
+        "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+      ];
+
+      packages = with pkgs; [
+      ];
+    };
+  };
+}
diff --git a/fleet/hosts/vityaz/git.nix b/fleet/hosts/vityaz/git.nix
new file mode 100644
index 0000000..66f26db
--- /dev/null
+++ b/fleet/hosts/vityaz/git.nix
@@ -0,0 +1,67 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ lib, pkgs, ... }:
+
+with lib;
+
+{
+  # TODO(edef): could we somehow make this use DynamicUser?
+  users.users.git = {
+    isSystemUser = true;
+
+    group = "git";
+
+    home = "/var/lib/git";
+    createHome = true;
+
+    useDefaultShell = true;
+
+    openssh.authorizedKeys.keys = [
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFovWcdS0vQAJiEvwjEIUOv7eip52oX7rVOEMQDJkSL6 v@january"
+      "cert-authority ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCvb/7ojfcbKvHIyjnrNUOOgzy44tCkgXY9HLuyFta1jQOE9pFIK19B4dR9bOglPKf145CCL0mSFJNNqmNwwavU2uRn+TQrW+U1dQAk8Gt+gh3O49YE854hwwyMU+xD6bIuUdfxPr+r5al/Ov5Km28ZMlHOs3FoAP0hInK+eAibioxL5rVJOtgicrOVCkGoXEgnuG+LRbOYTwzdClhRUxiPjK8alCbcJQ53AeZHO4G6w9wTr+W5ILCfvW4OmUXCX01sKzaBiQuuFCF6M/H4LlnsPWLMra2twXxkOIhZblwC+lncps9lQaUgiD4koZeOCORvHW00G0L39ilFbbnVcL6Itp/m8RRWm/xRxS4RMnsdV/AhvpRLrhL3lfQ7E2oCeSM36v1S9rdg6a47zcnpL+ahG76Gz39Y7KmVRQciNx7ezbwxj3Q5lZtFykgdfGIAN+bT8ijXMO6m68g60i9Bz4IoMZGkiJGqMYLTxMQ+oRgR3Ro5lbj7E11YBHyeimoBYXYGHMkiuxopQZ7lIj3plxIzhmUlXJBA4jMw9KGHdYaLhaicIYhvQmCTAjrkt2HvxEe6lU8iws2Qv+pB6tAGundN36RVVWAckeQPZ4ZsgDP8V2FfibZ1nsrQ+zBKqaslYMAHs01Cf0Hm0PnCqagf230xaobu0iooNuXx44QKoDnB+w== openpgp:0x803010E7"
+    ];
+
+    packages = with pkgs; [
+      git
+    ];
+  };
+
+  users.groups.git = {};
+
+  # TODO(V): Enable the reflog?
+  declarative.git.repositories = flip genAttrs (repo: {
+    hooks.post-receive = [
+      # FIXME(V): There are more than a number of issues with this!
+      # - non-generic (we could use $GIT_DIR or such)
+      # - requires an explicit remote (we could add this to the config)
+      # - only updates trunk (even if other branches were pushed)
+      # - has no way to filter specific branches from being published
+      # - does not synchronize tags
+      (pkgs.writeShellScript "sync-repository" ''
+        git push trieste:${repo} trunk
+      '')
+    ];
+  }) [
+    # TODO(V): Take the list of public repositories from hosts/trieste/git.nix
+    # (or do the inverse)
+    # (or put this information in a shared location)
+    "ripple"
+    "ripple-website"
+    "nixos-config"
+
+    # Note: private repositories are currently not configured here.
+    # If we find it acceptable to leak their names, they could take advantage of this module as well.
+  ];
+
+  # TODO(V): Linting hooks (honestly, these should just go in CI)
+  # - reuse lint
+  # - check there's a (owner) for every TODO, FIXME, XXX, etc
+  # - make sure everything has been run through rustfmt
+
+  # TODO(V): An equivalent of Bors ("Tolby"?) for our workflow
+  # (or, at least, a queue of commits that must individually pass CI to get merged)
+
+  # TODO(V): Set up CI
+}
diff --git a/fleet/hosts/vityaz/mail.nix b/fleet/hosts/vityaz/mail.nix
new file mode 100644
index 0000000..58d6866
--- /dev/null
+++ b/fleet/hosts/vityaz/mail.nix
@@ -0,0 +1,58 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-FileCopyrightText: edef <edef@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ pkgs, ... }:
+
+{
+  services.postfix = {
+    # TODO(V): Set myorigin to $mydomain?
+
+    # We accept mail to ourselves and to the apex
+    destination = [ "$myhostname" "$mydomain" ];
+
+    # TODO(V): Restrict authorized_submit_users to system users
+
+    # TODO(V): Authenticate users
+    networks = [
+      # Defaults
+      "127.0.0.1/32"
+      "157.90.172.8/32"
+      "10.102.120.0/32"
+      "[::1]/128"
+      "[2a01:4f8:1c0c:46a9::1:f93f]/128"
+      "[fe80::9400:ff:feae:b407]/128"
+
+      # Intranet
+      "10.102.120.0/24"
+    ];
+
+    # Wait, why is this enabled here?
+    recipientDelimiter = "+";
+
+    # TODO(V): postscreen + DNSBLs
+    # TODO(V): postgrey
+
+    rootAlias = "v, edef";
+
+    # TODO(V): Forward mails to root to both edef & V
+    # TODO(V): Forward mails to postmaster to both edef & V
+    # TODO(V): Add extra aliases (Alyssa has abuse, noc, security, hostmaster, usenet, news, webmaster, www, uucp, and ftp)
+    # TODO(V): Add more notify_classes
+  };
+
+  systemd.user.paths.mail = {
+    description = "New mail trigger";
+    wantedBy = [ "paths.target" ];
+    pathConfig.PathChanged = "/var/mail/%u/new";
+    unitConfig.ConditionPathExists = "%h/.notmuch-config";
+  };
+
+  systemd.user.services.mail = {
+    description = "New mail indexing";
+    serviceConfig = {
+      Type = "exec";
+      ExecStart = "${pkgs.notmuch}/bin/notmuch new";
+    };
+  };
+}
diff --git a/fleet/hosts/vityaz/mumble.nix b/fleet/hosts/vityaz/mumble.nix
new file mode 100644
index 0000000..dffc6a6
--- /dev/null
+++ b/fleet/hosts/vityaz/mumble.nix
@@ -0,0 +1,21 @@
+# SPDX-FileCopyrightText: V <v@unfathomable.blue>
+# SPDX-License-Identifier: OSL-3.0
+
+{ config, ... }:
+
+{
+  services.murmur = {
+    enable = true;
+
+    # This isn't actually the hostname, it's the address to bind on.
+    hostName = builtins.head config.networking.wireguard.interfaces.wg0.ips;
+
+    # Another misleading name— it's also used as the root channel name.
+    registerName = "Pool";
+  };
+
+  networking.firewall.interfaces.wg0 = {
+    allowedTCPPorts = [ config.services.murmur.port ];
+    allowedUDPPorts = [ config.services.murmur.port ];
+  };
+}