diff --git a/nixos/modules/services/monitoring/nezha-agent.nix b/nixos/modules/services/monitoring/nezha-agent.nix index 035485a939b853..87ecbfdde71057 100644 --- a/nixos/modules/services/monitoring/nezha-agent.nix +++ b/nixos/modules/services/monitoring/nezha-agent.nix @@ -6,6 +6,11 @@ }: let cfg = config.services.nezha-agent; + + # nezha-agent uses yaml as the configuration file format. + # Since we need to use jq to update the content, so here we generate json + settingsFormat = pkgs.formats.json { }; + configFile = settingsFormat.generate "config.json" cfg.settings; in { meta = { @@ -16,141 +21,280 @@ in enable = lib.mkEnableOption "Agent of Nezha Monitoring"; package = lib.mkPackageOption pkgs "nezha-agent" { }; + debug = lib.mkEnableOption "verbose log"; - tls = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Enable SSL/TLS encryption. - ''; - }; - gpu = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Enable GPU monitoring. - ''; - }; - temperature = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Enable temperature monitoring. - ''; - }; - useIPv6CountryCode = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Use ipv6 countrycode to report location. - ''; - }; - disableCommandExecute = lib.mkOption { - type = lib.types.bool; - default = true; - description = '' - Disable executing the command from dashboard. - ''; - }; - disableNat = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Disable NAT penetration. - ''; - }; - disableSendQuery = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Disable sending TCP/ICMP/HTTP requests. - ''; - }; - skipConnection = lib.mkOption { - type = lib.types.bool; - default = false; + + settings = lib.mkOption { description = '' - Do not monitor the number of connections. + Generate to {file}`config.json` as a Nix attribute set. + Check the [guide](https://nezha.wiki/en_US/guide/agent.html) + for possible options. ''; + type = lib.types.submodule { + freeformType = settingsFormat.type; + + options = { + disable_command_execute = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Disable executing the command from dashboard. + ''; + }; + disable_nat = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Disable NAT penetration. + ''; + }; + disable_send_query = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Disable sending TCP/ICMP/HTTP requests. + ''; + }; + gpu = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable GPU monitoring. + ''; + }; + tls = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable SSL/TLS encryption. + ''; + }; + temperature = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Enable temperature monitoring. + ''; + }; + use_ipv6_country_code = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Use ipv6 countrycode to report location. + ''; + }; + skip_connection_count = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Do not monitor the number of connections. + ''; + }; + skip_procs_count = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Do not monitor the number of processes. + ''; + }; + report_delay = lib.mkOption { + type = lib.types.enum [ + 1 + 2 + 3 + 4 + ]; + default = 3; + description = '' + The interval between system status reportings. + The value must be an integer from 1 to 4. + ''; + }; + server = lib.mkOption { + type = lib.types.str; + example = "127.0.0.1:8008"; + description = '' + Address to the dashboard. + ''; + }; + uuid = lib.mkOption { + type = with lib.types; nullOr str; + # pre-defined uuid of Dns in RFC 4122 + example = "6ba7b810-9dad-11d1-80b4-00c04fd430c8"; + default = null; + description = '' + Must be set to a unique identifier, preferably a UUID according to + RFC 4122. UUIDs can be generated with `uuidgen` command, found in + the `util-linux` package. + + Set {option}`services.nezha-agent.genUuid` to true to generate uuid + from {option}`networking.fqdn` automatically. + ''; + }; + }; + }; }; - skipProcess = lib.mkOption { + + genUuid = lib.mkOption { type = lib.types.bool; default = false; description = '' - Do not monitor the number of processes. + Whether to generate uuid from fqdn automatically. + Please note that changes in hostname/domain will result in different uuid. ''; }; - reportDelay = lib.mkOption { - type = lib.types.enum [ - 1 - 2 - 3 - 4 - ]; - default = 1; - description = '' - The interval between system status reportings. - The value must be an integer from 1 to 4 - ''; - }; - passwordFile = lib.mkOption { - type = with lib.types; nullOr str; + + clientSecretFile = lib.mkOption { + type = with lib.types; nullOr path; default = null; description = '' - Path to the file contained the password from dashboard. - ''; - }; - server = lib.mkOption { - type = lib.types.str; - description = '' - Address to the dashboard - ''; - }; - extraFlags = lib.mkOption { - type = lib.types.listOf lib.types.str; - default = [ ]; - example = [ "--gpu" ]; - description = '' - Extra command-line flags passed to nezha-agent. + Path to the file contained the client_secret of the dashboard. ''; }; }; }; + imports = with lib; [ + (mkRenamedOptionModule + [ "services" "nezha-agent" "disableCommandExecute" ] + [ "services" "nezha-agent" "settings" "disable_command_execute" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "disableNat" ] + [ "services" "nezha-agent" "settings" "disable_nat" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "disableSendQuery" ] + [ "services" "nezha-agent" "settings" "disable_send_query" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "gpu" ] + [ "services" "nezha-agent" "settings" "gpu" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "tls" ] + [ "services" "nezha-agent" "settings" "tls" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "temperature" ] + [ "services" "nezha-agent" "settings" "temperature" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "useIPv6CountryCode" ] + [ "services" "nezha-agent" "settings" "use_ipv6_country_code" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "skipConnection" ] + [ "services" "nezha-agent" "settings" "skip_connection_count" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "skipProcess" ] + [ "services" "nezha-agent" "settings" "skip_procs_count" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "reportDelay" ] + [ "services" "nezha-agent" "settings" "report_delay" ] + ) + (mkRenamedOptionModule + [ "services" "nezha-agent" "server" ] + [ "services" "nezha-agent" "settings" "server" ] + ) + (lib.mkRemovedOptionModule [ "services" "nezha-agent" "extraFlags" ] '' + Use `services.nezha-agent.settings` instead. + + Nezha-agent v1 is no longer configured via command line flags. + '') + (lib.mkRemovedOptionModule [ "services" "nezha-agent" "passwordFile" ] '' + Use `services.nezha-agent.clientSecretFile` instead. + + Nezha-agent v1 uses the client secret from the dashboard to connect. + '') + ]; + config = lib.mkIf cfg.enable { - systemd.packages = [ cfg.package ]; + assertions = [ + { + assertion = cfg.settings.uuid == null -> cfg.genUuid; + message = "Please set `service.nezha-agent.settings.uuid` while `genUuid` is false."; + } + { + assertion = cfg.settings.uuid != null -> !cfg.genUuid; + message = "When `service.nezha-agent.genUuid = true`, `settings.uuid` cannot be set."; + } + ]; + + services.nezha-agent.settings = { + debug = cfg.debug; + # Automatic updates should never be enabled in NixOS. + disable_auto_update = true; + disable_force_update = true; + }; systemd.services.nezha-agent = { serviceConfig = { - ProtectSystem = "full"; - PrivateDevices = "yes"; - PrivateTmp = "yes"; + Restart = "on-failure"; + StateDirectory = "nezha-agent"; + RuntimeDirectory = "nezha-agent"; + WorkingDirectory = "/var/lib/nezha-agent"; + ReadWritePaths = "/var/lib/nezha-agent"; + + LoadCredential = lib.optionalString ( + cfg.clientSecretFile != null + ) "client-secret:${cfg.clientSecretFile}"; + + # Hardening + ProcSubset = "all"; # Needed to get host information + DynamicUser = true; + RemoveIPC = true; + LockPersonality = true; + ProtectClock = true; + MemoryDenyWriteExecute = true; + PrivateUsers = true; + ProtectHostname = true; + RestrictSUIDSGID = true; + AmbientCapabilities = [ ]; + CapabilityBoundingSet = ""; NoNewPrivileges = true; + PrivateTmp = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + UMask = "0066"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + PrivateDevices = "yes"; }; - path = [ cfg.package ]; + environment.HOME = "/var/lib/nezha-agent"; + enableStrictShellChecks = true; startLimitIntervalSec = 10; startLimitBurst = 3; - script = lib.concatStringsSep " " ( - [ - "${lib.getExe cfg.package}" - "--disable-auto-update" - "--disable-force-update" - "--password $(cat ${cfg.passwordFile})" - ] - ++ lib.optional cfg.debug "--debug" - ++ lib.optional cfg.disableCommandExecute "--disable-command-execute" - ++ lib.optional cfg.disableNat "--disable-nat" - ++ lib.optional cfg.disableSendQuery "--disable-send-query" - ++ lib.optional (cfg.reportDelay != null) "--report-delay ${toString cfg.reportDelay}" - ++ lib.optional (cfg.server != null) "--server ${cfg.server}" - ++ lib.optional cfg.skipConnection "--skip-conn" - ++ lib.optional cfg.skipProcess "--skip-procs" - ++ lib.optional cfg.tls "--tls" - ++ lib.optional cfg.gpu "--gpu" - ++ lib.optional cfg.temperature "--temperature" - ++ lib.optional cfg.useIPv6CountryCode "--use-ipv6-countrycode" - ++ cfg.extraFlags - ); + script = '' + cp "${configFile}" "''${RUNTIME_DIRECTORY}"/config.json + ${lib.optionalString (cfg.clientSecretFile != null) '' + ${lib.getExe pkgs.jq} --arg client_secret "$(<"''${CREDENTIALS_DIRECTORY}"/client-secret)" \ + '. + { client_secret: $client_secret }' < "''${RUNTIME_DIRECTORY}"/config.json > "''${RUNTIME_DIRECTORY}"/config.json.tmp + mv "''${RUNTIME_DIRECTORY}"/config.json.tmp "''${RUNTIME_DIRECTORY}"/config.json + ''} + ${lib.optionalString cfg.genUuid '' + ${lib.getExe pkgs.jq} --arg uuid "$(${lib.getExe' pkgs.util-linux "uuidgen"} --md5 -n @dns -N "${config.networking.fqdn}")" \ + '. + { uuid: $uuid }' < "''${RUNTIME_DIRECTORY}"/config.json > "''${RUNTIME_DIRECTORY}"/config.json.tmp + mv "''${RUNTIME_DIRECTORY}"/config.json.tmp "''${RUNTIME_DIRECTORY}"/config.json + ''} + ${lib.getExe cfg.package} --config "''${RUNTIME_DIRECTORY}"/config.json + ''; wantedBy = [ "multi-user.target" ]; }; };