# 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";
  ts-domain = "hydra-pinecone.ts.net";
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;
    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.
  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.enableAllTerminfo = true;
  environment.homeBinInPath = true;
  environment.localBinInPath = true;
  environment.systemPackages = with pkgs; [
    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
      '')
      (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 = {
    enable = true;
    settings = {
      PermitRootLogin = "no";
      PasswordAuthentication = false;
      KbdInteractiveAuthentication = false;
    };
  };
  services.sshguard = {
    enable = true;
    services = [ "sshd" ];
  };
  programs.mosh.enable = true;

  system.autoUpgrade = {
    enable = true;
    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"
      "searchix"
    ];
  };

  nix = {
    daemonCPUSchedPolicy = "batch";
    daemonIOSchedPriority = 6;
    settings = {
      max-jobs = 2;
      auto-optimise-store = true;
      trusted-users = [ "root" "nixremote" ];
      experimental-features = [ "nix-command" "flakes" ];
      substituters = [
        "https://nix-community.cachix.org"
      ];

      trusted-public-keys = [
        "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
      ];
    };
    gc = {
      automatic = true;
      dates = "08:15";
      options = "--delete-older-than 14d";
    };
    optimise = {
      automatic = true;
      dates = [ "02:30" ];
    };
  };

  services.nix-serve = {
    enable = true;
    package = pkgs.nix-serve-ng;
    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
        443 # HTTP/3 (QUIC)
        3478
        6885 # DHT
        6922
      ];
      trustedInterfaces = [ "tailscale0" ];
    };
    resolvconf = {
      enable = false;
      useLocalResolver = false;
    };
  };
  services.resolved = {
    enable = true;
    llmnr = "false";
    dnssec = "true";
  };
  systemd.network = {
    enable = true;
    networks.${netif} =
      {
        name = netif;
        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 = [{
          Address = "${net-ip4}/${net-mask4}";
          Peer = "${net-gw}/32";
        }];
      };
  };

  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
  '';

  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 = ''
    Defaults:root,%wheel env_keep+=EDITOR
  '';

  nixpkgs = {
    config.allowUnfree = true;
  };

  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 AAAAC3NzaC1lZDI1NTE5AAAAIBmDSZnUzIPQowLrKSa24eSb1WFQe7yPjTcDPPe3UY0Q nix@mba"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIE9of82WBHK8nr8L9RGeieLMfcAWaFCeCkmvYHM9LCuT nanopi"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIy9jFioBvV0JA0lc+De2N+vDOABGHgCECW6vkD33CE4 sourcehut"
      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIII7sWEwsm8JZiJ0LUnjSt0Kg1RXypG6p5AzP/R2n5ca actions@github.com"
    ];
  };

  # 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
      ))

      policy.add(policy.domains(policy.REFUSE, policy.todnames({
        'use-application-dns.net',
        'telemetry.astro.build',
      })))

      -- 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-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 =
      let
        subValue = v:
          if builtins.isList v
          then
            builtins.concatStringsSep " "
              (builtins.map
                (v:
                  (if lib.strings.hasPrefix "http" v
                  then v
                  else "'${v}'"))
                v)
          else toString v;

        headerValue = sep: val:
          if builtins.isAttrs val
          then
            builtins.concatStringsSep "; "
              (lib.attrsets.mapAttrsToList
                (k: v:
                  if builtins.isBool v then k else
                  "${k}${sep}${subValue v}"
                )
                val)
          else toString val;
        genHeader = header:
          let
            sep = if header == "content-security-policy" then " " else "=";
          in
          value: "${header} \"${headerValue sep value}\"";

        headers = matcher: headers: ''
          header ${matcher} {
            ${builtins.concatStringsSep "\n"
              (lib.attrsets.mapAttrsToList genHeader headers)}
          }
        '';
        security-headers = { matcher ? "", overrides ? { } }: headers matcher ({
          strict-transport-security = {
            max-age = 2 * 365 * 24 * 60 * 60;
          };
          x-content-type-options = "nosniff";
          x-frame-options = "DENY";
        } // overrides);
      in
      {
        "http://" = {
          # Needed for HTTP->HTTPS servers
        };
        "alanpearce.eu" = {
          serverAliases = [ "www.alanpearce.eu" "test.alanpearce.eu" ];
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            root * /srv/http/website/public
            file_server
            ${security-headers {}}
            handle_errors {
              rewrite * /404.html
              file_server
            }
          '';
        };
        "${hostname}.alanpearce.eu" = {
          serverAliases = [ "https://" ];
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            respond * 204
            ${security-headers {}}
          '';
        };
        "pdns.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            log {
              output discard
            }
            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
            }
            encode zstd gzip
            reverse_proxy localhost:443 {
              transport http {
                tls_server_name dns.alanpearce.eu
              }
            }
          '';
        };
        "files.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            ${security-headers {}}
            root * /srv/http/files
            file_server browse
          '';
        };
        "git.alanpearce.eu" =
          let
            fcgi = config.services.fcgiwrap;
            fcgisocket = "${fcgi.socketType}/${fcgi.socketAddress}";
          in
          {
            useACMEHost = "alanpearce.eu";
            extraConfig = ''
              root * ${pkgs.cgit-pink}/cgit/
              encode zstd gzip
              ${security-headers {
                overrides.content-security-policy = {
                  default-src = [ "none" ];
                  base-uri = [ "none" ];
                  style-src = [ "self" "unsafe-inline" ];
                  script-src = [ "self" "unsafe-inline" ];
                  form-action = [ "self" ];
                  connect-src = [ "self" ];
                  img-src = [ "https" ];
                  object-src = [ "none" ];
                };
              }}
              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 "^.*/(HEAD|info/refs|objects/info/[^/]+|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
                  }
                }
              }
              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/
                    ''}
                    }
                  }
              }
            '';
          };
        "ntfy.alanpearce.eu" = {
          useACMEHost = "alanpearce.eu";
          extraConfig = ''
            encode zstd gzip
            ${security-headers {}}
            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
          {
            useACMEHost = "alanpearce.eu";
            extraConfig = ''
              encode zstd gzip
              handle_path /static/* {
                root * /srv/http/legit/src/static
                file_server
              }
              ${security-headers {
                overrides.content-security-policy = {
                  default-src = [ "none" ];
                  base-uri = [ "none" ];
                  style-src = [ "self" ];
                  script-src = [ "none" ];
                  form-action = [ "self" ];
                  connect-src = [ "self" ];
                  img-src = [ "https" ];
                  object-src = [ "none" ];
                };
              }}
              reverse_proxy ${server.host}:${toString server.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;
    user = "gitolite";
    group = "gitolite";
    preforkProcesses = 2;
    socketType = "tcp6";
    socketAddress = "[::1]:9000";
  };
  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 = "gitolite";
    group = "gitolite";
    basePath = "${config.services.gitolite.dataDir}/repositories/";
  };

  networking.nat = {
    enable = true;
    internalInterfaces = [ "ve-+" ];
    externalInterface = netif;
    enableIPv6 = true;
  };

  users.users.paperless = {
    group = "paperless";
    uid = config.ids.uids.paperless;
    home = "/srv/paperless";
  };
  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;
        };
      };
      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";
      };
    };

  services.etcd = {
    enable = true;
    initialClusterState = "existing";
    dataDir = "/var/lib/etcd"; # TODO backup
  };

  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;
    dataDir = "/srv/syncthing";
    configDir = "/var/lib/syncthing";
    openDefaultPorts = true;
    overrideDevices = false;
    overrideFolders = false;
  };

  services.searchix = {
    enable = true;
    settings = {
      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>
          '';
        };

      importer.sources = {
        darwin = {
          enable = true;
          fetcher = "download";
          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";
        };
        nixos = {
          enable = true;
          fetcher = "channel-nixpkgs";
          channel = "nixos-unstable";
        };
      };
    };
  };
}