Kieran's opinionated (and probably slightly dumb) nix config

feat: add harold

dunkirk.sh d65ec33b 58290bb5

verified
Changed files
+302
machines
atalanta
terebithia
modules
nixos
services
secrets
+22
flake.lock
··· 561 561 "type": "github" 562 562 } 563 563 }, 564 + "herald": { 565 + "inputs": { 566 + "nixpkgs": [ 567 + "nixpkgs" 568 + ] 569 + }, 570 + "locked": { 571 + "lastModified": 1768060179, 572 + "narHash": "sha256-tevqlXq0mrTo5KWLHQYjrgnhDvlTYblpEOjFDvnJg+c=", 573 + "ref": "main", 574 + "rev": "384c53a43b18d6e4351643055cedec76ec43b6c1", 575 + "revCount": 44, 576 + "type": "git", 577 + "url": "https://tangled.org/dunkirk.sh/herald" 578 + }, 579 + "original": { 580 + "ref": "main", 581 + "type": "git", 582 + "url": "https://tangled.org/dunkirk.sh/herald" 583 + } 584 + }, 564 585 "home-manager": { 565 586 "inputs": { 566 587 "nixpkgs": [ ··· 1100 1121 "flare": "flare", 1101 1122 "frc-nix": "frc-nix", 1102 1123 "hardware": "hardware", 1124 + "herald": "herald", 1103 1125 "home-manager": "home-manager_2", 1104 1126 "hyprland-contrib": "hyprland-contrib", 1105 1127 "import-tree": "import-tree",
+6
flake.nix
··· 73 73 inputs.nixpkgs.follows = "nixpkgs"; 74 74 }; 75 75 76 + herald = { 77 + url = "git+https://tangled.org/dunkirk.sh/herald?ref=main"; 78 + inputs.nixpkgs.follows = "nixpkgs"; 79 + }; 80 + 76 81 import-tree.url = "github:vic/import-tree"; 77 82 78 83 nur = { ··· 156 161 157 162 zmx-binary = prev.callPackage ./packages/zmx.nix { }; 158 163 bore-auth = prev.callPackage ./packages/bore-auth.nix { }; 164 + herald = inputs.herald.packages.${prev.system}.default; 159 165 }) 160 166 ]; 161 167 };
+5
machines/atalanta/home/default.nix
··· 93 93 zmx = true; 94 94 }; 95 95 96 + herald = { 97 + hostname = "herald.dunkirk.sh"; 98 + port = 2223; 99 + }; 100 + 96 101 prattle = { 97 102 hostname = "150.136.63.103"; 98 103 zmx = true;
+31
machines/terebithia/default.nix
··· 11 11 ./home-manager.nix 12 12 13 13 (inputs.import-tree ../../modules/nixos) 14 + ../../modules/nixos/services/herald.nix 14 15 inputs.tangled.nixosModules.knot 15 16 inputs.tangled.nixosModules.spindle 16 17 ]; ··· 142 143 file = ../../secrets/control.age; 143 144 owner = "control"; 144 145 }; 146 + herald = { 147 + file = ../../secrets/herald.age; 148 + owner = "herald"; 149 + }; 150 + herald-dkim = { 151 + file = ../../secrets/herald-dkim.age; 152 + owner = "herald"; 153 + mode = "0400"; 154 + }; 145 155 146 156 "restic/env".file = ../../secrets/restic/env.age; 147 157 "restic/repo".file = ../../secrets/restic/repo.age; ··· 223 233 22 224 234 80 225 235 443 236 + 2223 # Herald SSH 226 237 28868 # Minecraft server 227 238 ]; 228 239 allowedUDPPorts = [ ··· 483 494 }; 484 495 }; 485 496 }; 497 + }; 498 + 499 + atelier.services.herald = { 500 + enable = true; 501 + domain = "herald.dunkirk.sh"; 502 + sshPort = 2223; 503 + externalSshPort = 2223; 504 + httpPort = 8085; 505 + smtp = { 506 + host = "smtp.mailchannels.net"; 507 + port = 587; 508 + user = "kieranklukascontracting"; 509 + from = "herald@dunkirk.sh"; 510 + dkim = { 511 + selector = "mailchannels"; 512 + domain = "dunkirk.sh"; 513 + privateKeyFile = "${config.age.secrets.herald-dkim.path}"; 514 + }; 515 + }; 516 + secretsFile = config.age.secrets.herald.path; 486 517 }; 487 518 488 519 services.n8n = {
+219
modules/nixos/services/herald.nix
··· 1 + # Herald - RSS-to-Email via SSH 2 + # 3 + # Feeds uploaded via SSH/SCP, emails sent on schedule 4 + 5 + { config, lib, pkgs, ... }: 6 + 7 + let 8 + cfg = config.atelier.services.herald; 9 + 10 + # Generate config.yaml from options 11 + configFile = pkgs.writeText "herald-config.yaml" '' 12 + host: ${cfg.host} 13 + ssh_port: ${toString cfg.sshPort} 14 + http_port: ${toString cfg.httpPort} 15 + origin: https://${cfg.domain} 16 + external_ssh_port: ${toString cfg.externalSshPort} 17 + 18 + host_key_path: ${cfg.dataDir}/host_key 19 + db_path: ${cfg.dataDir}/herald.db 20 + 21 + smtp: 22 + host: ${cfg.smtp.host} 23 + port: ${toString cfg.smtp.port} 24 + user: ${cfg.smtp.user} 25 + pass: ''${SMTP_PASS} 26 + from: ${cfg.smtp.from} 27 + ${lib.optionalString (cfg.smtp.dkim.selector != null) ''dkim_selector: ${cfg.smtp.dkim.selector}''} 28 + ${lib.optionalString (cfg.smtp.dkim.domain != null) ''dkim_domain: ${cfg.smtp.dkim.domain}''} 29 + ${lib.optionalString (cfg.smtp.dkim.privateKeyFile != null) ''dkim_private_key_file: ${cfg.smtp.dkim.privateKeyFile}''} 30 + 31 + allow_all_keys: ${if cfg.allowAllKeys then "true" else "false"} 32 + ''; 33 + in 34 + { 35 + options.atelier.services.herald = { 36 + enable = lib.mkEnableOption "Herald RSS-to-Email service"; 37 + 38 + domain = lib.mkOption { 39 + type = lib.types.str; 40 + description = "Domain to serve Herald on"; 41 + example = "herald.dunkirk.sh"; 42 + }; 43 + 44 + host = lib.mkOption { 45 + type = lib.types.str; 46 + default = "0.0.0.0"; 47 + description = "Host address to bind to"; 48 + }; 49 + 50 + sshPort = lib.mkOption { 51 + type = lib.types.port; 52 + default = 2223; 53 + description = "Internal SSH port for Herald"; 54 + }; 55 + 56 + externalSshPort = lib.mkOption { 57 + type = lib.types.port; 58 + default = 2223; 59 + description = "External SSH port (for display in UI)"; 60 + }; 61 + 62 + httpPort = lib.mkOption { 63 + type = lib.types.port; 64 + default = 8085; 65 + description = "Internal HTTP port for Herald web interface"; 66 + }; 67 + 68 + dataDir = lib.mkOption { 69 + type = lib.types.path; 70 + default = "/var/lib/herald"; 71 + description = "Directory to store Herald data"; 72 + }; 73 + 74 + allowAllKeys = lib.mkOption { 75 + type = lib.types.bool; 76 + default = true; 77 + description = "Allow all SSH keys (false to use allowed_keys)"; 78 + }; 79 + 80 + smtp = { 81 + host = lib.mkOption { 82 + type = lib.types.str; 83 + description = "SMTP server host"; 84 + example = "smtp.gmail.com"; 85 + }; 86 + 87 + port = lib.mkOption { 88 + type = lib.types.port; 89 + default = 587; 90 + description = "SMTP server port"; 91 + }; 92 + 93 + user = lib.mkOption { 94 + type = lib.types.str; 95 + description = "SMTP username"; 96 + }; 97 + 98 + from = lib.mkOption { 99 + type = lib.types.str; 100 + description = "From address for emails"; 101 + example = "herald@dunkirk.sh"; 102 + }; 103 + 104 + dkim = { 105 + selector = lib.mkOption { 106 + type = lib.types.nullOr lib.types.str; 107 + default = null; 108 + description = "DKIM selector"; 109 + example = "mailchannels"; 110 + }; 111 + 112 + domain = lib.mkOption { 113 + type = lib.types.nullOr lib.types.str; 114 + default = null; 115 + description = "DKIM domain"; 116 + example = "dunkirk.sh"; 117 + }; 118 + 119 + privateKeyFile = lib.mkOption { 120 + type = lib.types.nullOr lib.types.path; 121 + default = null; 122 + description = "Path to DKIM private key file"; 123 + example = "/var/lib/herald/dkim_private.pem"; 124 + }; 125 + }; 126 + }; 127 + 128 + secretsFile = lib.mkOption { 129 + type = lib.types.path; 130 + description = "Path to agenix secrets file (must contain SMTP_PASS)"; 131 + }; 132 + 133 + package = lib.mkOption { 134 + type = lib.types.package; 135 + default = pkgs.herald; 136 + description = "Herald package to use"; 137 + }; 138 + }; 139 + 140 + config = lib.mkIf cfg.enable { 141 + # Create user and group 142 + users.groups.services = {}; 143 + 144 + users.users.herald = { 145 + isSystemUser = true; 146 + group = "herald"; 147 + extraGroups = [ "services" ]; 148 + home = cfg.dataDir; 149 + createHome = true; 150 + shell = pkgs.bash; 151 + }; 152 + 153 + users.groups.herald = {}; 154 + 155 + # Systemd service 156 + systemd.services.herald = { 157 + description = "Herald RSS-to-Email service"; 158 + wantedBy = [ "multi-user.target" ]; 159 + after = [ "network.target" ]; 160 + 161 + serviceConfig = { 162 + Type = "simple"; 163 + User = "herald"; 164 + Group = "herald"; 165 + WorkingDirectory = cfg.dataDir; 166 + EnvironmentFile = cfg.secretsFile; 167 + ExecStart = "${cfg.package}/bin/herald serve -c ${configFile}"; 168 + Restart = "always"; 169 + RestartSec = "10s"; 170 + 171 + # Security hardening 172 + NoNewPrivileges = true; 173 + ProtectSystem = "strict"; 174 + ProtectHome = true; 175 + ReadWritePaths = [ cfg.dataDir ]; 176 + PrivateTmp = true; 177 + }; 178 + 179 + preStart = '' 180 + mkdir -p ${cfg.dataDir} 181 + chown -R herald:services ${cfg.dataDir} 182 + chmod -R g+rwX ${cfg.dataDir} 183 + ''; 184 + }; 185 + 186 + # Ensure working directory exists 187 + systemd.tmpfiles.rules = [ 188 + "d ${cfg.dataDir} 0755 herald services -" 189 + ]; 190 + 191 + # Open firewall ports 192 + networking.firewall.allowedTCPPorts = [ cfg.sshPort ]; 193 + 194 + # Caddy reverse proxy for HTTP interface 195 + services.caddy.virtualHosts.${cfg.domain} = { 196 + extraConfig = '' 197 + tls { 198 + dns cloudflare {env.CLOUDFLARE_API_TOKEN} 199 + } 200 + header { 201 + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" 202 + } 203 + reverse_proxy localhost:${toString cfg.httpPort} { 204 + header_up X-Forwarded-Proto {scheme} 205 + header_up X-Forwarded-For {remote} 206 + } 207 + ''; 208 + }; 209 + 210 + # Backup configuration 211 + atelier.backup.services.herald = { 212 + paths = [ cfg.dataDir ]; 213 + exclude = [ "*.log" ]; 214 + # Uses SQLite, stop before backup 215 + preBackup = "systemctl stop herald"; 216 + postBackup = "systemctl start herald"; 217 + }; 218 + }; 219 + }
secrets/herald-dkim.age

This is a binary file and will not be displayed.

+13
secrets/herald.age
··· 1 + age-encryption.org/v1 2 + -> ssh-rsa DqcG0Q 3 + FM0Rezreqi8ZVC6v8KBCGxzdWmHBm7KRYK9RiS9LiWRsMjhhmgGZUM7lIjafKtJy 4 + 9TXHSQeXAv6iA7W5TZ059EJTx3R5q3Dn8Dim3MtLTUtthSSPst+QO9eWxBxnWmxo 5 + ZhzcmWO1Li6qmp8Mk6vO+lAdOrPWM91gPjQnubXBhzomXPMzlTlLYaSxSn3eMk+6 6 + uwMD4XSxKIdcXTjGPzSs+NnHEo6fw6WOCU1W0k+Ex7Aajj0qBXN+j86XIhloODv+ 7 + bAZ/g9ozxOTAiTZyVv2/mXGpOqUcfDn/T8Wx54EIdYdr1lbBO1lTOZ+oHoQF9HhI 8 + dJawI3lPyjrmnREpNa3nCjzZlbuunj0cWn2vwYBEju6wcYoB6t840iQl4CY+OJ0Y 9 + zAYy/JaEOmvx6qBVrDoPZQOZfErwDzqUzQOXkf8/D7e2sWtUDeN1TxcQZzRoV6Zj 10 + cYcGSQlbygLDWwJgIeuzCbgnYnKFkrnDHN5C5b/dHrJ30ozPd4skqf8dHj8JZ+VJ 11 + 12 + --- MYSw03+0Llf1UwKqYqEhkoKNNnfKJcHN6jsyy0PNAVc 13 + D����] EI��Y�f�5��]ː��� �N�H�� l������i���g�` P\'�3��K"[���]Ga
+6
secrets/secrets.nix
··· 80 80 "tangled-session.age".publicKeys = [ 81 81 kierank 82 82 ]; 83 + "herald.age".publicKeys = [ 84 + kierank 85 + ]; 86 + "herald-dkim.age".publicKeys = [ 87 + kierank 88 + ]; 83 89 }