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.nix787
1 files changed, 787 insertions, 0 deletions
diff --git a/system/linde.nix b/system/linde.nix
new file mode 100644
index 00000000..11818395
--- /dev/null
+++ b/system/linde.nix
@@ -0,0 +1,787 @@
+# Edit this configuration file to define what should be installed on
+# your system.  Help is available in the configuration.nix(5) man page
+# and in the NixOS manual (accessible by running `nixos-help`).
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  netif = "enp1s0";
+  hostname = "linde";
+  net-ip4 = "116.203.248.56";
+  net-mask4 = "32";
+  net-gw = "172.31.1.1";
+  net-ip6 = "2a01:4f8:c012:23a4::1";
+  net-rdnsip = "2a01:4f8:c012:23a4::53";
+  net-mask6 = "64";
+  net-gw6 = "fe80::1";
+in
+{
+  imports =
+    [
+      # Include the results of the hardware scan.
+      ./linde-hardware.nix
+    ];
+  age.secrets = {
+    paperless =
+      let
+        cfg = config.services.paperless;
+      in
+      {
+        file = ../secrets/paperless.age;
+        path = "${cfg.dataDir}/nixos-paperless-secret-key";
+        owner = cfg.user;
+        mode = "400";
+        symlink = false;
+      };
+    acme.file = ../secrets/acme.age;
+    binarycache.file = ../secrets/binarycache.age;
+    powerdns.file = ../secrets/powerdns.age;
+  };
+
+  # Use the systemd-boot EFI boot loader.
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+  boot.loader.efi.efiSysMountPoint = "/boot/efi";
+
+  time.timeZone = "Europe/Berlin";
+
+  i18n.defaultLocale = "en_GB.UTF-8";
+
+  environment.homeBinInPath = true;
+  environment.localBinInPath = true;
+  environment.systemPackages = with pkgs; [
+    kitty.terminfo
+    htop
+    lsof
+    gitMinimal
+    powerdns
+    sqlite-interactive
+    knot-dns
+
+    nixpkgs-review
+    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
+      '')
+    ];
+  };
+
+  # Initial empty root password for easy login:
+  users.users.root.initialHashedPassword = "";
+  services.openssh = {
+    enable = true;
+    settings = {
+      PermitRootLogin = "no";
+      PasswordAuthentication = false;
+      KbdInteractiveAuthentication = false;
+    };
+  };
+  services.sshguard = {
+    enable = true;
+    services = [ "sshd" ];
+  };
+  programs.mosh.enable = true;
+
+  system.autoUpgrade = {
+    enable = true;
+    dates = "05:10";
+    allowReboot = true;
+    flake = "git+file://${config.services.gitolite.dataDir}/repositories/nixfiles.git";
+    flags = [
+      "--no-write-lock-file"
+      "--update-input"
+      "nixpkgs"
+    ];
+  };
+
+  nix = {
+    daemonCPUSchedPolicy = "batch";
+    daemonIOSchedPriority = 6;
+    settings = {
+      max-jobs = 2;
+      auto-optimise-store = true;
+      trusted-users = [ "root" "nixremote" ];
+      experimental-features = [ "nix-command" "flakes" ];
+    };
+    gc = {
+      automatic = true;
+      dates = "08:15";
+      options = "--delete-older-than 14d";
+    };
+    optimise = {
+      automatic = true;
+      dates = [ "02:30" ];
+    };
+  };
+
+  services.nix-serve = {
+    enable = true;
+    secretKeyFile = config.age.secrets.binarycache.path;
+  };
+
+  programs.neovim = {
+    enable = true;
+    defaultEditor = true;
+    viAlias = true;
+    vimAlias = true;
+  };
+
+  networking = {
+    hostName = hostname;
+    useDHCP = false;
+    dhcpcd.enable = false;
+    nameservers = [
+      "2606:4700:4700::1111"
+      "2606:4700:4700::1001"
+      "1.1.1.1"
+      "1.0.0.1"
+    ];
+    hosts = lib.mkForce {
+      ${net-ip4} = [ "${hostname}.alanpearce.eu" hostname ];
+      ${net-ip6} = [ "${hostname}.alanpearce.eu" hostname ];
+      ${net-rdnsip} = [ "dns" ];
+    };
+    firewall = {
+      enable = true;
+      allowPing = true;
+      pingLimit = "--limit 60/minute --limit-burst 30";
+      logRefusedConnections = false;
+      allowedTCPPorts = [
+        22
+        80
+        443
+        53
+        853
+        9418
+        6922
+      ];
+      allowedUDPPorts = [
+        53
+        3478
+        6885 # DHT
+        6922
+      ];
+    };
+    resolvconf = {
+      enable = true;
+      useLocalResolver = false;
+    };
+  };
+  services.resolved.enable = false;
+  systemd.network = {
+    enable = true;
+    networks.${netif} =
+      {
+        name = netif;
+        gateway = [ net-gw ];
+        routes = [{
+          routeConfig = {
+            Gateway = net-gw6;
+            PreferredSource = net-ip6;
+          };
+        }];
+        address = [
+          "${net-ip6}/${net-mask6}"
+          "${net-rdnsip}/${net-mask6}"
+        ];
+        addresses = [{
+          addressConfig = {
+            Address = "${net-ip4}/${net-mask4}";
+            Peer = "${net-gw}/32";
+          };
+        }];
+      };
+  };
+
+  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";
+  };
+
+  security.sudo.execWheelOnly = true;
+  security.sudo.extraConfig = ''
+    Defaults:root,%wheel env_keep+=EDITOR
+  '';
+
+  nixpkgs = {
+    config.allowUnfree = true;
+    overlays = [
+      (self: super: {
+        cgit-pink = super.cgit-pink.overrideAttrs (old: {
+          patches = [ ../patches/cgit-pink.patch ];
+        });
+      })
+    ];
+  };
+
+  programs.fish = {
+    enable = true;
+    interactiveShellInit = ''
+      set --universal fish_greeting ""
+    '';
+  };
+  programs.zsh.enable = true;
+  users.users.root.shell = "${pkgs.fish}/bin/fish";
+  users.users.alan = {
+    shell = "${pkgs.fish}/bin/fish";
+    extraGroups = [ "wheel" "caddy" "docker" ];
+    isNormalUser = true;
+    home = "/home/alan";
+    createHome = true;
+
+    openssh.authorizedKeys.keys = [
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8VIII+598QOBxi/52O1Kb19RdUdX0aZmS1/dNoyqc5 alan@hetzner.strongbox"
+    ];
+  };
+
+  users.users.nixremote = {
+    shell = "/bin/sh";
+    isNormalUser = true;
+    home = "/var/lib/nixremote/";
+    createHome = true;
+    openssh.authorizedKeys.keys = [
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBxa7lxDu0M4chats/VvpFzjT3ruexKa3J9UC6ASo3bN root@NanoPi.lan"
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE9of82WBHK8nr8L9RGeieLMfcAWaFCeCkmvYHM9LCuT nanopi"
+    ];
+  };
+
+  # This value determines the NixOS release from which the default
+  # settings for stateful data, like file locations and database versions
+  # on your system were taken. It's perfectly fine and recommended to leave
+  # this value at the release version of the first install of this system.
+  # Before changing this value read the documentation for this option
+  # (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
+    '';
+  };
+
+  systemd.services.hagezi-blocklist-update = {
+    enable = true;
+    startAt = "daily";
+    serviceConfig = {
+      CacheDirectory = "blocklist";
+      UMask = "0077";
+      DynamicUser = "yes";
+      ProtectSystem = "strict";
+      ProtectHome = true;
+      PrivateTmp = true;
+      PrivateDevices = true;
+      PrivateUsers = true;
+      ProtectClock = true;
+      ProtectKernelTunables = true;
+      ProtectKernelModules = true;
+      ProtectKernelLogs = true;
+      ProtectControlGroups = true;
+      ProtectProc = "invisible";
+      RestrictAddressFamilies = "AF_INET AF_INET6";
+      RestrictNamespaces = true;
+      RestrictRealtime = true;
+      LockPersonality = true;
+      MemoryDenyWriteExecute = "true";
+      SystemCallFilter = [
+        "~@clock"
+        "~@cpu-emulation"
+        "~@debug"
+        "~@module"
+        "~@mount"
+        "~@obsolete"
+        "~@privileged"
+        "~@raw-io"
+        "~@reboot"
+        "~@resources"
+        "~@swap"
+      ];
+      SystemCallArchitectures = "native";
+      CapabilityBoundingSet = "";
+      DevicePolicy = "closed";
+      ProcSubset = "pid";
+      NoNewPrivileges = true;
+      ExecStart = "${pkgs.curl}/bin/curl --no-progress-meter --output %C/blocklist/hagezi.rpz https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/pro.plus.txt";
+      #  https://raw.githubusercontent.com/hagezi/dns-blocklists/main/rpz/pro.plus.txt"
+      ExecStartPost = [
+        "+/bin/sh -c 'exec install --compare --mode=644 %C/blocklist/hagezi.rpz /etc/knot-resolver/blocklist.rpz'"
+        "-/bin/sh -c 'exec rm -f %C/blocklist/hagezi.rpz'"
+      ];
+      Environment = [
+        "HOME=%C/blocklist"
+      ];
+    };
+  };
+
+  services.kresd = {
+    enable = true;
+    # package = pkgs.knot-resolver.override { extraFeatures = true; };
+    listenPlain = [
+      "[${net-rdnsip}]:53"
+    ];
+    listenTLS = [
+      "127.0.0.1:853"
+      "[::1]:853"
+      "${net-ip4}:853"
+      "[${net-ip6}]:853"
+    ];
+    listenDoH = [
+      "[::1]:443"
+      "127.0.0.1:443"
+    ];
+    instances = 2;
+    extraConfig = ''
+      modules = {
+        'rebinding < iterate',
+        'hints > iterate',
+        'serve_stale < cache',
+        'stats',
+        predict = {
+          window = 30,
+          period = 24 * (60/30),
+        },
+        'nsid',
+      }
+
+      local systemd_instance = os.getenv("SYSTEMD_INSTANCE")
+      nsid.name(systemd_instance)
+
+      log_groups({ 'policy' })
+
+      cache.size = 500 * MB
+
+      net.tls(
+        '/var/lib/acme/dns.alanpearce.eu/cert.pem',
+        '/var/lib/acme/dns.alanpearce.eu/key.pem'
+      )
+
+      -- override blocklist
+      policy.add(policy.suffix(policy.PASS, policy.todnames({
+      })))
+
+      policy.add(policy.rpz(
+        policy.DENY_MSG('domain blocked by hagezi'),
+        '/etc/knot-resolver/blocklist.rpz',
+        false -- needs wrapped kresd
+        -- true -- will watch the file for updates
+      ))
+
+      -- disable DNSSEC when using Quad9 since they do it
+      -- trust_anchors.remove('.')
+      -- policy.add(policy.all(policy.TLS_FORWARD({
+      --   {'2620:fe::fe', hostname='dns.quad9.net'},
+      --   {'2620:fe::9', hostname='dns.quad9.net'},
+      --   {'9.9.9.9', hostname='dns.quad9.net'},
+      --   {'149.112.122.122', hostname='dns.quad9.net'},
+      -- })))
+    '';
+  };
+
+  users.groups.ntfy = { };
+  users.users.ntfy = {
+    isSystemUser = true;
+    group = "ntfy";
+  };
+  services.ntfy-sh = {
+    enable = true;
+    user = "ntfy";
+    group = "ntfy";
+    settings = {
+      base-url = "https://ntfy.alanpearce.eu";
+      listen-http = ":2586";
+      behind-proxy = true;
+      manager-interval = "1h";
+      cache-startup-queries = ''
+        PRAGMA journal_mode = WAL;
+        PRAGMA busy_timeout = 5000;
+        PRAGMA synchronous = NORMAL;
+      '';
+      cache-file = "/var/cache/ntfy/cache.db";
+      attachment-cache-dir = "/var/cache/ntfy/attachments";
+      auth-default-access = "deny-all";
+      auth-file = "/var/lib/ntfy/user.db";
+      upstream-base-url = "https://ntfy.sh";
+    };
+  };
+  systemd.services.ntfy-sh = {
+    serviceConfig = {
+      DynamicUser = lib.mkForce false;
+      StateDirectory = lib.mkForce "ntfy";
+      RuntimeDirectory = "ntfy";
+      CacheDirectory = "ntfy";
+    };
+  };
+
+  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; [
+      rdiff-backup
+      openssh
+    ];
+    script = ''
+      rdiff-backup --api-version 201 backup ${config.services.gitolite.dataDir} ${hostname}@home.alanpearce.eu::gitolite
+      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::gitolite
+    '';
+    serviceConfig.Type = "oneshot";
+  };
+
+  systemd.services.backup-paperless = {
+    startAt = "daily";
+    path = with pkgs; [
+      sudo
+      rdiff-backup
+      openssh
+    ];
+    script = ''
+      sudo -u paperless ./paperless-manage document_exporter --delete --use-filename-format --no-archive --no-thumbnail --no-progress-bar ./export
+      rdiff-backup --api-version 201 backup ./export ${hostname}@home.alanpearce.eu::paperless
+      rdiff-backup --api-version 201 remove increments --older-than 3M ${hostname}@home.alanpearce.eu::paperless
+    '';
+    serviceConfig = {
+      Type = "oneshot";
+      WorkingDirectory = config.services.paperless.dataDir;
+    };
+  };
+
+  security.acme = {
+    defaults = {
+      email = "alan@alanpearce.eu";
+      dnsProvider = "pdns";
+      dnsResolver = "1.1.1.1:53";
+      credentialsFile = config.age.secrets.acme.path;
+      reloadServices = [ "caddy" ];
+      validMinDays = 32;
+    };
+    acceptTerms = true;
+    certs."alanpearce.eu" = {
+      extraDomainNames = [ "*.alanpearce.eu" ];
+    };
+    certs."dns.alanpearce.eu" = {
+      reloadServices = map (x: "kresd@${toString x}") (range 1 config.services.kresd.instances);
+      group = "knot-resolver";
+    };
+  };
+  users.groups.acme.members = [
+    "caddy"
+  ];
+
+  services.caddy = {
+    enable = true;
+    group = "caddy";
+    globalConfig = ''
+      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 = ''
+          root * /srv/http/files
+          encode gzip zstd
+          file_server browse
+        '';
+      };
+      "git.alanpearce.eu" = {
+        useACMEHost = "alanpearce.eu";
+        extraConfig = ''
+          root * ${pkgs.cgit-pink}/cgit/
+          encode gzip zstd
+          handle_path /custom/* {
+            file_server {
+              root /srv/http/cgit/
+            }
+          }
+          rewrite /robots.txt /assets/robots.txt
+          handle_path /assets/* {
+            file_server  {
+              hide cgit.cgi
+            }
+          }
+          @git_http_backend path_regexp "^/.+/(info/refs|git-upload-pack)$"
+          handle @git_http_backend {
+            reverse_proxy unix/run/fcgiwrap.sock {
+              transport fastcgi {
+                env SCRIPT_FILENAME ${pkgs.git}/libexec/git-core/git-http-backend
+                env GIT_PROJECT_ROOT ${config.services.gitolite.dataDir}/repositories
+              }
+            }
+          }
+          handle {
+            reverse_proxy unix/run/fcgiwrap.sock {
+              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/custom.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
+                  side-by-side-diffs=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/
+                ''}
+                }
+              }
+          }
+        '';
+      };
+      "ntfy.alanpearce.eu" = {
+        useACMEHost = "alanpearce.eu";
+        extraConfig = ''
+          reverse_proxy localhost${config.services.ntfy-sh.settings.listen-http}
+        '';
+      };
+      "legit.alanpearce.eu" =
+        let
+          server = config.services.legit.settings.server;
+        in
+        {
+          useACMEHost = "alanpearce.eu";
+          extraConfig = ''
+            handle_path /static/* {
+              root * /srv/http/legit/src/static
+              file_server
+            }
+            reverse_proxy ${server.host}:${toString server.port}
+          '';
+        };
+      "papers.alanpearce.eu" = {
+        extraConfig = ''
+          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
+        {
+          extraConfig = ''
+            reverse_proxy ${ns.bindAddress}:${toString ns.port}
+          '';
+        };
+    };
+  };
+  systemd.services.caddy.serviceConfig = {
+    UMask = "007";
+  };
+
+  services.fcgiwrap = {
+    enable = true;
+    group = "gitolite";
+  };
+  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' );
+    '';
+  };
+  services.legit = {
+    enable = true;
+    group = "gitolite";
+    settings = {
+      server.name = "legit.alanpearce.eu";
+      dirs = {
+        templates = "/srv/http/legit/src/templates";
+      };
+      repo = {
+        scanPath = "/srv/http/legit/repos";
+        readme = [
+          "readme"
+          "readme.md"
+          "README.md"
+        ];
+      };
+    };
+  };
+
+  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 = {
+    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/*" ];
+
+      PAPERLESS_FILENAME_FORMAT = "{correspondent}/{created} {title} {asn}";
+      PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = true;
+    };
+  };
+
+  services.syncthing = {
+    enable = true;
+    dataDir = "/srv/syncthing";
+    configDir = "/var/lib/syncthing";
+    overrideDevices = false;
+    overrideFolders = false;
+  };
+}