diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..22e6beb --- /dev/null +++ b/nix/module.nix @@ -0,0 +1,182 @@ +inputs: { + config, + pkgs, + lib, + ... +}: let + inherit (lib.modules) mkIf; + inherit (lib.options) mkOption mkEnableOption mkPackageOption; + inherit (lib.types) str path bool; + + settingsType = (pkgs.formats.yaml {}).type; + + cfg = config.services.watchdog; +in { + meta.maintainers = with lib.maintainers; [NotAShelf]; + + options.services.watchdog = { + enable = mkEnableOption "Watchdog privacy-preserving analytics"; + + package = mkPackageOption inputs.self.packages.${pkgs.stdenv.hostPlatform.system} "watchdog" { + pkgsText = "self.packages.\${pkgs.stdenv.hostPlatform.system}"; + }; + + settings = mkOption { + type = settingsType; + default = {}; + description = '' + Configuration for Watchdog analytics. + + See for available options. + ''; + example = { + site = { + domain = "example.com"; + salt_rotation = "daily"; + sampling = 1.0; + collect = { + pageviews = true; + country = false; + device = true; + referrer = "domain"; + }; + + path = { + strip_query = true; + strip_fragment = true; + collapse_numeric_segments = true; + max_segments = 5; + normalize_trailing_slash = true; + }; + }; + + limits = { + max_paths = 10000; + max_sources = 500; + max_custom_events = 100; + max_events_per_minute = 10000; + device_breakpoints = { + mobile = 768; + tablet = 1024; + }; + }; + + security = { + trusted_proxies = ["127.0.0.1" "::1"]; + cors = { + enabled = false; + allowed_origins = ["*"]; + }; + metrics_auth = { + enabled = false; + }; + }; + + server = { + listen_addr = "127.0.0.1:8080"; + metrics_path = "/metrics"; + ingestion_path = "/api/event"; + }; + }; + }; + + user = mkOption { + type = str; + default = "watchdog"; + description = "User account under which Watchdog runs."; + }; + + group = mkOption { + type = str; + default = "watchdog"; + description = "Group under which Watchdog runs."; + }; + + stateDir = mkOption { + type = path; + default = "/var/lib/watchdog"; + description = "Directory for Watchdog state (HLL persistence)."; + }; + + openFirewall = mkOption { + type = bool; + default = false; + description = "Open firewall port for Watchdog."; + }; + }; + + config = mkIf cfg.enable { + systemd.services.watchdog = { + description = "Watchdog Privacy Analytics"; + wantedBy = ["multi-user.target"]; + after = ["network-online.target"]; + wants = ["network-online.target"]; + + serviceConfig = { + Type = "simple"; + User = cfg.user; + Group = cfg.group; + + ExecStart = "${lib.getExe cfg.package} -config ${settingsType.generate "config.yaml" cfg.settings}"; + + Restart = "on-failure"; + RestartSec = "5s"; + + # Security hardening + NoNewPrivileges = true; + PrivateTmp = true; + ProtectSystem = "strict"; + ProtectHome = true; + ReadWritePaths = [cfg.stateDir]; + + # Capabilities + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + + # Sandboxing + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectControlGroups = true; + RestrictAddressFamilies = ["AF_INET" "AF_INET6" "AF_UNIX"]; + RestrictNamespaces = true; + LockPersonality = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + RemoveIPC = true; + PrivateMounts = true; + + # System call filtering + SystemCallFilter = ["@system-service" "~@privileged" "~@resources"]; + SystemCallErrorNumber = "EPERM"; + SystemCallArchitectures = "native"; + }; + }; + + users = { + users = mkIf (cfg.user == "watchdog") { + watchdog = { + description = "Watchdog analytics daemon user"; + isSystemUser = true; + group = cfg.group; + home = cfg.stateDir; + }; + }; + + groups = mkIf (cfg.group == "watchdog") { + watchdog = {}; + }; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group} - -" + ]; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = let + # Extract port from listen_addr (format: "host:port" or ":port") + listenAddr = cfg.settings.server.listen_addr or "127.0.0.1:8080"; + port = lib.toInt (lib.last (lib.splitString ":" listenAddr)); + in [port]; + }; + }; +} diff --git a/nix/package.nix b/nix/package.nix index 62e8dd7..f5a987e 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -1,11 +1,40 @@ -{buildGoModule}: -buildGoModule { - pname = "sample-go"; - version = "0.0.1"; +{ + lib, + buildGoModule, +}: +buildGoModule (finalAttrs: { + pname = "watchdog"; + version = "0.1.0"; - src = ../.; + src = let + fs = lib.fileset; + s = ../.; + in + fs.toSource { + root = s; + fileset = fs.unions [ + (s + /cmd) + (s + /internal) + (s + /go.mod) + (s + /go.sum) + ]; + }; - vendorHash = ""; + vendorHash = "sha256-jMqPVvMZDm406Gi2g4zNSRJMySLAN7/CR/2NgF+gqtA="; - ldflags = ["-s" "-w"]; -} + ldflags = ["-s" "-w" "-X main.version=${finalAttrs.version}"]; + + # Copy web assets + postInstall = '' + mkdir -p $out/share/watchdog + cp -r $src/web $out/share/watchdog/ + ''; + + meta = { + description = "Privacy-preserving web analytics with Prometheus-native metrics"; + homepage = "https://github.com/notashelf/watchdog"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [NotAShelf]; + mainProgram = "watchdog"; + }; +}) diff --git a/nix/shell.nix b/nix/shell.nix index 226ab68..5c746d3 100644 --- a/nix/shell.nix +++ b/nix/shell.nix @@ -2,13 +2,15 @@ mkShell, go, gopls, + golines, delve, }: mkShell { name = "go"; packages = [ - delve go - gopls + gopls # formatter + golines # line wrapper + delve # debugger ]; }