watchdog/nix/module.nix
NotAShelf 75018f69df
nix: initial nixos module; complete packaging
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I59ec5a4f67ddae2139e72ef4c0c113366a6a6964
2026-03-02 22:38:08 +03:00

182 lines
4.7 KiB
Nix

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 <https://github.com/notashelf/watchdog> 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];
};
};
}