{ config, lib, pkgs, ... }: let cfg = config.services.caddy-opnsense-blocker; yamlFormat = pkgs.formats.yaml { }; credentialsDirectory = "/run/credentials/caddy-opnsense-blocker.service"; credentialSettings = lib.optionalAttrs (cfg.credentials.opnsenseApiKeyFile != null) { opnsense.api_key_file = "${credentialsDirectory}/opnsense_api_key"; } // lib.optionalAttrs (cfg.credentials.opnsenseApiSecretFile != null) { opnsense.api_secret_file = "${credentialsDirectory}/opnsense_api_secret"; }; defaultSettings = { server = { listen_address = "127.0.0.1:9080"; read_timeout = "5s"; write_timeout = "10s"; shutdown_timeout = "15s"; }; storage.path = "/var/lib/${cfg.stateDirectoryName}/caddy-opnsense-blocker.db"; }; mergedSettings = lib.recursiveUpdate defaultSettings (lib.recursiveUpdate credentialSettings cfg.settings); configFile = yamlFormat.generate "caddy-opnsense-blocker.yaml" mergedSettings; loadCredential = lib.optional (cfg.credentials.opnsenseApiKeyFile != null) "opnsense_api_key:${toString cfg.credentials.opnsenseApiKeyFile}" ++ lib.optional (cfg.credentials.opnsenseApiSecretFile != null) "opnsense_api_secret:${toString cfg.credentials.opnsenseApiSecretFile}"; opnsenseEnabled = lib.attrByPath [ "opnsense" "enabled" ] false mergedSettings; hasOPNsenseAPIKey = cfg.credentials.opnsenseApiKeyFile != null || lib.hasAttrByPath [ "opnsense" "api_key" ] cfg.settings || lib.hasAttrByPath [ "opnsense" "api_key_file" ] cfg.settings; hasOPNsenseAPISecret = cfg.credentials.opnsenseApiSecretFile != null || lib.hasAttrByPath [ "opnsense" "api_secret" ] cfg.settings || lib.hasAttrByPath [ "opnsense" "api_secret_file" ] cfg.settings; in { options.services.caddy-opnsense-blocker = { enable = lib.mkEnableOption "caddy-opnsense-blocker"; package = lib.mkOption { type = lib.types.package; default = pkgs.callPackage ./package.nix { }; defaultText = lib.literalExpression "pkgs.callPackage ./package.nix { }"; description = "Package used to run caddy-opnsense-blocker."; }; user = lib.mkOption { type = lib.types.str; default = "caddy"; description = "User account used by the service."; }; group = lib.mkOption { type = lib.types.str; default = "caddy"; description = "Primary group used by the service."; }; stateDirectoryName = lib.mkOption { type = lib.types.str; default = "caddy-opnsense-blocker"; description = "Systemd state directory name for the service."; }; credentials = { opnsenseApiKeyFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Path to the OPNsense API key loaded through systemd credentials."; }; opnsenseApiSecretFile = lib.mkOption { type = lib.types.nullOr lib.types.path; default = null; description = "Path to the OPNsense API secret loaded through systemd credentials."; }; }; settings = lib.mkOption { type = yamlFormat.type; default = { }; example = { opnsense = { enabled = true; base_url = "https://router.example.test"; ensure_alias = true; alias.name = "blocked-ips"; }; profiles.public-web = { auto_block = true; block_unexpected_posts = true; suspicious_path_prefixes = [ "/wp-admin" "/wp-login.php" "/.env" ]; }; sources = [ { name = "public-web"; path = "/var/log/caddy/public-web.json"; profile = "public-web"; } ]; }; description = "YAML-equivalent application settings written to a generated runtime configuration file."; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = (cfg.credentials.opnsenseApiKeyFile == null) == (cfg.credentials.opnsenseApiSecretFile == null); message = "services.caddy-opnsense-blocker.credentials.opnsenseApiKeyFile and opnsenseApiSecretFile must either both be set or both be null."; } { assertion = !opnsenseEnabled || (hasOPNsenseAPIKey && hasOPNsenseAPISecret); message = "services.caddy-opnsense-blocker requires OPNsense credentials when settings.opnsense.enabled = true."; } ]; systemd.services.caddy-opnsense-blocker = { description = "Caddy OPNsense blocker"; wantedBy = [ "multi-user.target" ]; wants = [ "network-online.target" ]; after = [ "network-online.target" ]; serviceConfig = { Type = "simple"; User = cfg.user; Group = cfg.group; StateDirectory = cfg.stateDirectoryName; WorkingDirectory = "/var/lib/${cfg.stateDirectoryName}"; ExecStart = "${cfg.package}/bin/caddy-opnsense-blocker -config ${configFile}"; LoadCredential = loadCredential; Restart = "always"; RestartSec = "5s"; NoNewPrivileges = true; PrivateTmp = true; ProtectSystem = "strict"; ProtectHome = true; ProtectKernelTunables = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictSUIDSGID = true; RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; LockPersonality = true; MemoryDenyWriteExecute = true; SystemCallArchitectures = "native"; }; }; }; }