From 3689dedd75aa4d726d0e0f0a00b87f74fa42ab9b Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Fri, 17 Apr 2026 11:13:04 +0200 Subject: [PATCH] init --- flake.nix | 42 +--------- module.nix | 240 ++++++++++++++++------------------------------------- 2 files changed, 75 insertions(+), 207 deletions(-) diff --git a/flake.nix b/flake.nix index 3563331..6044005 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ { - description = "mail-quota-warning package and service"; + description = "stalwart-tlsa-cloudflare service and timer"; inputs.nixpkgs.url = "nixpkgs/nixos-25.11"; @@ -15,45 +15,7 @@ } ); in { - overlay = final: prev: { - mail-quota-warning = with final; python3Packages.buildPythonApplication { - pname = "mail-quota-warning"; - version = "0.0.1"; - format = "other"; - - src = self; - - dependencies = with python3Packages; [ - python - pyyaml - imaplib2 - ]; - - installPhase = '' - install -Dm755 ${./mail-quota-warning.py} $out/bin/mail-quota-warning - ''; - - meta.mainProgram = "mail-quota-warning"; - }; - }; - - packages = forAllSystems (system: { - inherit (nixpkgsFor.${system}) mail-quota-warning; - }); - - defaultPackage = forAllSystems (system: self.packages.${system}.mail-quota-warning); - - devShells = forAllSystems (system: let - pkgs = import nixpkgs { inherit system; overlays = [ self.overlay ]; }; - in pkgs.mkShell { - buildInputs = with pkgs; with python3Packages; [ - python - requests - beautifulsoup4 - ]; - }); - - # mail-quota-warning service module + # stalwart-tlsa-cloudflare-update service module nixosModule = (import ./module.nix); }; } diff --git a/module.nix b/module.nix index d65da3f..5b30f87 100644 --- a/module.nix +++ b/module.nix @@ -6,53 +6,29 @@ }: let - cfg = config.services.mail-quota-warning; + cfg = config.services.stalwart-tlsa-cloudflare-update; in { options = { - services.mail-quota-warning = { + services.stalwart-tlsa-cloudflare-update = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' - Enable mail-quota-warning daemon. + Enable stalwart-tlsa-cloudflare-update daemon. ''; }; settings = lib.mkOption { type = lib.types.submodule { freeformType = with lib.types; attrsOf anything; - options = { - CHECK_INTERVAL_DAYS = lib.mkOption { - default = 7; - type = lib.types.int; - description = '' - Interval of days in which a warning message will be - delivered. - ''; - }; - QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption { - default = 80; - type = lib.types.int; - description = '' - Threshold of used mailbox space in percent after which - a warning message will be delivered. - ''; - }; - }; }; default = { }; description = '' - Extra options which should be used by the mailbox quota warning script. - ''; - example = lib.literalExpression '' - { - CHECK_INTERVAL_DAYS = 7; - QUOTA_WARNING_THRESHOLD_PERCENT = 80; - } + Extra options which should be used by the stalwart-tlsa-cloudflare-update script. ''; }; @@ -62,10 +38,9 @@ in absolute = true; }); default = null; - example = "/run/keys/mail-quota-warning-secrets"; + example = "/run/keys/cloudflare-api-key"; description = '' - A YAML file containing secrets, see example config file - in the repository. + Secret file containing cloudflare api key ''; }; @@ -85,151 +60,82 @@ in config = lib.mkIf cfg.enable { - - # FIXME maybe move this to an external module - systemd.services.tlsa-cloudflare-update = { - description = "Check and update TLSA/DANE record for mx1 from Stalwart ACME Cert"; - - after = [ "network-online.target" "stalwart-mail.service" "agenix.service" ]; - wants = [ "network-online.target" "stalwart-mail.service" "agenix.service" ]; - - serviceConfig = { - Type = "oneshot"; - User = "stalwart-mail"; - Group = "stalwart-mail"; - EnvironmentFile = config.age.secrets.gotlsaflare-cloudflare-token.path; - RuntimeDirectory = "stalwart-tlsa"; - }; - - environment = { - DOMAIN = "project-insanity.org"; - SUBDOMAIN = "mx1"; - PORT = "25"; - ACME_PROVIDER_ID = "cloudflare3"; - }; - - path = with pkgs; [ - bash - coreutils - openssl - dnsutils - gotlsaflare - rocksdb.tools - gawk - ]; - - script = '' - set -eu - - TLSA_RECORD="_$PORT._tcp.$SUBDOMAIN.$DOMAIN" - DB_PATH="/var/lib/stalwart-mail/db" - TEMP_RAW="/run/stalwart-tlsa/cert.bundle" - TEMP_CRT="/run/stalwart-tlsa/cert.crt" - - echo "Starting TLSA update process for $DOMAIN" - - ldb --db="$DB_PATH" --column_family=s get "acme.$ACME_PROVIDER_ID.cert" | base64 -d > "$TEMP_RAW" - - if [ ! -s "$TEMP_RAW" ]; then - echo "ERROR: ACME certificate extraction failed" - exit 1 - fi - - openssl x509 -in "$TEMP_RAW" -out "$TEMP_CRT" - - LOCAL_HASH=$(openssl x509 -in "$TEMP_CRT" -pubkey -noout | openssl pkey -pubin -outform DER | openssl sha256 | awk '{print tolower($2)}') - echo "Local hash: $LOCAL_HASH" - - UPSTREAM_HASH=$(dig +nosplit +short TLSA "$TLSA_RECORD" | awk '{print tolower($4)}' | head -n1) - echo "Upstream hash: $UPSTREAM_HASH" - - if [ "$LOCAL_HASH" = "$UPSTREAM_HASH" ]; then - echo "Hashes match. DNS is up to date." - exit 0 - fi - - echo "Hashes differ! Updating Cloudflare..." - gotlsaflare update \ - --url "$DOMAIN" \ - --subdomain "$SUBDOMAIN" \ - --tcp"$PORT" \ - --cert "$TEMP_CRT" - - echo "TLSA update completed successfully." - ''; - }; - - systemd.timers.tlsa-cloudflare-update = { - description = "Run TLSA check and update every 15 minutes"; - wantedBy = [ "timers.target" ]; - timerConfig = { - OnBootSec = "2m"; - OnUnitActiveSec = "15m"; - Unit = "tlsa-cloudflare-update.service"; - }; - }; - - systemd.services."mail-quota-warning" = { - description = "mail-quota-warning script"; - after = [ "network.target" ]; - wants = [ "network-online.target" ]; - environment = { - PYTHONUNBUFFERED = "1"; - } - // lib.mapAttrs (_: v: toString v) cfg.settings; + systemd.services.tlsa-cloudflare-update = { + description = "Check and update TLSA/DANE record for mx1 from Stalwart ACME Cert"; + + after = [ "network-online.target" "stalwart-mail.service" "agenix.service" ]; + wants = [ "network-online.target" "stalwart-mail.service" "agenix.service" ]; + serviceConfig = { - Type = "simple"; - LoadCredential = lib.optionalString (cfg.secretFile != null) "secrets.yaml:${cfg.secretFile}"; - ExecStart = "${lib.getExe pkgs.mail-quota-warning} --config \${CREDENTIALS_DIRECTORY}/secrets.yaml"; - WorkingDirectory = "%S/mail-quota-warning"; - StateDirectory = "mail-quota-warning"; - - # hardening - AmbientCapabilities = ""; - CapabilityBoundingSet = ""; - DevicePolicy = "closed"; - DynamicUser = true; - LockPersonality = true; - MemoryDenyWriteExecute = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateTmp = true; - PrivateUsers = true; - ProcSubset = "pid"; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProtectSystem = "strict"; - RemoveIPC = true; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - "~@privileged" - ]; - UMask = "0077"; + Type = "oneshot"; + User = "stalwart-mail"; + Group = "stalwart-mail"; + EnvironmentFile = cfg.secretFile; + RuntimeDirectory = "stalwart-tlsa"; }; - }; - systemd.timers.mail-quota-warning = { + environment = lib.mapAttrs (_: v: toString v) cfg.settings; + + path = with pkgs; [ + bash + coreutils + openssl + dnsutils + gotlsaflare + rocksdb.tools + gawk + ]; + + script = '' + set -eu + + TLSA_RECORD="_$PORT._tcp.$SUBDOMAIN.$DOMAIN" + DB_PATH="/var/lib/stalwart-mail/db" + TEMP_RAW="/run/stalwart-tlsa/cert.bundle" + TEMP_CRT="/run/stalwart-tlsa/cert.crt" + + echo "Starting TLSA update process for $DOMAIN" + + ldb --db="$DB_PATH" --column_family=s get "acme.$ACME_PROVIDER_ID.cert" | base64 -d > "$TEMP_RAW" + + if [ ! -s "$TEMP_RAW" ]; then + echo "ERROR: ACME certificate extraction failed" + exit 1 + fi + + openssl x509 -in "$TEMP_RAW" -out "$TEMP_CRT" + + LOCAL_HASH=$(openssl x509 -in "$TEMP_CRT" -pubkey -noout | openssl pkey -pubin -outform DER | openssl sha256 | awk '{print tolower($2)}') + echo "Local hash: $LOCAL_HASH" + + UPSTREAM_HASH=$(dig +nosplit +short TLSA "$TLSA_RECORD" | awk '{print tolower($4)}' | head -n1) + echo "Upstream hash: $UPSTREAM_HASH" + + if [ "$LOCAL_HASH" = "$UPSTREAM_HASH" ]; then + echo "Hashes match. DNS is up to date." + exit 0 + fi + + echo "Hashes differ! Updating Cloudflare..." + gotlsaflare update \ + --url "$DOMAIN" \ + --subdomain "$SUBDOMAIN" \ + --tcp"$PORT" \ + --cert "$TEMP_CRT" + + echo "TLSA update completed successfully." + ''; + }; + + systemd.timers.stalwart-tlsa-cloudflare-update = { + description = "Run TLSA check and update"; + wantedBy = [ "timers.target" ]; timerConfig = { OnCalendar = [ "" cfg.interval ]; }; - wantedBy = [ "timers.target" ]; }; };