From d65ec33b2c63ccb5750fdd8a14de0ab7ff8f6038 Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Sat, 10 Jan 2026 16:12:26 +0700 Subject: [PATCH] feat: add harold --- flake.lock | 22 +++ flake.nix | 6 + machines/atalanta/home/default.nix | 5 + machines/terebithia/default.nix | 31 ++++ modules/nixos/services/herald.nix | 219 +++++++++++++++++++++++++++++ secrets/herald-dkim.age | Bin 0 -> 2321 bytes secrets/herald.age | 13 ++ secrets/secrets.nix | 6 + 8 files changed, 302 insertions(+) create mode 100644 modules/nixos/services/herald.nix create mode 100644 secrets/herald-dkim.age create mode 100644 secrets/herald.age diff --git a/flake.lock b/flake.lock index 2cebf4c..d08e253 100644 --- a/flake.lock +++ b/flake.lock @@ -561,6 +561,27 @@ "type": "github" } }, + "herald": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768060179, + "narHash": "sha256-tevqlXq0mrTo5KWLHQYjrgnhDvlTYblpEOjFDvnJg+c=", + "ref": "main", + "rev": "384c53a43b18d6e4351643055cedec76ec43b6c1", + "revCount": 44, + "type": "git", + "url": "https://tangled.org/dunkirk.sh/herald" + }, + "original": { + "ref": "main", + "type": "git", + "url": "https://tangled.org/dunkirk.sh/herald" + } + }, "home-manager": { "inputs": { "nixpkgs": [ @@ -1100,6 +1121,7 @@ "flare": "flare", "frc-nix": "frc-nix", "hardware": "hardware", + "herald": "herald", "home-manager": "home-manager_2", "hyprland-contrib": "hyprland-contrib", "import-tree": "import-tree", diff --git a/flake.nix b/flake.nix index 489b460..4b350af 100644 --- a/flake.nix +++ b/flake.nix @@ -73,6 +73,11 @@ inputs.nixpkgs.follows = "nixpkgs"; }; + herald = { + url = "git+https://tangled.org/dunkirk.sh/herald?ref=main"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + import-tree.url = "github:vic/import-tree"; nur = { @@ -156,6 +161,7 @@ zmx-binary = prev.callPackage ./packages/zmx.nix { }; bore-auth = prev.callPackage ./packages/bore-auth.nix { }; + herald = inputs.herald.packages.${prev.system}.default; }) ]; }; diff --git a/machines/atalanta/home/default.nix b/machines/atalanta/home/default.nix index a15ebe0..78cf957 100644 --- a/machines/atalanta/home/default.nix +++ b/machines/atalanta/home/default.nix @@ -93,6 +93,11 @@ zmx = true; }; + herald = { + hostname = "herald.dunkirk.sh"; + port = 2223; + }; + prattle = { hostname = "150.136.63.103"; zmx = true; diff --git a/machines/terebithia/default.nix b/machines/terebithia/default.nix index 68a2adf..cfd2c09 100644 --- a/machines/terebithia/default.nix +++ b/machines/terebithia/default.nix @@ -11,6 +11,7 @@ ./home-manager.nix (inputs.import-tree ../../modules/nixos) + ../../modules/nixos/services/herald.nix inputs.tangled.nixosModules.knot inputs.tangled.nixosModules.spindle ]; @@ -142,6 +143,15 @@ file = ../../secrets/control.age; owner = "control"; }; + herald = { + file = ../../secrets/herald.age; + owner = "herald"; + }; + herald-dkim = { + file = ../../secrets/herald-dkim.age; + owner = "herald"; + mode = "0400"; + }; "restic/env".file = ../../secrets/restic/env.age; "restic/repo".file = ../../secrets/restic/repo.age; @@ -223,6 +233,7 @@ 22 80 443 + 2223 # Herald SSH 28868 # Minecraft server ]; allowedUDPPorts = [ @@ -485,6 +496,26 @@ }; }; + atelier.services.herald = { + enable = true; + domain = "herald.dunkirk.sh"; + sshPort = 2223; + externalSshPort = 2223; + httpPort = 8085; + smtp = { + host = "smtp.mailchannels.net"; + port = 587; + user = "kieranklukascontracting"; + from = "herald@dunkirk.sh"; + dkim = { + selector = "mailchannels"; + domain = "dunkirk.sh"; + privateKeyFile = "${config.age.secrets.herald-dkim.path}"; + }; + }; + secretsFile = config.age.secrets.herald.path; + }; + services.n8n = { enable = true; environment = { diff --git a/modules/nixos/services/herald.nix b/modules/nixos/services/herald.nix new file mode 100644 index 0000000..6341bcf --- /dev/null +++ b/modules/nixos/services/herald.nix @@ -0,0 +1,219 @@ +# Herald - RSS-to-Email via SSH +# +# Feeds uploaded via SSH/SCP, emails sent on schedule + +{ config, lib, pkgs, ... }: + +let + cfg = config.atelier.services.herald; + + # Generate config.yaml from options + configFile = pkgs.writeText "herald-config.yaml" '' + host: ${cfg.host} + ssh_port: ${toString cfg.sshPort} + http_port: ${toString cfg.httpPort} + origin: https://${cfg.domain} + external_ssh_port: ${toString cfg.externalSshPort} + + host_key_path: ${cfg.dataDir}/host_key + db_path: ${cfg.dataDir}/herald.db + + smtp: + host: ${cfg.smtp.host} + port: ${toString cfg.smtp.port} + user: ${cfg.smtp.user} + pass: ''${SMTP_PASS} + from: ${cfg.smtp.from} + ${lib.optionalString (cfg.smtp.dkim.selector != null) ''dkim_selector: ${cfg.smtp.dkim.selector}''} + ${lib.optionalString (cfg.smtp.dkim.domain != null) ''dkim_domain: ${cfg.smtp.dkim.domain}''} + ${lib.optionalString (cfg.smtp.dkim.privateKeyFile != null) ''dkim_private_key_file: ${cfg.smtp.dkim.privateKeyFile}''} + + allow_all_keys: ${if cfg.allowAllKeys then "true" else "false"} + ''; +in +{ + options.atelier.services.herald = { + enable = lib.mkEnableOption "Herald RSS-to-Email service"; + + domain = lib.mkOption { + type = lib.types.str; + description = "Domain to serve Herald on"; + example = "herald.dunkirk.sh"; + }; + + host = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + description = "Host address to bind to"; + }; + + sshPort = lib.mkOption { + type = lib.types.port; + default = 2223; + description = "Internal SSH port for Herald"; + }; + + externalSshPort = lib.mkOption { + type = lib.types.port; + default = 2223; + description = "External SSH port (for display in UI)"; + }; + + httpPort = lib.mkOption { + type = lib.types.port; + default = 8085; + description = "Internal HTTP port for Herald web interface"; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/var/lib/herald"; + description = "Directory to store Herald data"; + }; + + allowAllKeys = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Allow all SSH keys (false to use allowed_keys)"; + }; + + smtp = { + host = lib.mkOption { + type = lib.types.str; + description = "SMTP server host"; + example = "smtp.gmail.com"; + }; + + port = lib.mkOption { + type = lib.types.port; + default = 587; + description = "SMTP server port"; + }; + + user = lib.mkOption { + type = lib.types.str; + description = "SMTP username"; + }; + + from = lib.mkOption { + type = lib.types.str; + description = "From address for emails"; + example = "herald@dunkirk.sh"; + }; + + dkim = { + selector = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "DKIM selector"; + example = "mailchannels"; + }; + + domain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "DKIM domain"; + example = "dunkirk.sh"; + }; + + privateKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + description = "Path to DKIM private key file"; + example = "/var/lib/herald/dkim_private.pem"; + }; + }; + }; + + secretsFile = lib.mkOption { + type = lib.types.path; + description = "Path to agenix secrets file (must contain SMTP_PASS)"; + }; + + package = lib.mkOption { + type = lib.types.package; + default = pkgs.herald; + description = "Herald package to use"; + }; + }; + + config = lib.mkIf cfg.enable { + # Create user and group + users.groups.services = {}; + + users.users.herald = { + isSystemUser = true; + group = "herald"; + extraGroups = [ "services" ]; + home = cfg.dataDir; + createHome = true; + shell = pkgs.bash; + }; + + users.groups.herald = {}; + + # Systemd service + systemd.services.herald = { + description = "Herald RSS-to-Email service"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + Type = "simple"; + User = "herald"; + Group = "herald"; + WorkingDirectory = cfg.dataDir; + EnvironmentFile = cfg.secretsFile; + ExecStart = "${cfg.package}/bin/herald serve -c ${configFile}"; + Restart = "always"; + RestartSec = "10s"; + + # Security hardening + NoNewPrivileges = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [ cfg.dataDir ]; + PrivateTmp = true; + }; + + preStart = '' + mkdir -p ${cfg.dataDir} + chown -R herald:services ${cfg.dataDir} + chmod -R g+rwX ${cfg.dataDir} + ''; + }; + + # Ensure working directory exists + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0755 herald services -" + ]; + + # Open firewall ports + networking.firewall.allowedTCPPorts = [ cfg.sshPort ]; + + # Caddy reverse proxy for HTTP interface + services.caddy.virtualHosts.${cfg.domain} = { + extraConfig = '' + tls { + dns cloudflare {env.CLOUDFLARE_API_TOKEN} + } + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + } + reverse_proxy localhost:${toString cfg.httpPort} { + header_up X-Forwarded-Proto {scheme} + header_up X-Forwarded-For {remote} + } + ''; + }; + + # Backup configuration + atelier.backup.services.herald = { + paths = [ cfg.dataDir ]; + exclude = [ "*.log" ]; + # Uses SQLite, stop before backup + preBackup = "systemctl stop herald"; + postBackup = "systemctl start herald"; + }; + }; +} diff --git a/secrets/herald-dkim.age b/secrets/herald-dkim.age new file mode 100644 index 0000000000000000000000000000000000000000..c2f88b689fc3ff8ca6b1a376c52f496d92dc244b GIT binary patch literal 2321 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 literal 0 HcmV?d00001 diff --git a/secrets/herald.age b/secrets/herald.age new file mode 100644 index 0000000..d5e03c6 --- /dev/null +++ b/secrets/herald.age @@ -0,0 +1,13 @@ +age-encryption.org/v1 +-> ssh-rsa DqcG0Q +FM0Rezreqi8ZVC6v8KBCGxzdWmHBm7KRYK9RiS9LiWRsMjhhmgGZUM7lIjafKtJy +9TXHSQeXAv6iA7W5TZ059EJTx3R5q3Dn8Dim3MtLTUtthSSPst+QO9eWxBxnWmxo +ZhzcmWO1Li6qmp8Mk6vO+lAdOrPWM91gPjQnubXBhzomXPMzlTlLYaSxSn3eMk+6 +uwMD4XSxKIdcXTjGPzSs+NnHEo6fw6WOCU1W0k+Ex7Aajj0qBXN+j86XIhloODv+ +bAZ/g9ozxOTAiTZyVv2/mXGpOqUcfDn/T8Wx54EIdYdr1lbBO1lTOZ+oHoQF9HhI +dJawI3lPyjrmnREpNa3nCjzZlbuunj0cWn2vwYBEju6wcYoB6t840iQl4CY+OJ0Y +zAYy/JaEOmvx6qBVrDoPZQOZfErwDzqUzQOXkf8/D7e2sWtUDeN1TxcQZzRoV6Zj +cYcGSQlbygLDWwJgIeuzCbgnYnKFkrnDHN5C5b/dHrJ30ozPd4skqf8dHj8JZ+VJ + +--- MYSw03+0Llf1UwKqYqEhkoKNNnfKJcHN6jsyy0PNAVc +D����] EI��Y�f�5��]ː��� �N�H�� l�����i���g�` P\'�3��K"[���]Ga \ No newline at end of file diff --git a/secrets/secrets.nix b/secrets/secrets.nix index bd478c2..2219687 100644 --- a/secrets/secrets.nix +++ b/secrets/secrets.nix @@ -80,4 +80,10 @@ in "tangled-session.age".publicKeys = [ kierank ]; + "herald.age".publicKeys = [ + kierank + ]; + "herald-dkim.age".publicKeys = [ + kierank + ]; } -- 2.51.2