From ec0965e2672899d25a5a3a8c072de3ea734076a2 Mon Sep 17 00:00:00 2001 From: V Date: Wed, 9 Jun 2021 15:43:16 +0200 Subject: fleet: init Co-authored-by: edef Change-Id: I36d2c4cca542ed91630b1b832f3c7a7b97b33c65 --- fleet/README.adoc | 12 + fleet/configuration.nix | 141 +++++++++++ fleet/hosts/trieste/build-from-git.nix | 16 ++ fleet/hosts/trieste/cgit/default.nix | 107 ++++++++ fleet/hosts/trieste/cgit/ripple.svg | 8 + fleet/hosts/trieste/cgit/un.svg | 6 + fleet/hosts/trieste/cgit/unicon.svg | 6 + fleet/hosts/trieste/default.nix | 52 ++++ fleet/hosts/trieste/git.nix | 47 ++++ fleet/hosts/trieste/lists.nix | 58 +++++ fleet/hosts/trieste/mail.nix | 14 ++ fleet/hosts/trieste/web.nix | 32 +++ fleet/hosts/vityaz/default.nix | 112 +++++++++ fleet/hosts/vityaz/git.nix | 67 +++++ fleet/hosts/vityaz/mail.nix | 58 +++++ fleet/hosts/vityaz/mumble.nix | 21 ++ fleet/modules/acme.nix | 55 +++++ fleet/modules/cgiserver.nix | 73 ++++++ fleet/modules/declarative-git.nix | 59 +++++ fleet/modules/mail.nix | 70 ++++++ fleet/modules/mlmmj.nix | 271 +++++++++++++++++++++ fleet/modules/public-inbox.nix | 134 ++++++++++ fleet/modules/web.nix | 46 ++++ fleet/pkgs/cgiserver/default.nix | 25 ++ fleet/pkgs/declarative-git-repository/default.nix | 53 ++++ fleet/pkgs/group-readable-archives.patch | 22 ++ fleet/pkgs/overlay.nix | 24 ++ .../permission-warnings-only-when-necessary.patch | 50 ++++ fleet/pkgs/public-inbox-init-lite/default.nix | 18 ++ .../public-inbox-init-lite/public-inbox-init-lite | 60 +++++ fleet/pkgs/public-inbox/default.nix | 45 ++++ fleet/test | 7 + 32 files changed, 1769 insertions(+) create mode 100644 fleet/README.adoc create mode 100644 fleet/configuration.nix create mode 100644 fleet/hosts/trieste/build-from-git.nix create mode 100644 fleet/hosts/trieste/cgit/default.nix create mode 100644 fleet/hosts/trieste/cgit/ripple.svg create mode 100644 fleet/hosts/trieste/cgit/un.svg create mode 100644 fleet/hosts/trieste/cgit/unicon.svg create mode 100644 fleet/hosts/trieste/default.nix create mode 100644 fleet/hosts/trieste/git.nix create mode 100644 fleet/hosts/trieste/lists.nix create mode 100644 fleet/hosts/trieste/mail.nix create mode 100644 fleet/hosts/trieste/web.nix create mode 100644 fleet/hosts/vityaz/default.nix create mode 100644 fleet/hosts/vityaz/git.nix create mode 100644 fleet/hosts/vityaz/mail.nix create mode 100644 fleet/hosts/vityaz/mumble.nix create mode 100644 fleet/modules/acme.nix create mode 100644 fleet/modules/cgiserver.nix create mode 100644 fleet/modules/declarative-git.nix create mode 100644 fleet/modules/mail.nix create mode 100644 fleet/modules/mlmmj.nix create mode 100644 fleet/modules/public-inbox.nix create mode 100644 fleet/modules/web.nix create mode 100644 fleet/pkgs/cgiserver/default.nix create mode 100644 fleet/pkgs/declarative-git-repository/default.nix create mode 100644 fleet/pkgs/group-readable-archives.patch create mode 100644 fleet/pkgs/overlay.nix create mode 100644 fleet/pkgs/permission-warnings-only-when-necessary.patch create mode 100644 fleet/pkgs/public-inbox-init-lite/default.nix create mode 100644 fleet/pkgs/public-inbox-init-lite/public-inbox-init-lite create mode 100644 fleet/pkgs/public-inbox/default.nix create mode 100755 fleet/test (limited to 'fleet') diff --git a/fleet/README.adoc b/fleet/README.adoc new file mode 100644 index 0000000..2a6e014 --- /dev/null +++ b/fleet/README.adoc @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: V +// SPDX-License-Identifier: OSL-3.0 + += NixOS configuration for Unfathomable infrastructure + +Here are the Nix expressions that comprise Unfathomable's infrastructure. + +Shared packages and NixOS modules can be found in `pkgs/` and `modules/`, respectively. Host-specific configuration goes under `hosts/`. + +== License + +Licensed under the Open Software License version 3.0 diff --git a/fleet/configuration.nix b/fleet/configuration.nix new file mode 100644 index 0000000..2ba819a --- /dev/null +++ b/fleet/configuration.nix @@ -0,0 +1,141 @@ +# SPDX-FileCopyrightText: V +# SPDX-FileCopyrightText: edef +# SPDX-License-Identifier: OSL-3.0 + +{ lib, pkgs, modulesPath, ... }: + +with lib; + +let + host = fileContents /etc/hostname; + # commit = commitIdFromGitRepo ./.git; +in { + imports = [ + "${modulesPath}/profiles/qemu-guest.nix" + (./hosts + "/${host}") + ] ++ mapAttrsToList (module: _: ./modules + "/${module}") (builtins.readDir ./modules); + + nixpkgs.overlays = [ (import ./pkgs/overlay.nix) ]; + + system.stateVersion = "20.09"; + + + ### Startup + + boot.loader.grub.device = "/dev/sda"; + + boot.initrd = { + availableKernelModules = [ "ata_piix" "virtio_pci" "xhci_pci" "sd_mod" "sr_mod" ]; + + luks.devices.rpool = { + device = "/dev/sda3"; + allowDiscards = true; + }; + + network.enable = true; + + network.ssh = { + enable = true; + port = 798; # Random unassigned port in the range [1, 1024] + hostKeys = [ "/etc/initrd/ssh_host_ed25519_key" ]; + }; + }; + + + ### Filesystems + + # Come on, why isn't this the default? + boot.tmpOnTmpfs = true; + + # Required by ZFS, but redundant on a single-pathed system. + networking.hostId = "00000000"; + + fileSystems = { + "/boot" = { + device = "/dev/sda2"; + fsType = "ext2"; + }; + + "/" = { + device = "rpool/root"; + fsType = "zfs"; + + # Extracted from the strace output of `zfs mount -a` + # NOTE: the pool is configured with `zfs set setuid=off rpool` + # TODO(V): come up with a less ugly solution + options = [ "defaults" "atime" "strictatime" "dev" "exec" "rw" "nosuid" "nomand" "zfsutil" ]; + }; + }; + + + ### Networking + + networking.useNetworkd = true; + + networking.hostName = host; + networking.domain = "unfathomable.blue"; + + # Misnomer, actually enables DHCP for all unmanaged interfaces. + # It's also incompatible with systemd-networkd. + networking.useDHCP = false; + + networking.interfaces.ens3.useDHCP = true; + + # This is exceedingly spammy, and not so useful for an Internet-facing machine. + networking.firewall.logRefusedConnections = false; + + + ### Security + privacy + + security.sudo.enable = false; + + + ### System services + + system.autoUpgrade.enable = true; + + services.openssh = { + enable = true; + passwordAuthentication = false; + challengeResponseAuthentication = false; + # TODO(V): Route exclusively over WireGuard, if you dare + }; + + + ### Programs + user services + + programs.fish.enable = true; + programs.mosh.enable = true; + programs.mtr.enable = true; + + + ### Environment + + time.timeZone = "UTC"; + + i18n = { + defaultLocale = "en_US.UTF-8"; + supportedLocales = [ "en_US.UTF-8/UTF-8" ]; + extraLocaleSettings.LC_COLLATE = "C"; + }; + + # TODO(V): Switch to https://github.com/NixOS/nixpkgs/pull/101127 once it's been merged and made its way into stable. + users.defaultUserShell = pkgs.fish; + environment.variables.EDITOR = "kak"; + + environment.systemPackages = with pkgs; [ + kakoune + tree + htop + ldns + ]; + + + ### Users + + users.mutableUsers = false; + + # This is here so we can `git push` directly to /etc/nixos. + # It should be removed if we stop using that workflow. + users.users.root.packages = [ pkgs.git ]; +} 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 +# SPDX-License-Identifier: OSL-3.0 + +{ repo, pkgs ? import {} }: + +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 +# SPDX-FileCopyrightText: edef +# 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 @@ + + + + + + + + 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 @@ + + + + + + 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 @@ + + + + + + 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 +# SPDX-FileCopyrightText: edef +# 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 +# SPDX-FileCopyrightText: edef +# 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 +# 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 +# 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 +# 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 +# SPDX-FileCopyrightText: edef +# 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 +# SPDX-FileCopyrightText: edef +# 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 +# SPDX-FileCopyrightText: edef +# 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 +# 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 ]; + }; +} diff --git a/fleet/modules/acme.nix b/fleet/modules/acme.nix new file mode 100644 index 0000000..f06ac4e --- /dev/null +++ b/fleet/modules/acme.nix @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +{ config, lib, pkgs, ... }: + +with lib; + +let + webroot = "/var/lib/acme/challenges"; +in { + options = { + # We want to set webroot on every single certificate, but trying + # to do this by using genAttrs on the certificate attribute names + # produces infinite recursion. To get around this, we're instead + # setting webroot from within the certificate submodule itself. + # + # The fact that this is even possible is because submodules are + # just like normal modules, insofar that they can have both an + # 'options' and a 'config' attribute, both of which are scoped + # to the submodule itself. Additionally, normal merging logic + # is applied to module options, meaning we can just define the + # certificates option again and that'll be handled correctly. + # + # TODO(V): Add a global security.acme.webroot option to the upstream module + security.acme.certs = mkOption { + type = types.attrsOf (types.submodule { + inherit webroot; + }); + }; + }; + + config = { + security.acme = { + acceptTerms = true; + email = "acme@unfathomable.blue"; + }; + + services.caddy.config = '' + ${concatStringsSep ", " (unique (mapAttrsToList (_: cert: "http://${cert.domain}") config.security.acme.certs))} { + import all + + route { + # TODO(V): make use of the 'file' matcher, so this is guaranteed to never 404? + file_server /.well-known/acme-challenge/* { + root ${webroot} + } + + # Manually handling http:// disables Caddy's automatic HTTPS + # redirects for the domain, so let's do that ourselves + redir https://{host}{uri} 308 + } + } + ''; + }; +} diff --git a/fleet/modules/cgiserver.nix b/fleet/modules/cgiserver.nix new file mode 100644 index 0000000..6cafbe0 --- /dev/null +++ b/fleet/modules/cgiserver.nix @@ -0,0 +1,73 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +{ config, lib, pkgs, modulesPath, ... }: + +with lib; + +let + cfg = config.services.cgiserver; + + inherit (import "${modulesPath}/system/boot/systemd-unit-options.nix" { inherit config lib; }) + serviceOptions socketOptions; + + # TODO(V): These descriptions could use a bit of work. + instanceOpts = { name, ... }: { + options = { + description = mkOption { + description = "Short description of the application."; + type = with types; nullOr str; + default = null; + }; + + application = mkOption { + description = "Path to the application."; + type = types.path; + }; + + environment = mkOption { + description = "Environment variables passed to the application."; + type = with types; attrsOf str; + default = {}; + }; + + serviceConfig = mkOption { + description = "Extra options to put in the [Service] section of the application's service unit."; + inherit (serviceOptions.serviceConfig) type; + default = {}; + }; + + listenStreams = mkOption { + description = "Addresses to listen on, in the format used by the ListenStream option of systemd.socket(5)."; + inherit (socketOptions.listenStreams) type; + default = [ "/run/${name}/${name}.sock" ]; + }; + }; + }; +in { + options.services.cgiserver = { + instances = mkOption { + description = "Definition of CGI application instances."; + type = with types; attrsOf (submodule instanceOpts); + default = {}; + }; + }; + + config = { + systemd.sockets = mapAttrs (name: config: { + inherit (config) listenStreams; + wantedBy = [ "sockets.target" ]; + }) cfg.instances; + + systemd.services = mapAttrs (name: config: { + inherit (config) description environment; + serviceConfig = { + ExecStart = "${pkgs.cgiserver}/bin/cgiserver ${config.application}"; + DynamicUser = true; + # TODO(V): Hardening options + } // config.serviceConfig; + }) cfg.instances; + }; + + meta.maintainers = with maintainers; [ V ]; +} diff --git a/fleet/modules/declarative-git.nix b/fleet/modules/declarative-git.nix new file mode 100644 index 0000000..ac4bd15 --- /dev/null +++ b/fleet/modules/declarative-git.nix @@ -0,0 +1,59 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +# TODO(V): Make the option descriptions not be terrible. + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.declarative.git; + + repoOpts = { name, config, ... }: { + options = { + description = mkOption { + description = "Description of the repository."; + type = types.str; + # Git defaults to "Unnamed repository; edit this file 'description' to name the repository." + # CGit defaults to "[no description]" + default = "Unnamed repository; edit this file 'description' to name the repository."; + }; + + config = mkOption { + description = "Git configuration for the repository."; + type = types.attrs; # TODO(V): be more precise + default = {}; + }; + + hooks = mkOption { + description = "Git hooks for the repository."; + type = with types; attrsOf (listOf path); + default = {}; + }; + }; + }; +in { + options.declarative.git = { + repositories = mkOption { + description = "Repositories to manage declaratively."; + type = types.attrsOf (types.submodule repoOpts); + default = {}; + }; + + hooks = mkOption { + description = "Git hooks to apply to all declarative repositories."; + type = with types; attrsOf (listOf path); + default = {}; + }; + }; + + config.systemd.tmpfiles.packages = mapAttrsToList (name: config: + pkgs.declarative-git-repository { + path = "/var/lib/git/${name}"; + inherit (config) description config; + hooks = zipAttrsWith (_: concatLists) [ cfg.hooks config.hooks ]; + user = "git"; + group = "git"; + }) cfg.repositories; +} diff --git a/fleet/modules/mail.nix b/fleet/modules/mail.nix new file mode 100644 index 0000000..24f3925 --- /dev/null +++ b/fleet/modules/mail.nix @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: V +# SPDX-FileCopyrightText: edef +# SPDX-License-Identifier: OSL-3.0 + +{ config, pkgs, ... }: + +{ + security.acme.certs = { + "${config.networking.fqdn}" = { + postRun = "systemctl reload postfix.service"; + }; + + # Older mail servers might not support ECDSA + "${config.networking.fqdn}-rsa2048" = { + domain = config.networking.fqdn; + keyType = "rsa2048"; + postRun = "systemctl reload postfix.service"; + }; + }; + + services.postfix = { + enable = true; + + # 'myhostname' is actually the FQDN, which Postfix incorrectly expects gethostname(3) to return + hostname = config.networking.fqdn; + + # TODO(edef): instrument postfix to find out how often opportunistic encryption works, and with which cipher suites/certificates + config = { + # Disable account enumeration + disable_vrfy_command = true; + + # TODO(V): Look into further hardening + + # Block DNSBLed addresses + postscreen_dnsbl_sites = [ "zen.spamhaus.org" "ix.dnsbl.manitu.net" ]; + postscreen_dnsbl_action = "enforce"; + + # Block overly eager robots + postscreen_greet_action = "enforce"; + + # TODO(V): Look into SpamAssassin for more advanced SPAM protection + + # TODO(V): Support https://github.com/NixOS/nixpkgs/pull/89178 so we can remove some of the following boilerplate + + # Outgoing TLS configuration + smtp_tls_security_level = "may"; + smtp_tls_CAfile = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; + smtp_tls_loglevel = "1"; + # TODO(V): disable TLSv1 and other insecure versions? + + # Incoming TLS configuration + smtpd_tls_security_level = "may"; + smtpd_tls_chain_files = [ + # TODO(V): add ed25519, in the bright, wonderful future of cryptography + "/var/lib/acme/${config.networking.fqdn}/full.pem" + "/var/lib/acme/${config.networking.fqdn}-rsa2048/full.pem" + ]; + smtpd_tls_loglevel = "1"; + # TODO(V): disable TLSv1 and other insecure versions? + }; + }; + + users.users.postfix.extraGroups = [ "acme" ]; + + # TODO(V): Figure out how to ensure that Postfix depends on there being a valid cert on + # first-run, without causing issues with mail deliverability for an already running service. + # Aren't there self-signed certs that the ACME module has for exactly this reason? + + networking.firewall.allowedTCPPorts = [ 25 ]; +} diff --git a/fleet/modules/mlmmj.nix b/fleet/modules/mlmmj.nix new file mode 100644 index 0000000..070e5ff --- /dev/null +++ b/fleet/modules/mlmmj.nix @@ -0,0 +1,271 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +# TODO(V): Get mlmmj to log to the journal, somehow. Currently it only writes to a file in each mailing list's directory. + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mlmmj; + + # TODO(V): Clean this up when upstreaming. + encodeKnob = knob: + if knob == true then "" + else if isInt knob then toString knob + else if isList knob then concatStrings (map (line: "${line}\n") knob) + else if isString knob then knob + else throw "invalid value"; + + writeKnob = name: value: + pkgs.writeTextDir name + (encodeKnob value); + + mergeKnobs = knobs: + if isList (head knobs) + then concatLists knobs + else last knobs; + + lists = flatten + (mapAttrsToList (domain: lists: + mapAttrsToList (name: config: rec { + # The name and domain of this list. + inherit name domain; + + # The title of this list. + inherit (config) description; + + # Where this list's state is stored + path = "/var/spool/mlmmj/${domain}/${name}"; + + # The address of this list. Pretty self-explanatory! + address = "${name}@${domain}"; + + # A dummy address, used for internal routing + conduit = "${address}.mlmmj.alt"; + + # An identifier safe for inclusion in unit names + unitName = "${domain}-${name}"; + + # The list's control directory. See http://mlmmj.org/docs/tunables/ + controlDir = pkgs.buildEnv { + name = "mlmmj-control"; + paths = mapAttrsToList writeKnob (zipAttrsWith (_: mergeKnobs) [ + { + listaddress = address; + + # FIXME(V): This is broken! But it's what the default config that mlmmj-make-ml gives you has. + # > mlmmj-send.c:742: No @ in address, ignoring postmaster: Success + owner = "postmaster"; + + # This is honestly a bit of a mess! These really ought to + # be synthesized by the mailing-list software instead. + customheaders = [ + "Reply-To: ${address}" + # TODO(V): Ensure the description is quoted/encoded properly + "List-ID: ${config.description} <${name}.${domain}>" + "List-Post: " + "List-Help: " + "List-Subscribe: " + "List-Unsubscribe: " + ]; + # TODO(V): Add some/all of the above headers to delheaders? + } + + (optionalAttrs (config.moderators != null) { + moderated = true; + inherit (config) moderators; + }) + + cfg.control + + config.control + ]); + }; + }) lists) cfg.lists); + + listOpts = { + options = { + description = mkOption { + description = "A description for this list. Used in the List-ID header, and for its public inbox if integration is enabled."; + type = types.str; + example = "Test mailing list"; + }; + + moderators = mkOption { + description = "A list of moderators for this list. See http://mlmmj.org/docs/tunables/ for options that make use of them."; + type = with types; nullOr (listOf str); + default = null; + example = [ + "jan@example.org" + ]; + }; + + control = mkOption { + description = "A set of control knobs for this list. See http://mlmmj.org/docs/tunables/ for more information."; + # TODO(V): Restrict the ability to include newlines in these strings? + type = with types; attrsOf (oneOf [ str int bool (listOf str) ]); + default = {}; + example = { + modonlypost = true; + }; + }; + }; + }; +in { + disabledModules = [ + "services/mail/mlmmj.nix" + ]; + + options.services.mlmmj = { + enablePostfix = mkEnableOption "Postfix integration"; + enablePublicInbox = mkEnableOption "public-inbox integration"; + + # TODO(V): add a template language option, possibly allowing a custom package or path to be used + + control = mkOption { + description = "A set of control knobs applied to all lists. See http://mlmmj.org/docs/tunables/ for more information."; + # TODO(V): Restrict the ability to include newlines in these strings? + type = with types; attrsOf (oneOf [ str int bool (listOf str) ]); + default = {}; + example = { + modonlypost = true; + }; + }; + + lists = mkOption { + description = "A set of lists to be managed, organised by domain."; + type = types.attrsOf (types.attrsOf (types.submodule listOpts)); + default = {}; + example = { "example.org".mlmmj-test = {}; }; + }; + }; + + config = mkMerge [ + # TODO(V): replace this check with an mlmmj.enable option? + (mkIf (lists != []) { + users.users.mlmmj = { + isSystemUser = true; + uid = config.ids.uids.mlmmj; # Only used for the siphon + group = "mlmmj"; + }; + + users.groups.mlmmj = { + gid = config.ids.gids.mlmmj; # Only used for the siphon + }; + + # This does the equivalent of running mlmmj-make-ml (http://mlmmj.org/docs/mlmmj-make-ml, http://mlmmj.org/docs/readme ) + systemd.tmpfiles.packages = map ({ unitName, path, controlDir, ... }: + pkgs.writeTextDir "lib/tmpfiles.d/mlmmj-${unitName}.conf" ('' + d ${path} - mlmmj mlmmj + f ${path}/index - mlmmj mlmmj + L+ ${path}/control - - - - ${controlDir} + L+ ${path}/text - - - - ${pkgs.mlmmj}/share/mlmmj/text.skel/en + '' + concatMapStrings (dir: '' + d ${path}/${dir} - mlmmj mlmmj + '') [ + "incoming" "queue" "queue/discarded" "requeue" "archive" + "subconf" "unsubconf" + "bounce" "moderation" + "subscribers.d" "digesters.d" "nomailsubs.d" + ])) lists; + + systemd.services = listToAttrs (map ({ unitName, address, path, ... }: + nameValuePair "mlmmj-maintd-${unitName}" { + description = "mlmmj list maintenance for ${address}"; + serviceConfig = { + ExecStart = "${pkgs.mlmmj}/bin/mlmmj-maintd -F -L ${path}"; + # TODO(V): Should this be Type=exec or Type=oneshot? + User = "mlmmj"; + # TODO(V): Hardening + }; + }) lists); + + systemd.timers = listToAttrs (map ({ unitName, address, ... }: + nameValuePair "mlmmj-maintd-${unitName}" { + description = "mlmmj list maintenance timer for ${address}"; + wantedBy = [ "timers.target" ]; + timerConfig.OnCalendar = "00/2:00"; # Every two hours, as suggested by upstream. + # TODO(V): set OnBootSec to 0, and use OnUnit(In)ActiveSec, as with the rss2email module, etc? + # Is there a compelling reason to actually run this at calendar times? + # TODO(V): Increase/decrease the frequency? Every 2 hours is pretty arbitrary, so perhaps we should think a bit about this. + }) lists); + }) + + # TODO(V): make conditional on mlmmj in general, also? + # Adapted from http://mlmmj.org/docs/readme-postfix + (mkIf cfg.enablePostfix { + services.postfix = { + masterConfig.mlmmj = { + privileged = true; + command = "pipe"; + args = [ + "flags=ORhu" + "user=mlmmj" + "argv=${pkgs.mlmmj}/bin/mlmmj-receive -F -L $nexthop" + ]; + }; + + # TODO(V): ensure mlmmj is in authorized_submit_users (but only if this is defined!) + + config.mlmmj_destination_recipient_limit = "1"; + + # TODO(V): Make this configurable? Make control.delimiter respect this? Get the user to set it instead? + recipientDelimiter = "+"; + + # TODO(V): Should this module be automatically adding domains to virtual_alias_domains? + # Actually, the NixOS Postfix module should probably do this anyway. + config.virtual_alias_domains = mapAttrsToList (domain: _: domain) cfg.lists; + + # TODO(V): Handle local domains too, via aliases(5) and a per-domain option. + # Actually, doesn't virtual(5) already handle rewrites for local addresses? It doesn't care how addresses are handled + virtual = concatStrings (map ({ address, conduit, ... }: "${address} ${address}.mlmmj-siphon.alt, ${conduit}\n") lists); + + # Siphon incoming mail to a Maildir, so we can be sure we're not losing anything important. + # TODO(V): Remove this once we're more certain of the stability of our archival setup. + config.virtual_mailbox_domains = mapAttrsToList (domain: _: "${domain}.mlmmj-siphon.alt") cfg.lists; + config.virtual_mailbox_base = "/var/mail/mlmmj-siphon"; + config.virtual_mailbox_maps = [ "hash:/etc/postfix/vmailbox" ]; + config.virtual_uid_maps = "static:${toString config.ids.uids.mlmmj}"; + config.virtual_gid_maps = "static:${toString config.ids.gids.mlmmj}"; + mapFiles.vmailbox = pkgs.writeText "postfix-vmailbox" + (concatStrings (map ({ address, domain, name, ... }: '' + ${address}.mlmmj-siphon.alt ${domain}/${name}/ + '') lists)); + + # TODO(V): Do we want to add owner-{list} -> {list}+owner aliases? + # From aliases(5): + # > In addition, when an alias exists for owner-name, this will override the enve‐ + # > lope sender address, so that delivery diagnostics are directed to owner-name, + # > instead of the originator of the message (for details, see owner_request_spe‐ + # > cial, expand_owner_alias and reset_owner_alias). This is typically used to + # > direct delivery errors to the maintainer of a mailing list, who is in a better + # > position to deal with mailing list delivery problems than the originator of + # > the undelivered mail. + + # TODO(V): Should we add {list}-request addresses per https://datatracker.ietf.org/doc/html/rfc2142 ? Seems kind of decrepit. + + transport = concatStrings (map ({ conduit, path, ... }: "${conduit} mlmmj:${path}\n") lists); + }; + }) + + (mkIf cfg.enablePublicInbox { + # TODO(V): public-inbox integration + services.public-inbox = { + enable = true; # TODO: Remove, this should be up to the user to enable? + + inboxes = listToAttrs (map ({ name, domain, description, path, ... }: + nameValuePair name { + inherit domain description; + watch = let maildir = path: pkgs.linkFarm "fake-maildir" [ + { name = "new"; inherit path; } + { name = "cur"; path = /var/empty; } + ]; in "maildir:${maildir "${path}/archive"}"; + }) lists); + }; + }) + ]; + + meta.maintainers = with maintainers; [ V ]; +} diff --git a/fleet/modules/public-inbox.nix b/fleet/modules/public-inbox.nix new file mode 100644 index 0000000..a8aa06b --- /dev/null +++ b/fleet/modules/public-inbox.nix @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +# TODO(V): Figure out what the coderepo/cgit stuff is about +# TODO(V): Consider a different architecture to what we're currently using + +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.public-inbox; + + environment.PI_CONFIG = "${pkgs.writeText "public-inbox-config" (generators.toGitINI public-inbox-config)}"; + + # TODO(V): Port this to a Nix type + # $ng =~ m![^A-Za-z0-9/_\.\-\~\@\+\=:]! and + # die "--newsgroup `$ng' is not valid\n"; + # ($ng =~ m!\A\.! || $ng =~ m!\.\z!) and + # die "--newsgroup `$ng' must not start or end with `.'\n"; + + public-inbox-config = recursiveUpdate { + publicinbox = mapAttrs (inbox: config: { + address = [ "${inbox}@${config.domain}" ]; + url = "https://${config.domain}/${inbox}"; # TODO(V): Allow using a different location than this + inboxdir = "/var/lib/public-inbox/${inbox}.git"; + inherit (config) watch; + }) cfg.inboxes; + } cfg.settings; + + inboxOpts = { + options = { + description = mkOption { + description = "Description of the inbox."; + type = types.str; + }; + + domain = mkOption { + description = "Domain of the inbox."; + type = types.str; + example = "example.org"; + }; + + watch = mkOption { + description = "Directory to watch for incoming mails in."; + type = types.str; + }; + }; + }; +in { + options.services.public-inbox = { + enable = mkOption { + type = types.bool; + default = false; + }; + + inboxes = mkOption { + type = with types; attrsOf (submodule inboxOpts); + default = {}; + }; + + settings = mkOption { + type = types.attrs; # TODO: Use freeformType + default = {}; + }; + }; + + config = mkIf cfg.enable { + users.users.public-inbox = { + isSystemUser = true; + group = "public-inbox"; + + home = "/var/lib/public-inbox"; + createHome = true; + }; + + users.groups.public-inbox = {}; + + systemd.services.public-inbox-init = { + description = "public-inbox mailing-list archive (initialization)"; + + wantedBy = [ "public-inbox-watch.service" "multi-user.target" ]; + before = [ "public-inbox-watch.service" ]; + + # TODO(V): Add ${pi-config.inboxdir}/description to the reload conditions, since new descriptions don't get picked up after being updated. + script = concatStrings (mapAttrsToList (inbox: config: let pi-config = public-inbox-config.publicinbox.${inbox}; in '' + ${pkgs.public-inbox-init-lite}/bin/public-inbox-init-lite ${inbox} ${pi-config.inboxdir} ${head pi-config.address} + ln -sf ${pkgs.writeText "public-inbox-description" config.description} ${pi-config.inboxdir}/description + ${pkgs.public-inbox}/bin/public-inbox-index ${pi-config.inboxdir} + '') cfg.inboxes); + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + User = "public-inbox"; + }; + }; + + # FIXME(V): Currently, if new mail appears in the mlmmj archive while public-inbox-watch is not running, it never gets added! + # This directly contradicts what the manpage states: "Upon startup, it scans the mailbox for new messages to be imported while it was not running." + # This might be a bug in public-inbox! + # We currently save a copy of all mails received by mlmmj to ensure none are lost if this happens. + systemd.services.public-inbox-watch = { + description = "public-inbox mailing-list archive (incoming mail monitor)"; + wantedBy = [ "multi-user.target" ]; + inherit environment; + serviceConfig = { + ExecStart = "${pkgs.public-inbox}/bin/public-inbox-watch"; + # TODO(V): ExecReload + User = "public-inbox"; + SupplementaryGroups = [ "mlmmj" ]; + }; + }; + + # TODO(V): Consider using public-inbox.cgi + cgiserver instead? + systemd.sockets.public-inbox-httpd = { + description = "public-inbox mailing-list archive (web server)"; + listenStreams = [ "/run/public-inbox/httpd.sock" ]; + wantedBy = [ "sockets.target" ]; + }; + + systemd.services.public-inbox-httpd = { + description = "public-inbox mailing-list archive (web server)"; + inherit environment; + serviceConfig = { + ExecStart = "${pkgs.public-inbox}/bin/public-inbox-httpd"; + DynamicUser = true; + SupplementaryGroups = [ "public-inbox" ]; + }; + }; + }; + + meta.maintainers = with maintainers; [ V ]; +} diff --git a/fleet/modules/web.nix b/fleet/modules/web.nix new file mode 100644 index 0000000..709b1e4 --- /dev/null +++ b/fleet/modules/web.nix @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +{ lib, pkgs, ... }: + +{ + services.caddy = { + enable = true; + + # Snippets must be defined before they are used, so the mkBefore ensures they come first. + config = lib.mkBefore '' + (all) { + log { + output file /var/log/caddy/access.log + } + header -Server + } + + http:// { + import all + redir https://{host}{uri} 308 + } + + (common) { + import all + + encode zstd gzip + + header { + Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" + Content-Security-Policy "script-src 'none'; object-src 'none'" + Permissions-Policy "interest-cohort=()" + X-Clacks-Overhead "GNU Terry Pratchett" + } + + handle_errors { + respond "{http.error.status_code} {http.error.status_text}" + } + } + ''; + }; + + systemd.services.caddy.serviceConfig.LogsDirectory = "caddy"; + + networking.firewall.interfaces.ens3.allowedTCPPorts = [ 80 443 ]; +} diff --git a/fleet/pkgs/cgiserver/default.nix b/fleet/pkgs/cgiserver/default.nix new file mode 100644 index 0000000..9e911d5 --- /dev/null +++ b/fleet/pkgs/cgiserver/default.nix @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +{ lib, buildGoModule, fetchzip, zstd }: + +buildGoModule rec { + pname = "cgiserver"; + version = "1.0.0"; + + src = (fetchzip { + url = "https://src.anomalous.eu/cgiserver/snapshot/cgiserver-${version}.tar.zst"; + sha256 = "14bp92sw0w6n5dzs4f7g4fcklh25nc9k0xjx4ia0gi7kn5jwx2mq"; + }).overrideAttrs ({ nativeBuildInputs, ... }: { + nativeBuildInputs = nativeBuildInputs ++ [ zstd ]; + }); + + vendorSha256 = "00jslxzf6p8zs1wxdx3qdb919i80xv4w9ihljd40nnydasshqa4v"; + + meta = with lib; { + homepage = "https://src.anomalous.eu/cgiserver/about/"; + description = "Lightweight web server for sandboxing CGI applications"; + license = licenses.osl3; + maintainers = with maintainers; [ V ]; + }; +} diff --git a/fleet/pkgs/declarative-git-repository/default.nix b/fleet/pkgs/declarative-git-repository/default.nix new file mode 100644 index 0000000..f3bb014 --- /dev/null +++ b/fleet/pkgs/declarative-git-repository/default.nix @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +{ lib, writeTextDir, writeText, buildEnv, writeTextFile, bash, writeScript }: + +{ path +, branch ? "trunk" +, description ? "Unnamed repository; edit this file 'description' to name the repository." +, config ? {} +, hooks ? {} +, user ? "-", group ? "-" +}: + +with lib; + +let + # As generated by an initial `git init --bare` + defaultConfig = { + core = { + repositoryFormatVersion = 0; + fileMode = true; + bare = true; + }; + }; + + hooksDir = buildEnv { + name = "git-repository-hooks"; + paths = mapAttrsToList (hook: scripts: writeTextFile { + name = hook; + text = '' + #! ${bash}/bin/bash -e + '' + concatMapStrings (script: '' + ${script} "$@" + '') scripts; + destination = "/${hook}"; + executable = true; + }) hooks; + }; +in writeTextDir "lib/tmpfiles.d/git-repository${replaceStrings [ "/" ] [ "-" ] path}.conf" '' + # Root directory needs the correct permissions + d ${path} - ${user} ${group} + + # This is the smallest set of paths that Git will still recognise as a valid repository. + # Everything else will be automatically filled out after a push or pull. + f+ ${path}/HEAD - ${user} ${group} - ref: refs/heads/${branch} + d ${path}/objects - ${user} ${group} + d ${path}/refs - ${user} ${group} + + # Extra stuff we want to use + L+ ${path}/config - - - - ${writeText "git-repository-config" (generators.toGitINI (recursiveUpdate defaultConfig config))} + L+ ${path}/description - - - - ${builtins.toFile "git-repository-description" description} + L+ ${path}/hooks - - - - ${hooksDir} +'' diff --git a/fleet/pkgs/group-readable-archives.patch b/fleet/pkgs/group-readable-archives.patch new file mode 100644 index 0000000..84b3e07 --- /dev/null +++ b/fleet/pkgs/group-readable-archives.patch @@ -0,0 +1,22 @@ +SPDX-FileCopyrightText: V +SPDX-License-Identifier: OSL-3.0 +--- a/src/mlmmj-process.c ++++ b/src/mlmmj-process.c +@@ -490,6 +490,9 @@ + { NULL, 0, NULL } + }; + ++ /* Postfix unconditionally sets this to 0077 */ ++ umask(0027); ++ + CHECKFULLPATH(argv[0]); + + log_set_name(argv[0]); +@@ -553,7 +556,7 @@ + donemailname = concatstr(3, listdir, "/queue/", randomstr); + + donemailfd = open(donemailname, O_RDWR|O_CREAT|O_EXCL, +- S_IRUSR|S_IWUSR); ++ S_IRUSR|S_IWUSR|S_IRGRP); + + } while ((donemailfd < 0) && (errno == EEXIST)); diff --git a/fleet/pkgs/overlay.nix b/fleet/pkgs/overlay.nix new file mode 100644 index 0000000..1f645f0 --- /dev/null +++ b/fleet/pkgs/overlay.nix @@ -0,0 +1,24 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +final: prev: { + cgiserver = final.callPackage ./cgiserver {}; + declarative-git-repository = final.callPackage ./declarative-git-repository {}; + public-inbox = final.perlPackages.callPackage ./public-inbox {}; + public-inbox-init-lite = final.callPackage ./public-inbox-init-lite {}; + + # Fixes bundler complaining loudly if $HOME is read-only or unset + # Taken from https://github.com/rubygems/rubygems/pull/4724 + # This is here because the CGit about filter invokes Asciidoctor, + # which otherwise causes its log to fill with spurious error messages. + # Can be removed once Bundler 2.2.23 or above makes its way into stable. + bundler = prev.bundler.overrideAttrs ({ patches ? [], ... }: { + patches = patches ++ [ ./permission-warnings-only-when-necessary.patch ]; + dontBuild = false; + }); + + # Fixes archives having silly permissions due to Postfix messing with the umask + mlmmj = prev.mlmmj.overrideAttrs ({ patches ? [], ... }: { + patches = patches ++ [ ./group-readable-archives.patch ]; + }); +} diff --git a/fleet/pkgs/permission-warnings-only-when-necessary.patch b/fleet/pkgs/permission-warnings-only-when-necessary.patch new file mode 100644 index 0000000..4a557a5 --- /dev/null +++ b/fleet/pkgs/permission-warnings-only-when-necessary.patch @@ -0,0 +1,50 @@ +SPDX-FileCopyrightText: David Rodríguez +SPDX-License-Identifier: MIT +--- a/lib/bundler.rb ++++ b/lib/bundler.rb +@@ -236,8 +236,9 @@ def user_home + end + + if warning +- user_home = tmp_home_path(warning) +- Bundler.ui.warn "#{warning}\nBundler will use `#{user_home}' as your home directory temporarily.\n" ++ Bundler.ui.warn "#{warning}\n" ++ user_home = tmp_home_path ++ Bundler.ui.warn "Bundler will use `#{user_home}' as your home directory temporarily.\n" + user_home + else + Pathname.new(home) +@@ -684,15 +685,13 @@ def configure_gem_home + Bundler.rubygems.clear_paths + end + +- def tmp_home_path(warning) ++ def tmp_home_path + Kernel.send(:require, "tmpdir") + SharedHelpers.filesystem_access(Dir.tmpdir) do + path = Bundler.tmp + at_exit { Bundler.rm_rf(path) } + path + end +- rescue RuntimeError => e +- raise e.exception("#{warning}\nBundler also failed to create a temporary home directory':\n#{e}") + end + + # @param env [Hash] + +--- a/lib/bundler/settings.rb ++++ b/lib/bundler/settings.rb +@@ -428,12 +428,8 @@ def printable_value(value, key) + def global_config_file + if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? + Pathname.new(ENV["BUNDLE_CONFIG"]) +- else +- begin +- Bundler.user_bundle_path("config") +- rescue PermissionError, GenericSystemCallError +- nil +- end ++ elsif Bundler.rubygems.user_home && !Bundler.rubygems.user_home.empty? ++ Pathname.new(Bundler.rubygems.user_home).join(".bundle/config") + end + end diff --git a/fleet/pkgs/public-inbox-init-lite/default.nix b/fleet/pkgs/public-inbox-init-lite/default.nix new file mode 100644 index 0000000..8704ea3 --- /dev/null +++ b/fleet/pkgs/public-inbox-init-lite/default.nix @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +{ lib, substituteAll, public-inbox, runCommand, makeWrapper, git, xapian }: + +let + perl = public-inbox.fullperl.withPackages + (ps: with ps; [ public-inbox URI DBDSQLite SearchXapian ]); + + subbed = substituteAll { + src = ./public-inbox-init-lite; + isExecutable = true; + inherit (perl) interpreter; + }; +in runCommand "public-inbox-init-lite" { nativeBuildInputs = [ makeWrapper ]; } '' + makeWrapper ${subbed} $out/bin/public-inbox-init-lite \ + --prefix PATH : ${lib.makeBinPath [ git xapian ]} +'' diff --git a/fleet/pkgs/public-inbox-init-lite/public-inbox-init-lite b/fleet/pkgs/public-inbox-init-lite/public-inbox-init-lite new file mode 100644 index 0000000..f6fd560 --- /dev/null +++ b/fleet/pkgs/public-inbox-init-lite/public-inbox-init-lite @@ -0,0 +1,60 @@ +#! @interpreter@ -w +# SPDX-FileCopyrightText: (C) 2014-2021 all contributors +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use v5.10.1; +use Getopt::Long qw/:config gnu_getopt no_ignore_case auto_abbrev/; +use Fcntl qw(:DEFAULT); + +require PublicInbox::Admin; +PublicInbox::Admin::require_or_die('-base'); + +my ($indexlevel, $skip_epoch, $skip_artnum, $jobs, $skip_docdata); +my %opts = ( + 'indexlevel=s' => \$indexlevel, + 'skip-epoch=i' => \$skip_epoch, + 'skip-artnum=i' => \$skip_artnum, + 'jobs=i' => \$jobs, + 'skip-docdata' => \$skip_docdata, +); +GetOptions(%opts) or exit 1; +PublicInbox::Admin::indexlevel_ok_or_die($indexlevel) if defined $indexlevel; +my $name = shift @ARGV or exit 1; +my $inboxdir = shift @ARGV or exit 1; +my $primary_address = shift @ARGV or exit 1; +# TODO(V): Error if any more arguments are passed + +$inboxdir = PublicInbox::Config::rel2abs_collapsed($inboxdir); +die "`\\n' not allowed in `$inboxdir'\n" if index($inboxdir, "\n") >= 0; + +if (-d "$inboxdir/objects") { + die "$inboxdir is a -V1 inbox\n" +} + +my $ibx = PublicInbox::Inbox->new({ + inboxdir => $inboxdir, + name => $name, + version => 2, + -primary_address => $primary_address, + indexlevel => $indexlevel, +}); + +my $creat_opt = {}; +if (defined $jobs) { + die "--jobs=$jobs must be >= 1\n" if $jobs <= 0; + $creat_opt->{nproc} = $jobs; +} + +require PublicInbox::InboxWritable; +$ibx = PublicInbox::InboxWritable->new($ibx, $creat_opt); +if ($skip_docdata) { + $ibx->{indexlevel} //= 'full'; # ensure init_inbox writes xdb + $ibx->{indexlevel} eq 'basic' and + die "--skip-docdata ignored with --indexlevel=basic\n"; + $ibx->{-skip_docdata} = $skip_docdata; +} +$ibx->init_inbox(0, $skip_epoch, $skip_artnum); + +require PublicInbox::Spawn; +PublicInbox::Spawn->import(qw(run_die)); diff --git a/fleet/pkgs/public-inbox/default.nix b/fleet/pkgs/public-inbox/default.nix new file mode 100644 index 0000000..bb5db29 --- /dev/null +++ b/fleet/pkgs/public-inbox/default.nix @@ -0,0 +1,45 @@ +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +# TODO(V): Enable highlighting support + +{ lib, buildPerlPackage, fetchurl, makeWrapper +, git, xapian +, URI, DBDSQLite, SearchXapian, Plack, PlackMiddlewareReverseProxy +, sqlite # Only used in tests +}: + +buildPerlPackage rec { + pname = "public-inbox"; + version = "unstable-2021-02-10"; + + # We need at least fa3f0cbcd1af5008e56c77e3c46ab60b5eca3a13 for public-inbox-watch to work with mlmmj's archive directory at all. + # See also: https://public-inbox.org/meta/CAMwyc-SmvBoVOs+vCMNaWOWPT3TCB-7rJ_0bp43QB+pjzbNv-w@mail.gmail.com/ + src = fetchurl { + url = "https://public-inbox.org/public-inbox.git/snapshot/public-inbox-fa3f0cbcd1af5008e56c77e3c46ab60b5eca3a13.tar.gz"; + sha256 = "03bynml6gw4936cri31ywqq5ackzkjjggksvpqf220xbcl55w93q"; + }; + + nativeBuildInputs = [ makeWrapper ]; + buildInputs = [ URI DBDSQLite SearchXapian Plack PlackMiddlewareReverseProxy ]; + + checkInputs = [ git sqlite xapian ]; + # TODO(edef): Only exclude the individual failing tests, not the entire file + preCheck = '' + rm t/search.t # Relies on set-gid, which is unavailable in the build sandbox. + rm t/spawn.t # Tries to setpgid to that of pid 1, which (unexpectedly for the test) succeeds in the sandbox. + ''; + + postFixup = '' + for x in $out/bin/*; do + wrapProgram $x --prefix PATH : ${lib.makeBinPath [ git xapian ]} + done + ''; + + meta = with lib; { + homepage = "https://public-inbox.org/README.html"; + description = "Git-based mailing-list archive"; + license = licenses.agpl3Plus; + maintainers = with maintainers; [ V ]; + }; +} diff --git a/fleet/test b/fleet/test new file mode 100755 index 0000000..d8e2a87 --- /dev/null +++ b/fleet/test @@ -0,0 +1,7 @@ +#! /bin/sh +# SPDX-FileCopyrightText: V +# SPDX-License-Identifier: OSL-3.0 + +git add . && git commit -m WIP +git push -f vityaz trunk && ssh vityaz-root nixos-rebuild test --show-trace +git push -f trieste trunk && ssh trieste-root nixos-rebuild test --show-trace -- cgit 1.4.1