summary refs log tree commit diff stats
path: root/system/linde.nix
diff options
context:
space:
mode:
Diffstat (limited to 'system/linde.nix')
-rw-r--r--system/linde.nix870
1 files changed, 505 insertions, 365 deletions
diff --git a/system/linde.nix b/system/linde.nix
index 765307cc..067d4dad 100644
--- a/system/linde.nix
+++ b/system/linde.nix
@@ -15,12 +15,22 @@ let
   net-rdnsip = "2a01:4f8:c012:23a4::53";
   net-mask6 = "64";
   net-gw6 = "fe80::1";
+  ts-domain = "hydra-pinecone.ts.net";
+  golink = (builtins.getFlake (toString <golink>)).nixosModules.default;
 in
 {
   imports =
     [
+      <personal/modules/laminar.nix>
+      <home-manager/nixos>
+      <agenix/modules/age.nix>
+      <searchix/nix/modules>
+      golink
       # Include the results of the hardware scan.
       ./linde-hardware.nix
+
+      ./settings/pin.nix
+      ./settings/services/git-server.nix
     ];
   age.secrets = {
     paperless =
@@ -36,7 +46,16 @@ in
       };
     acme.file = ../secrets/acme.age;
     binarycache.file = ../secrets/binarycache.age;
+    dex.file = ../secrets/dex.age;
     powerdns.file = ../secrets/powerdns.age;
+    golink = let golink = config.services.golink; in {
+      # hope this doesn't collide...
+      path = "${golink.dataDir}/.config/tsnet-golink/auth.key";
+      owner = golink.user;
+      mode = "400";
+      symlink = false;
+      file = ../secrets/golink.age;
+    };
   };
 
   # Use the systemd-boot EFI boot loader.
@@ -54,7 +73,6 @@ in
   environment.systemPackages = with pkgs; [
     htop
     lsof
-    gitMinimal
     powerdns
     sqlite-interactive
     knot-dns
@@ -63,51 +81,6 @@ in
     nix-output-monitor
   ];
 
-  programs.ssh = with pkgs; {
-    knownHostsFiles = [
-      (writeText "github.keys" ''
-        # github.com:22 SSH-2.0-babeld-05989c77
-        # github.com:22 SSH-2.0-babeld-05989c77
-        # github.com:22 SSH-2.0-babeld-05989c77
-        # github.com:22 SSH-2.0-babeld-05989c77
-        # github.com:22 SSH-2.0-babeld-05989c77
-        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
-        github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
-        github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
-      '')
-      (writeText "gitlab.keys" ''
-        # gitlab.com:22 SSH-2.0-GitLab-SSHD
-        # gitlab.com:22 SSH-2.0-GitLab-SSHD
-        # gitlab.com:22 SSH-2.0-GitLab-SSHD
-        # gitlab.com:22 SSH-2.0-GitLab-SSHD
-        # gitlab.com:22 SSH-2.0-GitLab-SSHD
-        gitlab.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsj2bNKTBSpIYDEGk9KxsGh3mySTRgMtXL583qmBpzeQ+jqCMRgBqB98u3z++J1sKlXHWfM9dyhSevkMwSbhoR8XIq/U0tCNyokEi/ueaBMCvbcTHhO7FcwzY92WK4Yt0aGROY5qX2UKSeOvuP4D6TPqKF1onrSzH9bx9XUf2lEdWT/ia1NEKjunUqu1xOB/StKDHMoX4/OKyIzuS0q/T1zOATthvasJFoPrAjkohTyaDUz2LN5JoH839hViyEG82yB+MjcFV5MU3N1l1QL3cVUCh93xSaua1N85qivl+siMkPGbO5xR/En4iEY6K2XPASUEMaieWVNTRCtJ4S8H+9
-        gitlab.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBFSMqzJeV9rUzU4kWitGjeR4PWSa29SPqJ1fVkhtj3Hw9xjLVXVYrU9QlYWrOLXBpQ6KWjbjTDTdDkoohFzgbEY=
-        gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf
-      '')
-      (writeText "codeberg.keys" ''
-        # codeberg.org:22 SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2
-        # codeberg.org:22 SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2
-        # codeberg.org:22 SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2
-        # codeberg.org:22 SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2
-        # codeberg.org:22 SSH-2.0-OpenSSH_9.2p1 Debian-2+deb12u2
-        codeberg.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC8hZi7K1/2E2uBX8gwPRJAHvRAob+3Sn+y2hxiEhN0buv1igjYFTgFO2qQD8vLfU/HT/P/rqvEeTvaDfY1y/vcvQ8+YuUYyTwE2UaVU5aJv89y6PEZBYycaJCPdGIfZlLMmjilh/Sk8IWSEK6dQr+g686lu5cSWrFW60ixWpHpEVB26eRWin3lKYWSQGMwwKv4LwmW3ouqqs4Z4vsqRFqXJ/eCi3yhpT+nOjljXvZKiYTpYajqUC48IHAxTWugrKe1vXWOPxVXXMQEPsaIRc2hpK+v1LmfB7GnEGvF1UAKnEZbUuiD9PBEeD5a1MZQIzcoPWCrTxipEpuXQ5Tni4mN
-        codeberg.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBL2pDxWr18SoiDJCGZ5LmxPygTlPu+cCKSkpqkvCyQzl5xmIMeKNdfdBpfbCGDPoZQghePzFZkKJNR/v9Win3Sc=
-        codeberg.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIVIC02vnjFyL+I4RHfvIGNtOgJMe769VTF1VR4EB3ZB
-      '')
-      (writeText "sr.ht.keys" ''
-        # git.sr.ht:22 SSH-2.0-OpenSSH_9.6
-        # git.sr.ht:22 SSH-2.0-OpenSSH_9.6
-        # git.sr.ht:22 SSH-2.0-OpenSSH_9.6
-        # git.sr.ht:22 SSH-2.0-OpenSSH_9.6
-        # git.sr.ht:22 SSH-2.0-OpenSSH_9.6
-        git.sr.ht ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDZ+l/lvYmaeOAPeijHL8d4794Am0MOvmXPyvHTtrqvgmvCJB8pen/qkQX2S1fgl9VkMGSNxbp7NF7HmKgs5ajTGV9mB5A5zq+161lcp5+f1qmn3Dp1MWKp/AzejWXKW+dwPBd3kkudDBA1fa3uK6g1gK5nLw3qcuv/V4emX9zv3P2ZNlq9XRvBxGY2KzaCyCXVkL48RVTTJJnYbVdRuq8/jQkDRA8lHvGvKI+jqnljmZi2aIrK9OGT2gkCtfyTw2GvNDV6aZ0bEza7nDLU/I+xmByAOO79R1Uk4EYCvSc1WXDZqhiuO2sZRmVxa0pQSBDn1DB3rpvqPYW+UvKB3SOz
-        git.sr.ht ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCj6y+cJlqK3BHZRLZuM+KP2zGPrh4H66DacfliU1E2DHAd1GGwF4g1jwu3L8gOZUTIvUptqWTkmglpYhFp4Iy4=
-        git.sr.ht ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60
-      '')
-    ];
-  };
-
   # Initial empty root password for easy login:
   users.users.root.initialHashedPassword = "";
   services.openssh = {
@@ -125,13 +98,14 @@ in
   programs.mosh.enable = true;
 
   system.autoUpgrade = {
-    enable = true;
+    enable = false;
     dates = "02:10";
     randomizedDelaySec = "59 min";
     allowReboot = true;
     flake = "git+file://${config.services.gitolite.dataDir}/repositories/nixfiles.git";
     flags = [
       "--no-write-lock-file"
+      "--impure"
       "--update-input"
       "nixpkgs-small"
       "--update-input"
@@ -168,6 +142,7 @@ in
 
   services.nix-serve = {
     enable = true;
+    package = pkgs.nix-serve-ng;
     secretKeyFile = config.age.secrets.binarycache.path;
   };
 
@@ -209,51 +184,111 @@ in
       ];
       allowedUDPPorts = [
         53
+        443 # HTTP/3 (QUIC)
         3478
         6885 # DHT
         6922
       ];
+      trustedInterfaces = [ "tailscale0" ];
     };
     resolvconf = {
-      enable = true;
+      enable = false;
       useLocalResolver = false;
     };
   };
-  services.resolved.enable = false;
+  services.resolved = {
+    enable = true;
+    llmnr = "false";
+    dnssec = "true";
+  };
   systemd.network = {
     enable = true;
     networks.${netif} =
       {
         name = netif;
-        gateway = [ net-gw ];
-        routes = [{
-          routeConfig = {
+        routes = [
+          {
             Gateway = net-gw6;
             PreferredSource = net-ip6;
-          };
-        }];
+            QuickAck = true;
+            InitialCongestionWindow = 30;
+            InitialAdvertisedReceiveWindow = 30;
+          }
+          {
+            Gateway = net-gw;
+            QuickAck = true;
+            InitialCongestionWindow = 30;
+            InitialAdvertisedReceiveWindow = 30;
+          }
+        ];
         address = [
           "${net-ip6}/${net-mask6}"
           "${net-rdnsip}/${net-mask6}"
         ];
         addresses = [{
-          addressConfig = {
-            Address = "${net-ip4}/${net-mask4}";
-            Peer = "${net-gw}/32";
-          };
+          Address = "${net-ip4}/${net-mask4}";
+          Peer = "${net-gw}/32";
         }];
       };
+    wait-online = {
+      extraArgs = [ "--interface=${netif}" ];
+    };
+  };
+
+  services.tailscale = {
+    enable = true;
+    extraUpFlags = [ "--accept-routes" ];
+    useRoutingFeatures = "client";
+  };
+  services.golink = {
+    enable = true;
+    tailscaleAuthKeyFile = config.age.secrets.golink.path;
   };
 
   services.journald.extraConfig = ''
     MaxRetentionSec=1 month
   '';
 
-  boot.kernel.sysctl = {
-    "net.ipv4.tcp_allowed_congestion_control" = "bbr illinois reno";
-    "net.ipv4.tcp_congestion_control" = "bbr";
-    "net.core.default_qdisc" = "fq";
-  };
+  zramSwap = {
+    enable = true;
+    algorithm = "zstd";
+  };
+
+  boot.kernel.sysctl =
+    let
+      buffer_size = 16 * 1024 * 1024;
+      server_count = 2;
+      max_clients = 100;
+      page_size = 4096;
+      # This server might have 100 clients simultaneously, so:
+      #   max(tcp_wmem) * 2 * 100 / 4096
+      mem = toString (buffer_size * server_count * max_clients / page_size);
+    in
+    {
+      "net.ipv4.tcp_allowed_congestion_control" = "bbr illinois reno";
+      "net.ipv4.tcp_congestion_control" = "bbr";
+      "net.core.default_qdisc" = "fq";
+
+      # Provide adequate buffer memory.
+      # rmem_max and wmem_max are TCP max buffer size
+      # settable with setsockopt(), in bytes
+      # tcp_rmem and tcp_wmem are per socket in bytes.
+      # tcp_mem is for all TCP streams, in 4096-byte pages.
+      # The following are suggested on IBM's
+      # High Performance Computing page
+      "net.core.rmem_max" = buffer_size;
+      "net.core.wmem_max" = buffer_size;
+      "net.core.rmem_default" = buffer_size;
+      "net.core.wmem_default" = buffer_size;
+      "net.ipv4.tcp_rmem" = "4096 87380 ${toString buffer_size}";
+      "net.ipv4.tcp_wmem" = "4096 87380 ${toString buffer_size}";
+      "net.ipv4.tcp_mem" = "${mem} ${mem} ${mem}";
+
+      "net.ipv4.tcp_sack" = false;
+      "net.ipv4.tcp_dsack" = false;
+
+      "net.ipv4.tcp_slow_start_after_idle" = false;
+    };
 
   security.sudo.execWheelOnly = true;
   security.sudo.extraConfig = ''
@@ -274,7 +309,7 @@ in
   users.users.root.shell = "${pkgs.fish}/bin/fish";
   users.users.alan = {
     shell = "${pkgs.fish}/bin/fish";
-    extraGroups = [ "wheel" "caddy" "docker" ];
+    extraGroups = [ "wheel" "caddy" "docker" "laminar" ];
     isNormalUser = true;
     home = "/home/alan";
     createHome = true;
@@ -283,6 +318,9 @@ in
       "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8VIII+598QOBxi/52O1Kb19RdUdX0aZmS1/dNoyqc5 alan@hetzner.strongbox"
     ];
   };
+  home-manager = {
+    users.alan = import ../user/server.nix;
+  };
 
   users.users.nixremote = {
     shell = "/bin/sh";
@@ -294,6 +332,7 @@ in
       "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBmDSZnUzIPQowLrKSa24eSb1WFQe7yPjTcDPPe3UY0Q nix@mba"
       "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE9of82WBHK8nr8L9RGeieLMfcAWaFCeCkmvYHM9LCuT nanopi"
       "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIy9jFioBvV0JA0lc+De2N+vDOABGHgCECW6vkD33CE4 sourcehut"
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII7sWEwsm8JZiJ0LUnjSt0Kg1RXypG6p5AzP/R2n5ca actions@github.com"
     ];
   };
 
@@ -305,40 +344,53 @@ in
   # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
   system.stateVersion = "23.05"; # Did you read the comment?
 
-  services.powerdns = {
-    enable = true;
-    secretFile = config.age.secrets.powerdns.path;
-    extraConfig = ''
-      launch=gsqlite3
-      dnsupdate=yes
-      allow-dnsupdate-from=0.0.0.0/0,::/0
-      only-notify=
-      also-notify=216.218.130.2
-      allow-axfr-ips=216.218.133.2,2001:470:600::2
-      outgoing-axfr-expand-alias=yes
-      expand-alias=yes
-      resolver=1.1.1.1
-      local-address=${net-ip4} ${net-ip6}
-      reuseport=yes
-      log-dns-details=no
-      log-dns-queries=no
-      loglevel=5
-      primary=yes
-      secondary=yes
-      send-signed-notify=no
-      prevent-self-notification=no
-
-      default-soa-edit=inception-increment
-
-      api=yes
-      # replaced by secretFile/envsubst
-      api-key=$API_KEY
-
-      gsqlite3-database=/var/db/pdns/zones.db
-      gsqlite3-pragma-foreign-keys=yes
-      gsqlite3-dnssec=yes
-    '';
-  };
+  services.powerdns =
+    let
+      inherit (lib.lists) flatten;
+      inherit (lib.strings) concatStringsSep;
+      he = {
+        notify = "216.218.130.2";
+        axfr = [
+          "216.218.133.2"
+          "2001:470:600::2"
+        ];
+      };
+      iplist = ips: concatStringsSep "," (flatten ips);
+    in
+    {
+      enable = true;
+      secretFile = config.age.secrets.powerdns.path;
+      extraConfig = ''
+        launch=gsqlite3
+        dnsupdate=yes
+        allow-dnsupdate-from=0.0.0.0/0,::/0
+        only-notify=
+        also-notify=${iplist [ he.notify ]}
+        allow-axfr-ips=${iplist [ he.axfr ]}
+        outgoing-axfr-expand-alias=yes
+        expand-alias=yes
+        resolver=1.1.1.1
+        local-address=${net-ip4} ${net-ip6}
+        reuseport=yes
+        log-dns-details=no
+        log-dns-queries=no
+        loglevel=5
+        primary=yes
+        secondary=yes
+        send-signed-notify=no
+        prevent-self-notification=no
+
+        default-soa-edit=inception-increment
+
+        api=yes
+        # replaced by secretFile/envsubst
+        api-key=$API_KEY
+
+        gsqlite3-database=/var/db/pdns/zones.db
+        gsqlite3-pragma-foreign-keys=yes
+        gsqlite3-dnssec=yes
+      '';
+    };
 
   systemd.services.hagezi-blocklist-update = {
     enable = true;
@@ -497,21 +549,6 @@ in
     };
   };
 
-  systemd.services.backup-etc-nixos = {
-    startAt = "04:30";
-    path = with pkgs; [
-      rdiff-backup
-      openssh
-    ];
-    script = ''
-      rdiff-backup --api-version 201 backup /etc/nixos ${hostname}@home.alanpearce.eu::nixos
-      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::nixos
-    '';
-    serviceConfig = {
-      Type = "oneshot";
-    };
-  };
-
   systemd.services.backup-gitolite = {
     startAt = "daily";
     path = with pkgs; [
@@ -554,7 +591,7 @@ in
     };
     acceptTerms = true;
     certs."alanpearce.eu" = {
-      extraDomainNames = [ "*.alanpearce.eu" ];
+      extraDomainNames = [ "*.alanpearce.eu" "*.linde.alanpearce.eu" ];
     };
     certs."dns.alanpearce.eu" = {
       reloadServices = map (x: "kresd@${toString x}") (range 1 config.services.kresd.instances);
@@ -572,259 +609,296 @@ in
       auto_https disable_certs
       default_bind ${net-ip6} ${net-ip4}
     '';
-    virtualHosts = {
-      "http://" = {
-        # Needed for HTTP->HTTPS servers
-      };
-      "${hostname}.alanpearce.eu" = {
-        serverAliases = [ "https://" ];
-        useACMEHost = "alanpearce.eu";
-        extraConfig = ''
-          respond * 204
-        '';
-      };
-      "pdns.alanpearce.eu" = {
-        useACMEHost = "alanpearce.eu";
-        extraConfig = ''
-          log {
-            output discard
-          }
-          reverse_proxy 127.0.0.1:8081
-        '';
-      };
-      "dns.alanpearce.eu" = {
-        useACMEHost = "alanpearce.eu";
-        extraConfig = ''
-          log {
-            output discard
-          }
-          reverse_proxy localhost:443 {
-            transport http {
-              tls_server_name dns.alanpearce.eu
-            }
-          }
-        '';
-      };
-      "files.alanpearce.eu" = {
-        useACMEHost = "alanpearce.eu";
-        extraConfig = ''
-          encode zstd gzip
-          root * /srv/http/files
-          file_server browse
-        '';
-      };
-      "git.alanpearce.eu" =
-        let
-          fcgi = config.services.fcgiwrap;
-          fcgisocket = "${fcgi.socketType}/${fcgi.socketAddress}";
-        in
-        {
+    virtualHosts =
+      let
+        inherit (import ../lib/caddy.nix { inherit lib; }) security-headers;
+      in
+      {
+        "http://" = {
+          # Needed for HTTP->HTTPS servers
+        };
+        "alanpearce.eu" = {
+          serverAliases = [ "www.alanpearce.eu" "test.alanpearce.eu" ];
           useACMEHost = "alanpearce.eu";
           extraConfig = ''
-            root * ${pkgs.cgit-pink}/cgit/
             encode zstd gzip
-            handle_path /custom/* {
-              file_server {
-                root /srv/http/cgit/
-              }
+            root * /srv/http/website/public
+            file_server
+            ${security-headers {}}
+            handle_errors {
+              rewrite * /404.html
+              file_server
             }
-            rewrite /robots.txt /assets/robots.txt
-            handle_path /assets/* {
-              file_server  {
-                hide cgit.cgi
-              }
+          '';
+        };
+        "${hostname}.alanpearce.eu" = {
+          serverAliases = [ "https://" ];
+          useACMEHost = "alanpearce.eu";
+          extraConfig = ''
+            respond * 204
+            ${security-headers {}}
+          '';
+        };
+        "pdns.alanpearce.eu" = {
+          useACMEHost = "alanpearce.eu";
+          extraConfig = ''
+            log {
+              output discard
             }
-            @git_http_backend path_regexp "^/.+/(info/refs|git-upload-pack)$"
-            handle @git_http_backend {
-              reverse_proxy ${fcgisocket} {
-                transport fastcgi {
-                  env SCRIPT_FILENAME ${pkgs.git}/libexec/git-core/git-http-backend
-                  env GIT_PROJECT_ROOT ${config.services.gitolite.dataDir}/repositories
-                }
-              }
+            reverse_proxy 127.0.0.1:8081
+          '';
+        };
+        "id.alanpearce.eu" = {
+          useACMEHost = "alanpearce.eu";
+          extraConfig = ''
+            encode zstd gzip
+            ${security-headers {}}
+            reverse_proxy http://${config.services.dex.settings.web.http}
+          '';
+        };
+        "dns.alanpearce.eu" = {
+          useACMEHost = "alanpearce.eu";
+          extraConfig = ''
+            log {
+              output discard
             }
-            handle {
-              reverse_proxy ${fcgisocket} {
-                transport fastcgi {
-                  env       SCRIPT_FILENAME  {http.vars.root}/cgit.cgi
-                  env       CGIT_CONFIG      ${pkgs.writeText "cgitrc" ''
-                    head-include=/srv/http/cgit/responsive-cgit-css-master/head.html
-                    css=/custom/responsive-cgit-css-master/cgit.css
-                    virtual-root=/
-                    logo=
-                    readme=:README.md
-                    source-filter=${pkgs.cgit-pink}/lib/cgit/filters/syntax-highlighting.py
-                    about-filter=${pkgs.cgit-pink}/lib/cgit/filters/about-formatting.sh
-                    enable-git-config=1
-                    enable-index-owner=0
-                    enable-index-links=1
-                    enable-follow-links=0
-                    enable-log-linecount=1
-                    max-stats=year
-                    snapshots=tar.lz tar.zst zip
-                    cache-size=10240
-                    enable-http-clone=1
-                    enable-commit-graph=1
-                    mimetype-file=${pkgs.nginx}/conf/mime.types
-                    section-from-path=1
-                    noplainemail=1
-                    repository-sort=age
-                    root-title=my personal projects
-                    clone-url=git://git.alanpearce.eu/$CGIT_REPO_URL https://git.alanpearce.eu/$CGIT_REPO_URL
-                    remove-suffix=1
-                    strict-export=git-daemon-export-ok
-                    scan-path=${config.services.gitolite.dataDir}/repositories/
-                  ''}
-                  }
-                }
+            encode zstd gzip
+            reverse_proxy localhost:443 {
+              transport http {
+                tls_server_name dns.alanpearce.eu
+              }
             }
           '';
         };
-      "ntfy.alanpearce.eu" = {
-        useACMEHost = "alanpearce.eu";
-        extraConfig = ''
-          encode zstd gzip
-          reverse_proxy localhost${config.services.ntfy-sh.settings.listen-http}
-        '';
-      };
-      "searchix.alanpearce.eu" = {
-        useACMEHost = "alanpearce.eu";
-        extraConfig = ''
-          reverse_proxy localhost:${toString config.services.searchix.settings.web.port} {
-            health_uri /health
-            health_status 2xx
-          }
-          encode zstd gzip {
-            match {
-              header Content-Type text/*
-              header Content-Type application/json*
-              header Content-Type application/javascript*
-              header Content-Type application/opensearchdescription+xml
-              header Content-Type application/atom+xml*
-              header Content-Type application/rss+xml*
-              header Content-Type image/svg+xml*
-            }
-          }
-        '';
-      };
-      "legit.alanpearce.eu" =
-        let
-          server = config.services.legit.settings.server;
-        in
-        {
+        "files.alanpearce.eu" = {
           useACMEHost = "alanpearce.eu";
           extraConfig = ''
             encode zstd gzip
-            handle_path /static/* {
-              root * /srv/http/legit/src/static
-              file_server
+            ${security-headers {}}
+            root * /srv/http/files
+            file_server browse
+          '';
+        };
+        "ntfy.alanpearce.eu" = {
+          useACMEHost = "alanpearce.eu";
+          extraConfig = ''
+            encode zstd gzip
+            ${security-headers {}}
+            reverse_proxy localhost${config.services.ntfy-sh.settings.listen-http} {
+              health_uri /v1/health
+              health_body `"healthy":true`
             }
-            reverse_proxy ${server.host}:${toString server.port}
           '';
         };
-      "papers.alanpearce.eu" = {
-        extraConfig = ''
-          encode zstd gzip
-          handle_path /static/* {
-            root * ${config.services.paperless.package}/lib/paperless-ngx/static
-            file_server
-          }
-          reverse_proxy localhost:${toString config.services.paperless.port}
-
-        '';
-      };
-      "binarycache.alanpearce.eu" =
-        let
-          ns = config.services.nix-serve;
-        in
-        {
+        "searchix.alanpearce.eu" = {
+          useACMEHost = "alanpearce.eu";
+          serverAliases = [ "searchix.linde.alanpearce.eu" ];
           extraConfig = ''
-            reverse_proxy ${ns.bindAddress}:${toString ns.port}
+            reverse_proxy localhost:${toString config.services.searchix.settings.web.port} {
+              health_uri /health
+              health_status 2xx
+            }
+            encode zstd gzip {
+              match {
+                header Content-Type text/*
+                header Content-Type application/json*
+                header Content-Type application/javascript*
+                header Content-Type application/opensearchdescription+xml
+                header Content-Type application/atom+xml*
+                header Content-Type application/rss+xml*
+                header Content-Type image/svg+xml*
+              }
+            }
           '';
         };
-    };
+        "binarycache.alanpearce.eu" =
+          let
+            ns = config.services.nix-serve;
+          in
+          {
+            extraConfig = ''
+              reverse_proxy ${ns.bindAddress}:${toString ns.port}
+            '';
+          };
+        "ci.alanpearce.eu" =
+          let
+            srv = config.services.laminar;
+          in
+          {
+            extraConfig = ''
+              reverse_proxy ${srv.settings.bindHTTP}
+            '';
+          };
+      };
   };
   systemd.services.caddy.serviceConfig = {
     UMask = "007";
   };
 
-  services.fcgiwrap = {
+  networking.nat = {
     enable = true;
-    group = "gitolite";
-    preforkProcesses = 2;
-    socketType = "tcp6";
-    socketAddress = "[::1]:9000";
+    internalInterfaces = [ "ve-+" ];
+    externalInterface = netif;
+    enableIPv6 = true;
   };
-  services.gitolite = {
-    enable = true;
-    adminPubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8VIII+598QOBxi/52O1Kb19RdUdX0aZmS1/dNoyqc5 alan@hetzner.strongbox";
-    extraGitoliteRc = ''
-      $RC{UMASK} = 0027;
-      $RC{LOG_EXTRA} = 0;
-      $RC{HOSTNAME} = "${config.networking.hostName}";
-      $RC{LOCAL_CODE} = "$rc{GL_ADMIN_BASE}/local";
-      push( @{$RC{ENABLE}}, 'D' );
-      push( @{$RC{ENABLE}}, 'Shell alan' );
-      push( @{$RC{ENABLE}}, 'cgit' );
-      push( @{$RC{ENABLE}}, 'repo-specific-hooks' );
-    '';
+
+  users.users.paperless = {
+    group = "paperless";
+    uid = config.ids.uids.paperless;
+    home = "/srv/paperless";
   };
-  services.legit = {
-    enable = true;
-    group = "gitolite";
-    settings = {
-      server.name = "legit.alanpearce.eu";
-      dirs = {
-        templates = "/srv/http/legit/src/templates";
+  users.groups.paperless.members = [ "alan" "syncthing" ];
+  containers.papers =
+    let
+      hostDataDir = config.users.users.paperless.home;
+      localAddress6 = "fc00::2";
+      tsHostname = "papers.${ts-domain}";
+      tsPort = 41642;
+    in
+    {
+      # or maybe socket activated?
+      autoStart = true;
+      # does TS need this?
+      enableTun = true;
+      privateNetwork = true;
+      hostAddress6 = "fc00::1";
+      inherit localAddress6;
+      forwardPorts = [{
+        hostPort = tsPort;
+      }];
+      bindMounts = {
+        ${config.services.paperless.dataDir} = {
+          hostPath = hostDataDir;
+          isReadOnly = false;
+        };
       };
-      repo = {
-        scanPath = "/srv/http/legit/repos";
-        readme = [
-          "readme"
-          "readme.md"
-          "README.md"
+      config = {
+        environment.systemPackages = with pkgs; [
+          lsof
         ];
+        networking = {
+          useHostResolvConf = false;
+          resolvconf.enable = false;
+          firewall.trustedInterfaces = [ "tailscale0" ];
+          firewall.rejectPackets = true;
+          nameservers = config.networking.nameservers;
+        };
+        services.resolved = {
+          enable = true;
+          llmnr = "false";
+        };
+        services.tailscale = {
+          enable = true;
+          openFirewall = true;
+          permitCertUid = "caddy";
+          port = tsPort;
+        };
+        services.caddy = {
+          enable = true;
+          email = "caddy@alanpearce.eu";
+          virtualHosts = {
+            "http://" = {
+              # avoid logging to an awkward file name based on the attribute name i.e. http://
+              hostName = "papers";
+              extraConfig = ''
+                redir ${tsHostname}{uri}
+              '';
+            };
+            ${tsHostname} = {
+              extraConfig = ''
+                encode zstd gzip
+                tls {
+                  get_certificate tailscale
+                }
+                handle_path /static/* {
+                  root * ${config.services.paperless.package}/lib/paperless-ngx/static
+                  file_server
+                }
+                reverse_proxy [::1]:${toString config.services.paperless.port}
+              '';
+            };
+          };
+        };
+        services.paperless = {
+          enable = true;
+          address = "[::1]";
+          settings = {
+            PAPERLESS_DBENGINE = "sqlite";
+            PAPERLESS_TIME_ZONE = "Europe/Berlin";
+
+            PAPERLESS_URL = "https://${tsHostname}";
+            PAPERLESS_TRUSTED_PROXIES = "[::1]";
+            PAPERLESS_USE_X_FORWARD_HOST = true;
+            PAPERLESS_USE_X_FORWARD_PORT = true;
+            PAPERLESS_PROXY_SSL_HEADER = [ "HTTP_X_FORWARDED_PROTO" "https" ];
+            PAPERLESS_ENABLE_COMPRESSION = false; # let caddy do it
+
+            PAPERLESS_OCR_SKIP_ARCHIVE_FILE = "with_text";
+            PAPERLESS_OCR_LANGUAGE = "deu+eng";
+            PAPERLESS_IGNORE_DATES = "09.08.90";
+
+            PAPERLESS_TASK_WORKERS = 2;
+            PAPERLESS_THREADS_PER_WORKER = 1;
+            PAPERLESS_NUMBER_OF_SUGGESTED_DATES = 4;
+
+            PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ".stfolder/*" ".stversions/*" ];
+
+            PAPERLESS_FILENAME_FORMAT = "{correspondent}/{created} {title} {asn}";
+            PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = true;
+          };
+        };
+        system.stateVersion = "24.11";
       };
     };
-  };
-  users.groups.git.gid = config.ids.gids.git;
-  services.gitDaemon = {
-    enable = true;
-    user = "git";
-    group = "gitolite";
-    basePath = "${config.services.gitolite.dataDir}/repositories/";
-  };
 
-  users.groups.paperless.members = [ "alan" "syncthing" ];
-  services.paperless = {
+  services.etcd = {
     enable = true;
-    package = pkgs.paperless-ngx;
-    dataDir = "/srv/paperless";
-    settings = {
-      PAPERLESS_DBENGINE = "sqlite";
-      PAPERLESS_TIME_ZONE = "Europe/Berlin";
-
-      PAPERLESS_URL = "https://papers.alanpearce.eu";
-      PAPERLESS_TRUSTED_PROXIES = "127.0.0.1";
-      PAPERLESS_USE_X_FORWARD_HOST = true;
-      PAPERLESS_USE_X_FORWARD_PORT = true;
-      PAPERLESS_PROXY_SSL_HEADER = [ "HTTP_X_FORWARDED_PROTO" "https" ];
-      PAPERLESS_ENABLE_COMPRESSION = false; # let caddy do it
-
-      PAPERLESS_OCR_SKIP_ARCHIVE_FILE = "with_text";
-      PAPERLESS_OCR_LANGUAGE = "deu+eng";
-      PAPERLESS_IGNORE_DATES = "09.08.90";
-
-      PAPERLESS_TASK_WORKERS = 2;
-      PAPERLESS_THREADS_PER_WORKER = 1;
-      PAPERLESS_NUMBER_OF_SUGGESTED_DATES = 4;
-
-      PAPERLESS_CONSUMER_IGNORE_PATTERN = [ ".DS_STORE/*" "desktop.ini" ".stfolder/*" ".stversions/*" ];
+    initialClusterState = "existing";
+    dataDir = "/var/lib/etcd"; # TODO backup
+  };
 
-      PAPERLESS_FILENAME_FORMAT = "{correspondent}/{created} {title} {asn}";
-      PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = true;
+  services.dex =
+    let
+      issuer = "https://id.alanpearce.eu/";
+    in
+    {
+      enable = true;
+      environmentFile = config.age.secrets.dex.path;
+      settings = {
+        inherit issuer;
+        storage = {
+          type = "etcd";
+          config = {
+            endpoints = config.services.etcd.listenClientUrls;
+            namespace = "dex/";
+          };
+        };
+        web.http = "127.0.0.1:5556";
+        connectors = [{
+          type = "github";
+          id = "github";
+          name = "GitHub";
+          config = {
+            clientID = "$GITHUB_CLIENT_ID";
+            clientSecret = "$GITHUB_CLIENT_SECRET";
+            redirectURI = "${issuer}callback";
+            orgs = [{
+              name = "alan-pearce";
+            }];
+            teamNameField = "slug";
+            useLoginAsID = true;
+          };
+        }];
+        staticClients = [
+          {
+            name = "Tailscale";
+            id = "oCaiv7aije1thaep0eib";
+            secretEnv = "TAILSCALE_CLIENT_SECRET";
+            redirectURIs = [ "https://login.tailscale.com/a/oauth_response" ];
+          }
+        ];
+      };
     };
-  };
 
   services.syncthing = {
     enable = true;
@@ -838,46 +912,112 @@ in
   services.searchix = {
     enable = true;
     settings = {
-      web = {
-        baseURL = "https://searchix.alanpearce.eu";
-        sentryDSN = "https://26d4cd8d20157ae2f6b4726ceae1a563@o4507187730120704.ingest.de.sentry.io/4507187734970448";
-        contentSecurityPolicy = {
-          script-src = [
-            "'self'"
-            "https://gc.zgo.at"
-            "https://js-de.sentry-cdn.com"
-            "https://browser.sentry-cdn.com"
-          ];
-          img-src = [
-            "'self'"
-            "https://gc.zgo.at"
-          ];
-          connect-src = [
-            "'self'"
-            "https://searchix.goatcounter.com/count"
-            "*.sentry.io"
-          ];
-          worker-src = [
-            "blob:"
-          ];
+      web =
+        let
+          baseURL = "https://searchix.alanpearce.eu";
+        in
+        {
+          inherit baseURL;
+          sentryDSN = "https://26d4cd8d20157ae2f6b4726ceae1a563@o4507187730120704.ingest.de.sentry.io/4507187734970448";
+          contentSecurityPolicy =
+            let
+              self = "'self'";
+            in
+            {
+              script-src = [
+                (baseURL + "/static/")
+                "https://gc.zgo.at"
+                "https://js-de.sentry-cdn.com"
+                "https://browser.sentry-cdn.com"
+              ];
+              img-src = [
+                self
+                "https://gc.zgo.at"
+              ];
+              connect-src = [
+                self
+                "https://searchix.goatcounter.com/count"
+                "*.sentry.io"
+              ];
+              worker-src = [
+                "blob:"
+              ];
+            };
+          extraHeadHTML = ''
+            <script async
+              src="https://js-de.sentry-cdn.com/d735e99613a86e1625fb85d0e8e762de.min.js"
+              crossorigin="anonymous"></script>
+            <script data-goatcounter="https://searchix.goatcounter.com/count"
+                    async src="//gc.zgo.at/count.v4.js"
+                    crossorigin="anonymous"
+                    integrity="sha384-nRw6qfbWyJha9LhsOtSb2YJDyZdKvvCFh0fJYlkquSFjUxp9FVNugbfy8q1jdxI+"></script>
+          '';
         };
-        extraHeadHTML = ''
-          <script async
-            src="https://js-de.sentry-cdn.com/d735e99613a86e1625fb85d0e8e762de.min.js"
-            crossorigin="anonymous"></script>
-          <script data-goatcounter="https://searchix.goatcounter.com/count"
-                async src="//gc.zgo.at/count.js"></script>
-        '';
-      };
 
       importer.sources = {
         darwin = {
           enable = true;
           fetcher = "download";
-          url = "https://alanpearce.github.io/nix-darwin-options";
+          url = "https://alanpearce.github.io/nix-options/darwin";
+        };
+        home-manager = {
+          enable = true;
+          fetcher = "download";
+          url = "https://alanpearce.github.io/nix-options/home-manager";
+        };
+        nixpkgs = {
+          enable = true;
+          fetcher = "channel-nixpkgs";
+          channel = "nixos-unstable";
         };
-        home-manager.enable = true;
+        nixos = {
+          enable = true;
+          fetcher = "channel-nixpkgs";
+          channel = "nixos-unstable";
+        };
+      };
+    };
+  };
+
+  programs.git = {
+    enable = true;
+    package = pkgs.gitMinimal;
+    config = {
+      advice = {
+        detachedHead = false;
+        mergeConflict = false;
       };
     };
   };
+
+  systemd.services.laminar.environment = {
+    NIX_PATH = "nixpkgs=${<nixpkgs>}";
+  };
+  services.laminar = {
+    enable = true;
+    path = with pkgs; [
+      bash
+      stdenv
+      git
+      cached-nix-shell
+      nix
+      config.programs.ssh.package
+      flock
+      just
+    ];
+    settings = {
+      bindHTTP = "[::1]:8002";
+      keepRundirs = 1;
+    };
+  };
+  users.users.laminar = {
+    homeMode = "770";
+  };
+
+  virtualisation.containers = {
+    enable = true;
+    policy = {
+      default = [{ type = "insecureAcceptAnything"; }];
+    };
+  };
 }