summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorAlan Pearce2024-04-11 00:04:06 +0200
committerAlan Pearce2024-04-11 00:04:06 +0200
commit6c18a33c758f0226e660f924ddd71a6d3ad53004 (patch)
tree946b87a7e268604a47217e5e3250e144b4eaf3f0
parent2b09b74ba617346a0c9c932543e658837ef9e5d2 (diff)
downloadnixfiles-6c18a33c758f0226e660f924ddd71a6d3ad53004.tar.lz
nixfiles-6c18a33c758f0226e660f924ddd71a6d3ad53004.tar.zst
nixfiles-6c18a33c758f0226e660f924ddd71a6d3ad53004.zip
Import server configurations
-rw-r--r--.envrc1
-rw-r--r--flake.lock216
-rw-r--r--flake.nix66
-rw-r--r--patches/cgit-pink.patch26
-rw-r--r--secrets/acme.age10
-rw-r--r--secrets/binarycache.agebin0 -> 435 bytes
-rw-r--r--secrets/dyndns.agebin0 -> 476 bytes
-rw-r--r--secrets/identities/se.txt4
-rw-r--r--secrets/paperless.age7
-rw-r--r--secrets/powerdns.age7
-rw-r--r--secrets/secrets.nix31
-rw-r--r--secrets/syncthing.agebin0 -> 608 bytes
-rwxr-xr-xsetup/hetzner.sh81
-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
17 files changed, 2141 insertions, 27 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 00000000..3550a30f
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/flake.lock b/flake.lock
index e7ba6924..1e6b6e15 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,8 +1,53 @@
 {
   "nodes": {
+    "agenix": {
+      "inputs": {
+        "darwin": "darwin",
+        "home-manager": "home-manager",
+        "nixpkgs": [
+          "nixpkgs"
+        ],
+        "systems": "systems"
+      },
+      "locked": {
+        "lastModified": 1712079060,
+        "narHash": "sha256-/JdiT9t+zzjChc5qQiF+jhrVhRt8figYH29rZO7pFe4=",
+        "owner": "ryantm",
+        "repo": "agenix",
+        "rev": "1381a759b205dff7a6818733118d02253340fd5e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "ryantm",
+        "repo": "agenix",
+        "type": "github"
+      }
+    },
     "darwin": {
       "inputs": {
         "nixpkgs": [
+          "agenix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1700795494,
+        "narHash": "sha256-gzGLZSiOhf155FW7262kdHo2YDeugp3VuIFb4/GGng0=",
+        "owner": "lnl7",
+        "repo": "nix-darwin",
+        "rev": "4b9b83d5a92e8c1fbfd8eb27eda375908c11ec4d",
+        "type": "github"
+      },
+      "original": {
+        "owner": "lnl7",
+        "ref": "master",
+        "repo": "nix-darwin",
+        "type": "github"
+      }
+    },
+    "darwin_2": {
+      "inputs": {
+        "nixpkgs": [
           "nixpkgs"
         ]
       },
@@ -21,6 +66,26 @@
         "type": "github"
       }
     },
+    "deploy-rs": {
+      "inputs": {
+        "flake-compat": "flake-compat",
+        "nixpkgs": "nixpkgs",
+        "utils": "utils"
+      },
+      "locked": {
+        "lastModified": 1711973905,
+        "narHash": "sha256-UFKME/N1pbUtn+2Aqnk+agUt8CekbpuqwzljivfIme8=",
+        "owner": "serokell",
+        "repo": "deploy-rs",
+        "rev": "88b3059b020da69cbe16526b8d639bd5e0b51c8b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "serokell",
+        "repo": "deploy-rs",
+        "type": "github"
+      }
+    },
     "emacs-overlay": {
       "inputs": {
         "flake-utils": "flake-utils",
@@ -30,11 +95,11 @@
         "nixpkgs-stable": "nixpkgs-stable"
       },
       "locked": {
-        "lastModified": 1711789603,
-        "narHash": "sha256-5c8prZYLBFgMDoBrBTMuAu6F33HHF9kfK+i4d39gUDA=",
+        "lastModified": 1712768907,
+        "narHash": "sha256-o3yQ8ZWR4AOoLPk3+If1F0xmm65LsszDLvC7iUqWVG0=",
         "owner": "nix-community",
         "repo": "emacs-overlay",
-        "rev": "f8ad90d467e2a48cf91aa0b3b75ac7929dc07425",
+        "rev": "d4d9c62d5e11ad212aee995ad56b8885067179e7",
         "type": "github"
       },
       "original": {
@@ -43,9 +108,25 @@
         "type": "github"
       }
     },
+    "flake-compat": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1696426674,
+        "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
     "flake-utils": {
       "inputs": {
-        "systems": "systems"
+        "systems": "systems_3"
       },
       "locked": {
         "lastModified": 1710146030,
@@ -64,15 +145,36 @@
     "home-manager": {
       "inputs": {
         "nixpkgs": [
+          "agenix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1703113217,
+        "narHash": "sha256-7ulcXOk63TIT2lVDSExj7XzFx09LpdSAPtvgtM7yQPE=",
+        "owner": "nix-community",
+        "repo": "home-manager",
+        "rev": "3bfaacf46133c037bb356193bd2f1765d9dc82c1",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "home-manager",
+        "type": "github"
+      }
+    },
+    "home-manager_2": {
+      "inputs": {
+        "nixpkgs": [
           "nixpkgs"
         ]
       },
       "locked": {
-        "lastModified": 1711625603,
-        "narHash": "sha256-W+9dfqA9bqUIBV5u7jaIARAzMe3kTq/Hp2SpSVXKRQw=",
+        "lastModified": 1712759992,
+        "narHash": "sha256-2APpO3ZW4idlgtlb8hB04u/rmIcKA8O7pYqxF66xbNY=",
         "owner": "nix-community",
         "repo": "home-manager",
-        "rev": "c0ef0dab55611c676ad7539bf4e41b3ec6fa87d2",
+        "rev": "31357486b0ef6f4e161e002b6893eeb4fafc3ca9",
         "type": "github"
       },
       "original": {
@@ -88,11 +190,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1711249705,
-        "narHash": "sha256-h/NQECj6mIzF4XR6AQoSpkCnwqAM+ol4+qOdYi2ykmQ=",
+        "lastModified": 1712459390,
+        "narHash": "sha256-e12bNDottaGoBgd0AdH/bQvk854xunlWAdZwr/oHO1c=",
         "owner": "Mic92",
         "repo": "nix-index-database",
-        "rev": "34519f3bb678a5abbddf7b200ac5347263ee781b",
+        "rev": "4676d72d872459e1e3a248d049609f110c570e9a",
         "type": "github"
       },
       "original": {
@@ -103,11 +205,11 @@
     },
     "nixos-hardware": {
       "locked": {
-        "lastModified": 1711352745,
-        "narHash": "sha256-luvqik+i3HTvCbXQZgB6uggvEcxI9uae0nmrgtXJ17U=",
+        "lastModified": 1712760404,
+        "narHash": "sha256-4zhaEW1nB+nGbCNMjOggWeY5nXs/H0Y71q0+h+jdxoU=",
         "owner": "NixOS",
         "repo": "nixos-hardware",
-        "rev": "9a763a7acc4cfbb8603bb0231fec3eda864f81c0",
+        "rev": "e1c4bac14beb8c409d0534382cf967171706b9d9",
         "type": "github"
       },
       "original": {
@@ -118,27 +220,27 @@
     },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1711703276,
-        "narHash": "sha256-iMUFArF0WCatKK6RzfUJknjem0H9m4KgorO/p3Dopkk=",
+        "lastModified": 1702272962,
+        "narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "d8fe5e6c92d0d190646fb9f1056741a229980089",
+        "rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "ref": "nixos-unstable",
+        "ref": "nixpkgs-unstable",
         "repo": "nixpkgs",
         "type": "github"
       }
     },
     "nixpkgs-stable": {
       "locked": {
-        "lastModified": 1711668574,
-        "narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
+        "lastModified": 1712588820,
+        "narHash": "sha256-y31s5idk3jMJMAVE4Ud9AdI7HT3CgTAeMTJ0StqKN7Y=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
+        "rev": "d272ca50d1f7424fbfcd1e6f1c9e01d92f6da167",
         "type": "github"
       },
       "original": {
@@ -148,16 +250,34 @@
         "type": "github"
       }
     },
+    "nixpkgs_2": {
+      "locked": {
+        "lastModified": 1712608508,
+        "narHash": "sha256-vMZ5603yU0wxgyQeHJryOI+O61yrX2AHwY6LOFyV1gM=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "4cba8b53da471aea2ab2b0c1f30a81e7c451f4b6",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
     "root": {
       "inputs": {
-        "darwin": "darwin",
+        "agenix": "agenix",
+        "darwin": "darwin_2",
+        "deploy-rs": "deploy-rs",
         "emacs-overlay": "emacs-overlay",
-        "home-manager": "home-manager",
+        "home-manager": "home-manager_2",
         "nix-index-database": "nix-index-database",
         "nixos-hardware": "nixos-hardware",
-        "nixpkgs": "nixpkgs",
+        "nixpkgs": "nixpkgs_2",
         "secrets": "secrets",
-        "utils": "utils"
+        "utils": "utils_2"
       }
     },
     "secrets": {
@@ -206,11 +326,59 @@
         "type": "github"
       }
     },
+    "systems_3": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "systems_4": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
     "utils": {
       "inputs": {
         "systems": "systems_2"
       },
       "locked": {
+        "lastModified": 1701680307,
+        "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "flake-utils",
+        "type": "github"
+      }
+    },
+    "utils_2": {
+      "inputs": {
+        "systems": "systems_4"
+      },
+      "locked": {
         "lastModified": 1710146030,
         "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
         "owner": "numtide",
diff --git a/flake.nix b/flake.nix
index 9115ef97..0ce7efb8 100644
--- a/flake.nix
+++ b/flake.nix
@@ -14,6 +14,14 @@
       flake = false;
     };
     utils.url = "github:numtide/flake-utils";
+    agenix.url = "github:ryantm/agenix";
+    agenix.inputs.nixpkgs.follows = "nixpkgs";
+    deploy-rs.url = "github:serokell/deploy-rs";
+  };
+
+  nixConfig = {
+    extra-substituters = [ "https://deploy-rs.cachix.org" ];
+    extra-trusted-public-keys = [ "deploy-rs.cachix.org-1:xfNobmiwF/vzvK1gpfediPwpdIP0rpDV2rYqx40zdSI=" ];
   };
 
   outputs =
@@ -27,6 +35,8 @@
     , nix-index-database
     , secrets
     , emacs-overlay
+    , agenix
+    , deploy-rs
     , ...
     }:
     let
@@ -41,7 +51,7 @@
     in
     {
       nixosConfigurations.prefect = nixpkgs.lib.nixosSystem {
-        system = "x86_64-linux";
+        system = utils.lib.system.x86_64-linux;
         specialArgs = { inherit inputs; };
         modules = [
           ./system/prefect.nix
@@ -56,7 +66,18 @@
       nixosConfigurations.nanopi = nixpkgs.lib.nixosSystem {
         system = utils.lib.system.aarch64-linux;
         specialArgs = { inherit inputs; };
-        modules = [ ./nanopi.nix ];
+        modules = [
+          agenix.nixosModules.default
+          ./system/nanopi.nix
+        ];
+      };
+      nixosConfigurations.linde = nixpkgs.lib.nixosSystem {
+        system = utils.lib.system.aarch64-linux;
+        specialArgs = { inherit inputs; };
+        modules = [
+          agenix.nixosModules.default
+          ./system/linde.nix
+        ];
       };
       darwinConfigurations.mba = darwin.lib.darwinSystem {
         system = utils.lib.system.aarch64-darwin;
@@ -99,5 +120,44 @@
           (secrets + "/default.nix")
         ];
       };
-    };
+
+      checks = builtins.mapAttrs
+        (system: deployLib:
+          deployLib.deployChecks self.deploy)
+        deploy-rs.lib;
+
+      deploy = {
+        remoteBuild = true;
+        interactiveSudo = true;
+        nodes.linde = {
+          hostname = "linde";
+          profiles.system = {
+            user = "root";
+            path = deploy-rs.lib.${utils.lib.system.aarch64-linux}.activate.nixos
+              self.nixosConfigurations.linde;
+          };
+        };
+        nodes.nanopi = {
+          hostname = "nanopi";
+          profiles.system = {
+            user = "root";
+            path = deploy-rs.lib.${utils.lib.system.aarch64-linux}.activate.nixos
+              self.nixosConfigurations.nanopi;
+          };
+        };
+      };
+    } // utils.lib.eachDefaultSystem (system:
+    let
+      pkgs = import nixpkgs { inherit system; };
+    in
+    {
+      devShells = {
+        default = pkgs.mkShell {
+          packages = [
+            deploy-rs.packages.${system}.default
+            agenix.packages.${system}.default
+          ];
+        };
+      };
+    });
 }
diff --git a/patches/cgit-pink.patch b/patches/cgit-pink.patch
new file mode 100644
index 00000000..0e91525e
--- /dev/null
+++ b/patches/cgit-pink.patch
@@ -0,0 +1,26 @@
+diff --git a/cgit.c b/cgit.c
+index dd28a79..451f518 100644
+--- a/cgit.c
++++ b/cgit.c
+@@ -489,7 +489,7 @@ static char *guess_defbranch(void)
+ 
+ 	ref = resolve_ref_unsafe("HEAD", 0, &oid, NULL);
+ 	if (!ref || !skip_prefix(ref, "refs/heads/", &refname))
+-		return "master";
++		return "main";
+ 	return xstrdup(refname);
+ }
+ 
+diff --git a/ui-repolist.c b/ui-repolist.c
+index 97b11c5..cde9cd0 100644
+--- a/ui-repolist.c
++++ b/ui-repolist.c
+@@ -53,7 +53,7 @@ static int get_repo_modtime(const struct cgit_repo *repo, time_t *mtime)
+ 
+ 	strbuf_reset(&path);
+ 	strbuf_addf(&path, "%s/refs/heads/%s", repo->path,
+-		    repo->defbranch ? repo->defbranch : "master");
++		    repo->defbranch ? repo->defbranch : "main");
+ 	if (stat(path.buf, &s) == 0) {
+ 		*mtime = s.st_mtime;
+ 		r->mtime = *mtime;
diff --git a/secrets/acme.age b/secrets/acme.age
new file mode 100644
index 00000000..0a7be3b7
--- /dev/null
+++ b/secrets/acme.age
@@ -0,0 +1,10 @@
+age-encryption.org/v1
+-> ssh-ed25519 cvV2sw 9M8YWkJtggtWDra9rnc3iaf9qbXF+pdRaVtQbSMOQkY
+/jxEwLo3+qmuyWIpQD65O2Kp0qEKJwydM4tFnXdvRfU
+-> ssh-ed25519 hzg5VQ BLUqRuSfJXtSc/M1H1jTBwCWnkSZqm5SC+LrxIXNn34
+D1A2DDFQ7FK3bOPUvJJpumQM7MeESMHqhwZXxug6b34
+-> piv-p256 u9NeZg AkXH20bJj+m6TgPzvsPltDyOIPRAB9YR0MXx/b8DFFD2
+kGH6MvfeDaKgXf5Ba92PF4PwTRotSZglGQZO2impo1Q
+--- wYsP2oTEuD/C40pKjx0LAYuoE9/w2LgxuDRGqsmcnCo
+5ZPse!$lI}4޻o!6W1[c͆7;IO hn_"Ɗ[㉪}QܭR@:Mlw_cl]lk:mߊb}#WkUV;NYha!e:8éTC8l5[qc]U}}`NxdX1D#
+ck
\ No newline at end of file
diff --git a/secrets/binarycache.age b/secrets/binarycache.age
new file mode 100644
index 00000000..fae59d4d
--- /dev/null
+++ b/secrets/binarycache.age
Binary files differdiff --git a/secrets/dyndns.age b/secrets/dyndns.age
new file mode 100644
index 00000000..cd1668f1
--- /dev/null
+++ b/secrets/dyndns.age
Binary files differdiff --git a/secrets/identities/se.txt b/secrets/identities/se.txt
new file mode 100644
index 00000000..e1c6b851
--- /dev/null
+++ b/secrets/identities/se.txt
@@ -0,0 +1,4 @@
+# created: 2024-04-10T12:44:17Z
+# access control: any biometry or passcode
+# public key: age1se1qdx3wrvaxevk3g40ngqreqc9n4gl0rwcjdvnptz5vw96jjjuf2rv2wp8c5m
+AGE-PLUGIN-SE-1QJPQZ7P3SGQHGVYP75XQYUNTXXQ7UVQTPSPKY6TYQSZ85T758GCYSRQRWP6KYPZPQ3X3WRVAXEVK3G40NGQREQC9N4GL0RWCJDVNPTZ5VW96JJJUF2RV2XR0REV8SUYMVLR9LK9VWDZGRRTNSKQL0ATZYYWS9NAZZACW5QMMXQYQCQMJDDHSYQGQXQRSCQNTWSPQZPPS9CXQYAMTQS599D2V5HHZE0VLL5MW9EW28X23MP9NRSULQL3GAHD0RU0M5EG3F38XWDKEJM6LPWTNQPCVQF3XXQSPPYCQWRQZDDMQYQGZXQTSCQMTD9JQGY8R2D8H498GF5PMR8WYFNAUD7L8XQNSCQMJDDKSGGYSRXZGXMRKCX08VHSJTFQWK28KT7SX2TYS6HLC3CQQUE303RKEEUC85RQZV4JRZAPSWGXQXCTRDSCKKVPFPSPK7CMTXY3RQGQVQD3HQMCVR9ZX2ANFVDJ57AMWV4EYZAT5DPJKUARFVDSHG6T0DCCQJRQYDAJX2MQPQYQNQ2SVQ3HHXEMWXY3RQGQVQD3HQMCVR9ZX2ANFVDJ57AMWV4EYZAT5DPJKUARFVDSHG6T0DCCQWRQZDASSZQGP9T2Q6F
\ No newline at end of file
diff --git a/secrets/paperless.age b/secrets/paperless.age
new file mode 100644
index 00000000..5fe24928
--- /dev/null
+++ b/secrets/paperless.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 cvV2sw ytN6X4qbcYAEijPOcZ3CV0BQOU5Osocy5Zv3ebnekF0
+WNzH2Gr0L1qKENdRalb44Xg0Ay4tD38+CED6crF3Nd4
+-> piv-p256 u9NeZg A4sl4hcJrAyDZxWkPn84u3gNXLZBj3guVya3vP60X3WT
+8uVbdrw6ZNvpaYc056vqTMDraJYLMWviXt+LnhGQDn4
+--- m1ofHvgDQvjWZV9iU5ran6oG1pK+jfMKKiouQc9SYfo
+c&$IנX8pcPobor„VE.,.Z*"-	`wLeӊxl	f-4%9-vC90YP!n
fsVO?CfӉCd_iW,lZ>J`9
\ No newline at end of file
diff --git a/secrets/powerdns.age b/secrets/powerdns.age
new file mode 100644
index 00000000..b4a3de03
--- /dev/null
+++ b/secrets/powerdns.age
@@ -0,0 +1,7 @@
+age-encryption.org/v1
+-> ssh-ed25519 cvV2sw 8XNRgmCnrZX7Gug5WDA9uBUPjXW+hz+NxGbpAfI5nBY
+5DvhP919xM/ccBaRHjd+JnsiWNSbz3118p5iHUoDf8E
+-> piv-p256 u9NeZg AlR0PR5A+mSyaT8wStnNKuWnO28YwUEwV/UPXK2JvlEi
+xBtUPfZehUkzTeNTVk6FBZt4R/XfvKwzrkWipVJbHMY
+--- mdXzr4rVzRNBekbnenHAXzr8SFYhHRJIO0/HaeL7QVI
+$GAC[bmtty$Oc:<'`V<'/	XJ*!@լ!|g8ņ4ǞE`CpmAK-
\ No newline at end of file
diff --git a/secrets/secrets.nix b/secrets/secrets.nix
new file mode 100644
index 00000000..86d1062c
--- /dev/null
+++ b/secrets/secrets.nix
@@ -0,0 +1,31 @@
+let
+  users = {
+    alan = [
+      "age1se1qdx3wrvaxevk3g40ngqreqc9n4gl0rwcjdvnptz5vw96jjjuf2rv2wp8c5m" # mba age-plugin-se
+    ];
+  };
+
+  machines = {
+    linde = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHHdh3J7dEmh9G+CVmzFEC8/ont35ZXpCFcpLUO863vC";
+    nanopi = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG/KOwhb4pyuw4U8hnkPAbRNk6o41Fmvsa67cY6MHA9k";
+  };
+
+  secrets = with machines; {
+    acme = [ linde nanopi ];
+
+    binarycache = [ linde ];
+    paperless = [ linde ];
+    powerdns = [ linde ];
+
+    dyndns = [ nanopi ];
+    syncthing = [ nanopi ];
+  };
+in
+builtins.listToAttrs (
+  map
+    (secretName: {
+      name = "${secretName}.age";
+      value.publicKeys = secrets.${secretName} ++ users.alan;
+    })
+    (builtins.attrNames secrets)
+)
diff --git a/secrets/syncthing.age b/secrets/syncthing.age
new file mode 100644
index 00000000..680dd1ce
--- /dev/null
+++ b/secrets/syncthing.age
Binary files differdiff --git a/setup/hetzner.sh b/setup/hetzner.sh
new file mode 100755
index 00000000..250a9211
--- /dev/null
+++ b/setup/hetzner.sh
@@ -0,0 +1,81 @@
+#! /usr/bin/env bash
+
+# Script to install NixOS from the Hetzner Cloud NixOS bootable ISO image.
+# (tested with Hetzner's `NixOS 20.03 (amd64/minimal)` ISO image).
+#
+# This script wipes the disk of the server!
+#
+# Instructions:
+#
+# 1. Mount the above mentioned ISO image from the Hetzner Cloud GUI
+#    and reboot the server into it; do not run the default system (e.g. Ubuntu).
+# 2. To be able to SSH straight in (recommended), you must replace hardcoded pubkey
+#    further down in the section labelled "Replace this by your SSH pubkey" by you own,
+#    and host the modified script way under a URL of your choosing
+#    (e.g. gist.github.com with git.io as URL shortener service).
+# 3. Run on the server:
+#
+#       # Replace this URL by your own that has your pubkey in
+#       curl -L https://home.alanpearce.eu/public/hetzner.sh | sudo bash
+#
+#    This will install NixOS and power off the server.
+# 4. Unmount the ISO image from the Hetzner Cloud GUI.
+# 5. Turn the server back on from the Hetzner Cloud GUI.
+#
+# To run it from the Hetzner Cloud web terminal without typing it down,
+# you can either select it and then middle-click onto the web terminal, (that pastes
+# to it), or use `xdotool` (you have e.g. 3 seconds to focus the window):
+#
+#     sleep 3 && xdotool type --delay 50 'curl YOUR_URL_HERE | sudo bash'
+#
+# (In the xdotool invocation you may have to replace chars so that
+# the right chars appear on the US-English keyboard.)
+#
+# If you do not replace the pubkey, you'll be running with my pubkey, but you can
+# change it afterwards by logging in via the Hetzner Cloud web terminal as `root`
+# with empty password.
+
+set -e
+
+# Hetzner Cloud OS images grow the root partition to the size of the local
+# disk on first boot. In case the NixOS live ISO is booted immediately on
+# first powerup, that does not happen. Thus we need to grow the partition
+# by deleting and re-creating it.
+sgdisk -d 1 /dev/sda
+sgdisk -N 1 /dev/sda
+partprobe /dev/sda
+
+mkfs.ext4 -F /dev/sda1 # wipes all data!
+
+mount /dev/sda1 /mnt
+
+nixos-generate-config --root /mnt
+
+# Delete trailing `}` from `configuration.nix` so that we can append more to it.
+sed -i -E 's:^\}\s*$::g' /mnt/etc/nixos/configuration.nix
+
+# Extend/override default `configuration.nix`:
+echo '
+  boot.loader.grub.devices = [ "/dev/sda" ];
+
+  # Initial empty root password for easy login:
+  users.users.root.initialHashedPassword = "";
+  services.openssh = {
+    permitRootLogin = "prohibit-password";
+    enable = true;
+  };
+
+	programs.fish.enable = true;
+  users.users.root = {
+    initialHashedPassword = "";
+    shell = "${pkgs.fish}/bin/fish";
+    openssh.authorizedKeys.keys = [
+      "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII8VIII+598QOBxi/52O1Kb19RdUdX0aZmS1/dNoyqc5 alan@hetzner"
+    ];
+  };
+}
+' >> /mnt/etc/nixos/configuration.nix
+
+nixos-install --no-root-passwd
+
+poweroff
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
+      -- ))
+    '';
+  };
+}