summary refs log tree commit diff stats
path: root/system
diff options
context:
space:
mode:
authorAlan Pearce2024-04-11 00:04:06 +0200
committerAlan Pearce2024-04-11 00:04:06 +0200
commit6c18a33c758f0226e660f924ddd71a6d3ad53004 (patch)
tree946b87a7e268604a47217e5e3250e144b4eaf3f0 /system
parent2b09b74ba617346a0c9c932543e658837ef9e5d2 (diff)
downloadnixfiles-6c18a33c758f0226e660f924ddd71a6d3ad53004.tar.lz
nixfiles-6c18a33c758f0226e660f924ddd71a6d3ad53004.tar.zst
nixfiles-6c18a33c758f0226e660f924ddd71a6d3ad53004.zip
Import server configurations
Diffstat (limited to 'system')
-rw-r--r--system/linde-hardware.nix38
-rw-r--r--system/linde.nix787
-rw-r--r--system/nanopi-hardware.nix35
-rwxr-xr-xsystem/nanopi.nix859
4 files changed, 1719 insertions, 0 deletions
diff --git a/system/linde-hardware.nix b/system/linde-hardware.nix
new file mode 100644
index 00000000..ba48156f
--- /dev/null
+++ b/system/linde-hardware.nix
@@ -0,0 +1,38 @@
+# Do not modify this file!  It was generated by ‘nixos-generate-config’
+# and may be overwritten by future invocations.  Please make changes
+# to /etc/nixos/configuration.nix instead.
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports =
+    [ (modulesPath + "/profiles/qemu-guest.nix")
+    ];
+
+  boot.initrd.availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" "sr_mod" ];
+  boot.initrd.kernelModules = [ ];
+  boot.kernelModules = [ ];
+  boot.extraModulePackages = [ ];
+
+  fileSystems."/" =
+    { device = "/dev/disk/by-uuid/09c12218-c189-439a-9ef1-846b87538841";
+      fsType = "ext4";
+    };
+
+  fileSystems."/boot/efi" =
+    { device = "/dev/disk/by-uuid/1C43-4EC4";
+      fsType = "vfat";
+    };
+
+  swapDevices =
+    [ { device = "/dev/disk/by-uuid/29793e8e-5c0d-4e5b-80e0-11252d786294"; }
+    ];
+
+  # Enables DHCP on each ethernet and wireless interface. In case of scripted networking
+  # (the default) this is the recommended approach. When using systemd-networkd it's
+  # still possible to use this option, but it's recommended to use it in conjunction
+  # with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
+  networking.useDHCP = lib.mkDefault true;
+  # networking.interfaces.enp1s0.useDHCP = lib.mkDefault true;
+
+  nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
+}
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;
+  };
+}
diff --git a/system/nanopi-hardware.nix b/system/nanopi-hardware.nix
new file mode 100644
index 00000000..7f7d03ca
--- /dev/null
+++ b/system/nanopi-hardware.nix
@@ -0,0 +1,35 @@
+# Do not modify this file!  It was generated by ‘nixos-generate-config’
+# and may be overwritten by future invocations.  Please make changes
+# to /etc/nixos/configuration.nix instead.
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+  imports =
+    [ (modulesPath + "/installer/scan/not-detected.nix")
+    ];
+
+  boot.initrd.availableKernelModules = [ "nvme" ];
+  boot.initrd.kernelModules = [ ];
+  boot.kernelModules = [ ];
+  boot.extraModulePackages = [ ];
+
+  fileSystems."/" =
+    { device = "/dev/disk/by-uuid/6adfb32b-ebe9-4116-86e8-829d2c9dc79d";
+      fsType = "ext4";
+    };
+
+  fileSystems."/mnt/sd" =
+    { device = "/dev/disk/by-uuid/79d9c190-1728-42ae-8cfd-b03d4a10bdb3";
+      fsType = "ext4";
+    };
+
+  fileSystems."/boot" =
+    { device = "/mnt/sd/boot";
+      fsType = "none";
+      options = [ "bind" ];
+    };
+
+  swapDevices = [ ];
+
+  nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux";
+}
diff --git a/system/nanopi.nix b/system/nanopi.nix
new file mode 100755
index 00000000..3b10e606
--- /dev/null
+++ b/system/nanopi.nix
@@ -0,0 +1,859 @@
+{ config
+, pkgs
+, lib
+, inputs
+, ...
+}:
+let
+  fsTypes = [ "f2fs" "ext" "exfat" "vfat" ];
+in
+{
+  imports = [
+    ./nanopi-hardware.nix
+    (inputs.nixos-hardware + "/friendlyarm/nanopi-r5s")
+  ];
+
+  age.secrets = {
+    dyndns.file = ../secrets/dyndns.age;
+    acme.file = ../secrets/acme.age;
+    syncthing.file = ../secrets/syncthing.age;
+  };
+
+  boot = {
+    supportedFilesystems = fsTypes;
+    initrd.supportedFilesystems = fsTypes;
+
+    loader.timeout = 1;
+    kernelPatches = lib.mkForce [ ];
+  };
+
+  systemd.services."irqbalance-oneshot" = {
+    enable = true;
+    description = "Distribute interrupts after boot using \"irqbalance --oneshot\"";
+    documentation = [ "man:irqbalance" ];
+    wantedBy = [ "sysinit.target" ];
+    serviceConfig = {
+      Type = "oneshot";
+      RemainAfterExit = true;
+      ExecStart = "${pkgs.irqbalance.out}/bin/irqbalance --foreground --oneshot";
+    };
+  };
+  systemd.tmpfiles.settings."leds-off" = {
+    "/sys/class/leds/green:*/brightness" = {
+      w = {
+        argument = "0";
+      };
+    };
+  };
+
+  services.udev.extraRules = ''
+    # set scheduler for NVMe
+    ACTION=="add|change", KERNEL=="nvme[0-9]n[0-9]", ATTR{queue/scheduler}="kyber"
+    # set scheduler for SSD and eMMC
+    ACTION=="add|change", KERNEL=="sd[a-z]|mmcblk[0-9]*", ATTR{queue/rotational}=="0", ATTR{queue/scheduler}="mq-deadline"
+    # set scheduler for rotating disks
+    ACTION=="add|change", KERNEL=="sd[a-z]", ATTR{queue/rotational}=="1", ATTR{queue/scheduler}="kyber"
+  '';
+
+  systemd.services.dynamic-dns-update = {
+    enable = true;
+    startAt = [ "hourly" ];
+    description = "Update IP addresses";
+    path = with pkgs; [ curl iproute2 dig.dnsutils miller ];
+    after = [ "sys-devices-platform-fe2a0000.ethernet-net-wan0.device" ];
+    bindsTo = [ "sys-devices-platform-fe2a0000.ethernet-net-wan0.device" ];
+    serviceConfig = {
+      Type = "oneshot";
+      ExecStart = "/bin/sh /etc/nixos/update-ip ${config.age.secrets.dyndns.path}";
+    };
+  };
+
+  services.journald.extraConfig = ''
+    MaxRetentionSec=1 month
+  '';
+
+  environment.systemPackages = with pkgs; [
+    kitty.terminfo
+    htop
+    lsof
+    usbutils
+    lzop
+    zstd
+    sqlite
+  ];
+
+  systemd.network.config.networkConfig = {
+    SpeedMeter = true;
+  };
+  networking = {
+    hostName = "nanopi";
+    domain = "lan";
+    useDHCP = false;
+    useNetworkd = true;
+    nameservers = [
+      "176.9.93.198"
+      "176.9.1.117"
+      "2a01:4f8:151:34aa::198"
+      "2a01:4f8:141:316d::117"
+    ];
+    firewall = {
+      enable = true;
+      rejectPackets = true;
+      logRefusedConnections = false;
+      pingLimit = "5/second";
+      filterForward = true; # we are a router
+      allowedUDPPorts = [
+        53
+        123
+      ];
+      allowedTCPPorts = [
+        53
+        123
+        80
+        443
+      ];
+      interfaces.bridge0 = {
+        allowedTCPPorts = [
+          53
+          67
+          139
+          445
+          1883
+          3689
+          5357
+          5533 # SmartDNS
+          8096
+          9091 # Transmission
+          8096 # Jellyfin
+        ];
+        allowedUDPPorts = [
+          53
+          67
+          69
+          137
+          4011 # PXE
+          5533 # SmartDNS
+          5353
+          5355 # LLMNR
+          1900 # DLNA Jellyfin
+          3702 # Samba WSDD
+          21027 # Syncthing LNDP
+          41641
+          51827
+        ];
+      };
+      interfaces.wan0 = {
+        allowedTCPPorts = [
+          6980 # aria2c
+        ];
+        allowedUDPPorts = [
+          6976
+          6980
+          41641
+        ];
+      };
+      extraForwardRules = ''
+        iifname { "wan0", "wlan0", "wwan0" } oifname { "lan1", "lan2", "bridge0" } icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-request, mld-listener-query, nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } accept
+        iifname { "lan1", "lan2", "bridge0" } oifname { "wan0", "wlan0", "wwan0" } accept
+      '';
+    };
+    nftables = {
+      enable = true;
+      tables = {
+        firewall = {
+          family = "inet";
+          content = ''
+            chain postrouting {
+              type nat hook postrouting priority srcnat; policy accept;
+              oifname { "wan0", "wlan0", "wwan0" } masquerade
+            }
+            chain prerouting {
+              type nat hook prerouting priority dstnat;
+              iifname "wan0" tcp dport { 6922, 51413 } dnat ip to 10.0.0.42
+            }
+          '';
+        };
+      };
+    };
+    wireless = {
+      enable = true;
+      # iwd = {
+      #   enable = true;
+      #   settings = {
+      #     Network = {
+      #       RoutePriorityOffset = 300;
+      #     };
+      #   };
+      # };
+    };
+  };
+  services.resolved.enable = false;
+
+  programs.command-not-found.enable = false;
+
+  services.openssh = {
+    enable = true;
+    openFirewall = true;
+    startWhenNeeded = false;
+  };
+  programs.mosh.enable = true;
+  services.sshguard = {
+    enable = true;
+    services = [ "sshd" ];
+  };
+
+  systemd.network = {
+    enable = true;
+    wait-online = {
+      ignoredInterfaces = [ "wan0" "wlan0" "wwan0" ];
+    };
+    links = {
+      "10-name-lan1" = {
+        matchConfig.Path = "platform-3c0000000.pcie-pci-0000:01:00.0";
+        linkConfig = {
+          Name = "lan1";
+          MACAddress = "a8:95:85:0d:67:38";
+        };
+      };
+      "10-name-lan2" = {
+        matchConfig.Path = "platform-3c0400000.pcie-pci-0001:01:00.0";
+        linkConfig = {
+          Name = "lan2";
+          MACAddress = "a8:95:85:0d:67:39";
+        };
+      };
+      "10-name-wan0" = {
+        matchConfig.Path = "platform-fe2a0000.ethernet";
+        linkConfig = {
+          Name = "wan0";
+          MACAddress = "a8:95:85:0d:67:3a";
+        };
+      };
+      "10-name-wlan0" = {
+        matchConfig.MACAddress = "9c:53:22:33:bf:e9";
+        linkConfig = {
+          Name = "wlan0";
+        };
+      };
+      "10-name-wwan0" = {
+        matchConfig.MACAddress = "34:4b:50:00:00:00";
+        linkConfig = {
+          Name = "wwan0";
+        };
+      };
+    };
+    netdevs = {
+      "20-bridge" = {
+        netdevConfig = {
+          Kind = "bridge";
+          Name = "bridge0";
+        };
+      };
+    };
+    networks = {
+      "30-lan-ports" = {
+        matchConfig.Name = "lan*";
+        bridge = [ "bridge0" ];
+        linkConfig = {
+          MACAddress = "82:E0:06:9C:8E:7C";
+          RequiredForOnline = "no";
+        };
+        networkConfig.LinkLocalAddressing = "no";
+      };
+      "40-bridge0" = {
+        matchConfig.Name = "bridge0";
+        linkConfig.RequiredForOnline = "routable";
+        address = [
+          "10.0.0.1/20"
+          "fd12:d04f:65d:42::1/56"
+        ];
+        networkConfig = {
+          IPv6AcceptRA = false;
+          IPv6SendRA = true;
+          DHCPPrefixDelegation = true;
+          ConfigureWithoutCarrier = true;
+        };
+        dhcpPrefixDelegationConfig = {
+          UplinkInterface = "wan0";
+          SubnetId = "42";
+          Assign = true;
+          Token = "::1";
+        };
+        ipv6SendRAConfig = {
+          RouterLifetimeSec = 1800;
+          EmitDNS = true;
+          DNS = "fd12:d04f:65d:42::1";
+          EmitDomains = true;
+          Domains = [ config.networking.domain ];
+        };
+      };
+      "50-wwan0" = {
+        matchConfig.Name = "wwan0";
+        linkConfig.RequiredForOnline = false;
+        networkConfig = {
+          DHCP = "yes";
+          IPv6AcceptRA = true;
+          IPForward = "yes";
+        };
+        dhcpV4Config = {
+          UseDNS = false;
+          SendHostname = false;
+          RouteMetric = 2048;
+        };
+        ipv6AcceptRAConfig.UseDNS = false;
+        routes = [
+          {
+            routeConfig = {
+              Gateway = "_dhcp4";
+              QuickAck = true;
+              InitialCongestionWindow = 30;
+              InitialAdvertisedReceiveWindow = 30;
+            };
+          }
+        ];
+        cakeConfig = {
+          Bandwidth = "1M";
+          OverheadBytes = 18;
+          MPUBytes = 64;
+          CompensationMode = "none";
+          NAT = true;
+          PriorityQueueingPreset = "diffserv8";
+        };
+      };
+      "50-wan" = {
+        matchConfig.Name = "wan0";
+        linkConfig.RequiredForOnline = "no";
+        networkConfig = {
+          DHCP = "yes";
+          IPv6AcceptRA = true;
+          IPForward = "yes";
+        };
+        dhcpV4Config = {
+          UseDNS = false;
+          SendHostname = false;
+          SendRelease = false;
+          UseHostname = false;
+          # Label = "wan0:1";
+        };
+        dhcpV6Config = {
+          UseDNS = false;
+          RapidCommit = true;
+          PrefixDelegationHint = "::/56";
+        };
+        dhcpPrefixDelegationConfig = {
+          UplinkInterface = ":self";
+        };
+        ipv6AcceptRAConfig = {
+          UseDNS = false;
+        };
+        addresses = [
+          {
+            addressConfig = {
+              Address = "192.168.100.10/24";
+              # Peer = "192.168.100.1/32";
+              Label = "wan0:0";
+              # Scope = "link";
+            };
+          }
+        ];
+        cakeConfig = {
+          Bandwidth = "24M";
+          OverheadBytes = 18;
+          MPUBytes = 64;
+          CompensationMode = "none";
+          NAT = true;
+          PriorityQueueingPreset = "diffserv8";
+        };
+      };
+      "60-wlan" = {
+        matchConfig.MACAddress = "9c:53:22:33:bf:e9";
+        linkConfig.RequiredForOnline = "no";
+        networkConfig = {
+          DHCP = "yes";
+          IPForward = "yes";
+          IgnoreCarrierLoss = "3s";
+        };
+        dhcpV4Config = {
+          UseDNS = false;
+          SendHostname = false;
+          SendRelease = true;
+          UseHostname = false;
+          RouteMetric = 2048;
+        };
+        routes = [
+          {
+            routeConfig = {
+              Metric = 2048;
+              Gateway = "_dhcp4";
+              QuickAck = true;
+              InitialCongestionWindow = 30;
+              InitialAdvertisedReceiveWindow = 30;
+            };
+          }
+        ];
+        cakeConfig = {
+          Bandwidth = "1M";
+          OverheadBytes = 18;
+          MPUBytes = 64;
+          CompensationMode = "none";
+          NAT = true;
+          PriorityQueueingPreset = "diffserv8";
+        };
+      };
+    };
+  };
+  boot.kernelModules = [
+    "tcp_lp"
+  ];
+  boot.kernel.sysctl = {
+    "net.ipv4.conf.bridge0.send_redirects" = 1;
+    "net.ipv4.conf.bridge0.accept_source_route" = 1;
+    "net.ipv4.tcp_slow_start_after_idle" = 0;
+    "net.ipv4.tcp_ecn" = 1;
+    "net.ipv4.tcp_fastopen" = "0x3";
+    "net.ipv4.tcp_allowed_congestion_control" = "reno cubic lp";
+    "net.core.default_qdisc" = "fq";
+  };
+
+  services.dnsmasq = {
+    enable = true;
+    resolveLocalQueries = true;
+    alwaysKeepRunning = true;
+    settings = {
+      local-ttl = 60;
+      domain = "lan";
+      dhcp-fqdn = false;
+      domain-needed = true;
+      bogus-priv = true;
+      no-resolv = true;
+      no-negcache = true;
+      strict-order = true;
+      log-queries = false;
+      server = [
+        "9.9.9.9"
+        "149.112.112.112"
+        "2620::fe:fe"
+        "2620::fe:9"
+        "116.203.248.56"
+        "2a01:4f8:c012:23a4::1"
+        # "127.0.0.1#5553"
+        # "::1#5553"
+        "127.0.0.1#5533"
+        "::1#5533"
+      ];
+      localise-queries = true;
+      cname = [
+        "homeassistant,ha"
+      ];
+      interface-name = [
+        "home.alanpearce.eu,wan0"
+        "nanopi.alanpearce.eu,wan0"
+        "nanopi.lan.alanpearce.eu,bridge0"
+        "syncthing.lan.alanpearce.eu,bridge0"
+        "wan,wan0"
+        "wlan,wlan0"
+        "wwan,wwan0"
+      ];
+      interface = [
+        "bridge0"
+      ];
+      # auth-zone = "lan,wan0";
+      # auth-server = [
+      #   "nanopi.alanpearce.eu,wan0"
+      # ];
+      bind-interfaces = false;
+
+      no-hosts = true;
+
+      enable-ra = true;
+      dhcp-lease-max = 240;
+      dhcp-authoritative = true;
+      dhcp-rapid-commit = true;
+      dhcp-range = [
+        "10.0.1.0,10.0.1.250,12h"
+        "::, constructor:bridge0, ra-stateless, 48h"
+        "fd12:d04f:65d::, ra-stateless, ra-names, 48h"
+      ];
+      dhcp-host = [
+        "00:a0:de:b3:0c:01,10.0.0.50,wxa-50"
+        "10:f0:68:12:b1:e0,10.0.0.11,Ruckus"
+        "9c:93:4e:ad:05:c8,10.0.0.210,xerox-b210"
+        "00:08:9b:f5:b8:25,10.0.0.42,dontpanic"
+        "d8:3a:dd:34:85:cc,d8:3a:dd:34:85:cd,10.0.0.81,ha"
+      ];
+      dhcp-option = [
+        "option:ntp-server,0.0.0.0"
+        "option:dns-server,0.0.0.0,10.0.0.81"
+        "option:tftp-server,0.0.0.0"
+        "option:ip-forward-enable,0" # ip-forwarding
+        "252,\"\\n\""
+      ];
+      dhcp-name-match = "set:wpad-ignore,wpad";
+      dhcp-ignore-names = "tag:wpad-ignore";
+
+      tftp-root = "/srv/tftp/";
+      dhcp-boot = [
+        "tag:bios,netboot.xyz.kpxe"
+        "tag:efi32,netboot.xyz.efi"
+        "tag:efi32-1,netboot.xyz.efi"
+        "tag:efi64,netboot.xyz.efi"
+        "tag:efi64-1,netboot.xyz.efi"
+        "tag:efi64-2,netboot.xyz.efi"
+      ];
+      dhcp-match = [
+        "set:bios,60,PXEClient:Arch:00000"
+        "set:efi32,60,PXEClient:Arch:00002"
+        "set:efi32-1,60,PXEClient:Arch:00006"
+        "set:efi64,60,PXEClient:Arch:00007"
+        "set:efi64-1,60,PXEClient:Arch:00008"
+        "set:efi64-2,60,PXEClient:Arch:00009"
+      ];
+    };
+  };
+  systemd.services.dnsmasq.wants = [ "network-online.target" ];
+
+  services.networkd-dispatcher = {
+    enable = true;
+    rules = {
+      update-home-address = {
+        onState = [ "configured" "configuring" ];
+        script = ''
+          #!${pkgs.runtimeShell}
+          set -eu
+
+          if [[ $IFACE == "wan0" && $OperationalState == "routable" ]]
+          then
+            systemctl start dynamic-dns-update.service
+          fi
+          exit 0
+        '';
+      };
+    };
+  };
+
+  system.stateVersion = "23.05";
+
+  programs.fish = {
+    enable = true;
+  };
+  programs.neovim = {
+    enable = true;
+    defaultEditor = true;
+    vimAlias = true;
+    viAlias = true;
+  };
+
+  users.users.root.shell = "${pkgs.fish}/bin/fish";
+
+  users.users.alan = {
+    description = "Alan Pearce";
+    isNormalUser = true;
+    extraGroups = [ "wheel" "lp" "scanner" "dialout" ];
+    shell = "${pkgs.fish}/bin/fish";
+    home = "/home/alan";
+    uid = 1000;
+    openssh.authorizedKeys.keys = [
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMvcW4Z9VxOQgEJjsRC1uSMwEJ4vru9BwjT+Z50nawp4 lan"
+    ];
+  };
+
+  users.groups = {
+    linde.members = [ ];
+  };
+  users.users = {
+    linde = {
+      group = "linde";
+      description = "Backup user for system 'linde'";
+      isSystemUser = true;
+      shell = "/bin/sh";
+      home = "/srv/backup/linde";
+      createHome = true;
+      packages = with pkgs; [ rdiff-backup ];
+      openssh.authorizedKeys.keys = [
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ74cPdIX9OlDkzHb6Y1E5sWqtIqMaf0z/SN3Tfy1Fjl root@linde"
+        "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINNXwIdGcP1vKyjmgeLw/sJntn7lajaZivepgdzaXvOt rdiff-backup"
+      ];
+    };
+  };
+
+  nix = {
+    distributedBuilds = true;
+    buildMachines = [
+      {
+        protocol = "ssh-ng";
+        sshUser = "nixremote";
+        hostName = "linde.alanpearce.eu";
+        system = "aarch64-linux";
+        sshKey = "/root/.ssh/id_buche.alanpearce.eu_nixremote";
+        maxJobs = 2;
+        speedFactor = 4;
+        supportedFeatures = [ ];
+      }
+    ];
+    settings = {
+      builders-use-substitutes = true;
+      max-jobs = 2;
+      auto-optimise-store = true;
+      experimental-features = [ "nix-command" "flakes" ];
+      substituters = [ "https://binarycache.alanpearce.eu" ];
+      trusted-public-keys = [
+        "mba-1:CxokFjx7YAQWPWMJJKcP50ZpcPUCAFEOrtWdNUMTVjw="
+        "binarycache.alanpearce.eu:ZwqO3XMuajPictjwih8OY2+RXnOKpjZEZFHJjGSxAI4="
+      ];
+    };
+    daemonCPUSchedPolicy = "batch";
+    daemonIOSchedPriority = 6;
+    gc = {
+      automatic = true;
+      dates = "weekly";
+      options = "--delete-older-than 30d";
+    };
+    optimise = {
+      automatic = true;
+      dates = [ "04:00" ];
+    };
+  };
+  nixpkgs.config.allowUnfree = true;
+  nixpkgs.overlays = [ ];
+  system.autoUpgrade = {
+    enable = false;
+    dates = "01:00";
+    randomizedDelaySec = "59 min";
+    channel = "https://nixos.org/channels/nixos-unstable-small";
+    allowReboot = true;
+    rebootWindow = {
+      lower = "01:00";
+      upper = "05:00";
+    };
+  };
+
+  services.miniupnpd = {
+    enable = false;
+    natpmp = true;
+    internalIPs = [ "bridge0" ];
+    externalInterface = "wan0";
+  };
+
+  users.groups.videos = {
+    members = [ "alan" "jellyfin" ];
+  };
+  services.jellyfin = {
+    enable = true;
+  };
+
+  users.users.syncthing = {
+    isSystemUser = true;
+    group = "syncthing";
+    homeMode = "0755";
+  };
+  users.groups.syncthing.members = [ "alan" ];
+  services.syncthing = {
+    enable = true;
+    openDefaultPorts = true;
+    dataDir = "/srv/syncthing";
+    user = "syncthing";
+    group = "syncthing";
+    key = config.age.secrets.syncthing.path;
+    cert = toString (pkgs.writeText "syncthing.crt" ''
+      -----BEGIN CERTIFICATE-----
+      MIIBmjCCASCgAwIBAgIIUOEmXGFrrX0wCgYIKoZIzj0EAwIwFDESMBAGA1UEAxMJ
+      c3luY3RoaW5nMB4XDTIyMDcxMzEwMzIxOVoXDTQ5MTIzMTIzNTk1OVowFDESMBAG
+      A1UEAxMJc3luY3RoaW5nMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEPiJT41NqucQf
+      UXiBwt+yPYnMg9G8oTt9XNA72V99K46D7mIs1F/5oESlDiCSAngXPsajxRY7wyZV
+      VoiWegfiaBOGZmq+TyaLlQ5bq/hm/Mp/jVED/rUA+BggohoZZMa2oz8wPTAOBgNV
+      HQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
+      EwEB/wQCMAAwCgYIKoZIzj0EAwIDaAAwZQIwLp4Gv5EEmjRO9EphbYJ4jxEJks7E
+      oblgnTmhfWmVWmf9avJyeGB212VYu4X8cCKDAjEAn7tTB9Y6LZvYPaLSwUKY3EzF
+      hKTYCb7VA/P1dU3tTR1vSQxnu1DsiliD/XcKe2IK
+      -----END CERTIFICATE-----
+    '');
+    settings = {
+      options = {
+        maxRecvKbps = 10240;
+        maxSendKbps = 1024;
+        globalAnnounceEnabled = false;
+        relaysEnabled = false;
+        natEnabled = false;
+        urAccepted = 4;
+        trafficClass = 1;
+      };
+      overrideFolders = false;
+      overrideDevices = false;
+    };
+  };
+
+  time.timeZone = "Europe/Berlin";
+
+  services.chrony = {
+    enable = true;
+    extraConfig = ''
+      rtcdevice /dev/rtc0
+      rtcfile /var/lib/chrony/rtc
+      rtcautotrim 30
+
+      allow 10.0.0.0/8
+      allow fd12:d04f:65d:42::0/56
+    '';
+  };
+
+  services.avahi = {
+    enable = true;
+    nssmdns4 = true;
+    denyInterfaces = [ "wan0" "wwan0" "wlan0" ];
+    browseDomains = [
+      "alanpearce.eu"
+    ];
+    publish = {
+      enable = true;
+      hinfo = true;
+      addresses = true;
+      userServices = true;
+      workstation = true;
+    };
+  };
+
+  services.samba = {
+    enable = true;
+    enableNmbd = false;
+    extraConfig = ''
+      log level = 1
+
+      interfaces = bridge0
+
+      min protocol = SMB2
+      disable netbios = yes
+      smb ports = 445
+
+      socket options = IPTOS_LOWDELAY TCP_NODELAY SO_KEEPALIVE SO_RCVBUF=65536 SO_SNDBUF=65536
+      max xmit = 131072
+      min receivefile size = 131072
+
+      aio read size = 1
+      aio write size = 1
+
+      load printers = no
+      disable spoolss = yes
+
+      mdns name = mdns
+
+      follow symlinks = yes
+
+      veto files = /Thumbs.db/.DS_Store/._.DS_Store/.apdisk/
+      delete veto files = yes
+    '';
+    shares = {
+      public = {
+        path = "/srv/public";
+        browseable = "yes";
+        "guest ok" = "yes";
+        "create mask" = "0666";
+        "directory mask" = "0777";
+        "read only" = "no";
+      };
+      Homes = {
+        "read only" = "no";
+        "valid users" = "%S";
+        "inherit acls" = "yes";
+      };
+      Videos = {
+        path = "/srv/videos";
+        "valid users" = "alan";
+        "create mask" = "0664";
+        "directory mask" = "0775";
+        "writeable" = "yes";
+      };
+    };
+  };
+  services.samba-wsdd = {
+    enable = true;
+    interface = "bridge0";
+  };
+
+  security.acme = {
+    acceptTerms = true;
+    defaults.email = "tls@alanpearce.eu";
+    certs."dns.alanpearce.eu" = {
+      reloadServices = map (x: "kresd@${toString x}") (lib.range 1 config.services.kresd.instances);
+      dnsProvider = "pdns";
+      dnsResolver = "1.1.1.1:53";
+      credentialsFile = config.age.secrets.acme.path;
+      group = "knot-resolver";
+    };
+  };
+
+  services.smartdns = {
+    enable = false;
+    bindPort = "5533";
+    settings = {
+      bind = "[::]:5533";
+      address = [
+        "/use-application-dns.net/#"
+      ];
+      server = [
+        "[::1]:5553"
+        "10.0.0.1:53 -group lan -exclude-default-group"
+      ];
+      nameserver = [
+        "/lan/lan"
+      ];
+      dualstack-ip-selection = true;
+      dualstack-ip-selection-threshold = 10;
+      dualstack-ip-allow-force-AAAA = false;
+      dnsmasq-lease-file = "/var/lib/dnsmasq/dnsmasq.leases";
+      mdns-lookup = true;
+    };
+  };
+
+  services.kresd = {
+    enable = true;
+    instances = 4;
+    listenPlain = [ "[::1]:5553" ];
+    # listenTLS = [ "853" ];
+    listenDoH = [ "[::1]:5443" ];
+    extraConfig = ''
+      net.tls(
+        '/var/lib/acme/dns.alanpearce.eu/cert.pem',
+        '/var/lib/acme/dns.alanpearce.eu/key.pem'
+      )
+
+      -- Load useful modules
+      modules = {
+        'serve_stale < cache',
+        'workarounds < iterate',
+        'hints > iterate',
+        'nsid',
+      }
+
+      local systemd_instance = os.getenv("SYSTEMD_INSTANCE")
+      nsid.name(systemd_instance)
+
+      -- Cache size
+      cache.size = 500 * MB
+
+      local internalDomains = policy.todnames({'lan.alanpearce.eu.', '10.in-addr.arpa.', '.172.in-addr.arpa.', '.168.192.in-addr.arpa.'})
+      policy.add(policy.suffix(policy.FLAGS({'NO_CACHE'}), internalDomains))
+      policy.add(policy.suffix(policy.STUB({'10.0.0.1'}), internalDomains))
+
+      -- disable duplicate DNSSEC validation when using Quad9 or private
+      trust_anchors.remove('.')
+
+      -- policy.add(policy.all(policy.TLS_FORWARD({
+      --   { "23.88.111.219", hostname="dns.alanpearce.eu" },
+      --   { "2a01:4f8:c0c:d9ce::1", hostname="dns.alanpearce.eu" },
+      -- })))
+
+      policy.add(policy.all(policy.TLS_FORWARD({
+        {'9.9.9.11', hostname='dns11.quad9.net'},
+        {'149.112.122.11', hostname='dns11.quad9.net'},
+        {'2620:fe::11', hostname='dns11.quad9.net'},
+        {'2620:fe::fe:11', hostname='dns11.quad9.net'}
+      })))
+
+      -- policy.add(policy.rpz(
+      -- 	policy.DENY_MSG('domain blocked by hblock'),
+      -- 	'/etc/knot-resolver/blocklist.rpz',
+      -- 	true
+      -- ))
+    '';
+  };
+}