1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.nezha-agent;
9
10 # nezha-agent uses yaml as the configuration file format.
11 # Since we need to use jq to update the content, so here we generate json
12 settingsFormat = pkgs.formats.json { };
13 configFile = settingsFormat.generate "config.json" cfg.settings;
14in
15{
16 meta = {
17 maintainers = with lib.maintainers; [ moraxyc ];
18 };
19 options = {
20 services.nezha-agent = {
21 enable = lib.mkEnableOption "Agent of Nezha Monitoring";
22
23 package = lib.mkPackageOption pkgs "nezha-agent" { };
24
25 debug = lib.mkEnableOption "verbose log";
26
27 settings = lib.mkOption {
28 description = ''
29 Generate to {file}`config.json` as a Nix attribute set.
30 Check the [guide](https://nezha.wiki/en_US/guide/agent.html)
31 for possible options.
32 '';
33 type = lib.types.submodule {
34 freeformType = settingsFormat.type;
35
36 options = {
37 disable_command_execute = lib.mkOption {
38 type = lib.types.bool;
39 default = true;
40 description = ''
41 Disable executing the command from dashboard.
42 '';
43 };
44 disable_nat = lib.mkOption {
45 type = lib.types.bool;
46 default = false;
47 description = ''
48 Disable NAT penetration.
49 '';
50 };
51 disable_send_query = lib.mkOption {
52 type = lib.types.bool;
53 default = false;
54 description = ''
55 Disable sending TCP/ICMP/HTTP requests.
56 '';
57 };
58 gpu = lib.mkOption {
59 type = lib.types.bool;
60 default = false;
61 description = ''
62 Enable GPU monitoring.
63 '';
64 };
65 tls = lib.mkOption {
66 type = lib.types.bool;
67 default = false;
68 description = ''
69 Enable SSL/TLS encryption.
70 '';
71 };
72 temperature = lib.mkOption {
73 type = lib.types.bool;
74 default = true;
75 description = ''
76 Enable temperature monitoring.
77 '';
78 };
79 use_ipv6_country_code = lib.mkOption {
80 type = lib.types.bool;
81 default = true;
82 description = ''
83 Use ipv6 countrycode to report location.
84 '';
85 };
86 skip_connection_count = lib.mkOption {
87 type = lib.types.bool;
88 default = false;
89 description = ''
90 Do not monitor the number of connections.
91 '';
92 };
93 skip_procs_count = lib.mkOption {
94 type = lib.types.bool;
95 default = false;
96 description = ''
97 Do not monitor the number of processes.
98 '';
99 };
100 report_delay = lib.mkOption {
101 type = lib.types.enum [
102 1
103 2
104 3
105 4
106 ];
107 default = 3;
108 description = ''
109 The interval between system status reportings.
110 The value must be an integer from 1 to 4.
111 '';
112 };
113 server = lib.mkOption {
114 type = lib.types.str;
115 example = "127.0.0.1:8008";
116 description = ''
117 Address to the dashboard.
118 '';
119 };
120 uuid = lib.mkOption {
121 type = with lib.types; nullOr str;
122 # pre-defined uuid of Dns in RFC 4122
123 example = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
124 default = null;
125 description = ''
126 Must be set to a unique identifier, preferably a UUID according to
127 RFC 4122. UUIDs can be generated with `uuidgen` command, found in
128 the `util-linux` package.
129
130 Set {option}`services.nezha-agent.genUuid` to true to generate uuid
131 from {option}`networking.fqdn` automatically.
132 '';
133 };
134 };
135 };
136 };
137
138 genUuid = lib.mkOption {
139 type = lib.types.bool;
140 default = false;
141 description = ''
142 Whether to generate uuid from fqdn automatically.
143 Please note that changes in hostname/domain will result in different uuid.
144 '';
145 };
146
147 clientSecretFile = lib.mkOption {
148 type = with lib.types; nullOr path;
149 default = null;
150 description = ''
151 Path to the file contained the client_secret of the dashboard.
152 '';
153 };
154 };
155 };
156
157 imports = with lib; [
158 (mkRenamedOptionModule
159 [ "services" "nezha-agent" "disableCommandExecute" ]
160 [ "services" "nezha-agent" "settings" "disable_command_execute" ]
161 )
162 (mkRenamedOptionModule
163 [ "services" "nezha-agent" "disableNat" ]
164 [ "services" "nezha-agent" "settings" "disable_nat" ]
165 )
166 (mkRenamedOptionModule
167 [ "services" "nezha-agent" "disableSendQuery" ]
168 [ "services" "nezha-agent" "settings" "disable_send_query" ]
169 )
170 (mkRenamedOptionModule
171 [ "services" "nezha-agent" "gpu" ]
172 [ "services" "nezha-agent" "settings" "gpu" ]
173 )
174 (mkRenamedOptionModule
175 [ "services" "nezha-agent" "tls" ]
176 [ "services" "nezha-agent" "settings" "tls" ]
177 )
178 (mkRenamedOptionModule
179 [ "services" "nezha-agent" "temperature" ]
180 [ "services" "nezha-agent" "settings" "temperature" ]
181 )
182 (mkRenamedOptionModule
183 [ "services" "nezha-agent" "useIPv6CountryCode" ]
184 [ "services" "nezha-agent" "settings" "use_ipv6_country_code" ]
185 )
186 (mkRenamedOptionModule
187 [ "services" "nezha-agent" "skipConnection" ]
188 [ "services" "nezha-agent" "settings" "skip_connection_count" ]
189 )
190 (mkRenamedOptionModule
191 [ "services" "nezha-agent" "skipProcess" ]
192 [ "services" "nezha-agent" "settings" "skip_procs_count" ]
193 )
194 (mkRenamedOptionModule
195 [ "services" "nezha-agent" "reportDelay" ]
196 [ "services" "nezha-agent" "settings" "report_delay" ]
197 )
198 (mkRenamedOptionModule
199 [ "services" "nezha-agent" "server" ]
200 [ "services" "nezha-agent" "settings" "server" ]
201 )
202 (lib.mkRemovedOptionModule [ "services" "nezha-agent" "extraFlags" ] ''
203 Use `services.nezha-agent.settings` instead.
204
205 Nezha-agent v1 is no longer configured via command line flags.
206 '')
207 (lib.mkRemovedOptionModule [ "services" "nezha-agent" "passwordFile" ] ''
208 Use `services.nezha-agent.clientSecretFile` instead.
209
210 Nezha-agent v1 uses the client secret from the dashboard to connect.
211 '')
212 ];
213
214 config = lib.mkIf cfg.enable {
215 assertions = [
216 {
217 assertion = cfg.settings.uuid == null -> cfg.genUuid;
218 message = "Please set `service.nezha-agent.settings.uuid` while `genUuid` is false.";
219 }
220 {
221 assertion = cfg.settings.uuid != null -> !cfg.genUuid;
222 message = "When `service.nezha-agent.genUuid = true`, `settings.uuid` cannot be set.";
223 }
224 ];
225
226 services.nezha-agent.settings = {
227 debug = cfg.debug;
228 # Automatic updates should never be enabled in NixOS.
229 disable_auto_update = true;
230 disable_force_update = true;
231 };
232
233 systemd.services.nezha-agent = {
234 serviceConfig = {
235 Restart = "on-failure";
236 StateDirectory = "nezha-agent";
237 RuntimeDirectory = "nezha-agent";
238 WorkingDirectory = "/var/lib/nezha-agent";
239 ReadWritePaths = "/var/lib/nezha-agent";
240
241 LoadCredential = lib.optionalString (
242 cfg.clientSecretFile != null
243 ) "client-secret:${cfg.clientSecretFile}";
244
245 # Hardening
246 ProcSubset = "all"; # Needed to get host information
247 DynamicUser = true;
248 RemoveIPC = true;
249 LockPersonality = true;
250 ProtectClock = true;
251 MemoryDenyWriteExecute = true;
252 PrivateUsers = true;
253 ProtectHostname = true;
254 RestrictSUIDSGID = true;
255 AmbientCapabilities = [ ];
256 CapabilityBoundingSet = "";
257 NoNewPrivileges = true;
258 PrivateTmp = true;
259 ProtectControlGroups = true;
260 ProtectHome = true;
261 ProtectKernelLogs = true;
262 ProtectKernelModules = true;
263 ProtectKernelTunables = true;
264 ProtectProc = "invisible";
265 ProtectSystem = "strict";
266 RestrictNamespaces = true;
267 RestrictRealtime = true;
268 SystemCallArchitectures = "native";
269 UMask = "0066";
270 SystemCallFilter = [
271 "@system-service"
272 "~@privileged"
273 ];
274 RestrictAddressFamilies = [
275 "AF_INET"
276 "AF_INET6"
277 ];
278 PrivateDevices = "yes";
279 };
280 environment.HOME = "/var/lib/nezha-agent";
281 enableStrictShellChecks = true;
282 startLimitIntervalSec = 10;
283 startLimitBurst = 3;
284 script = ''
285 cp "${configFile}" "''${RUNTIME_DIRECTORY}"/config.json
286 ${lib.optionalString (cfg.clientSecretFile != null) ''
287 ${lib.getExe pkgs.jq} --arg client_secret "$(<"''${CREDENTIALS_DIRECTORY}"/client-secret)" \
288 '. + { client_secret: $client_secret }' < "''${RUNTIME_DIRECTORY}"/config.json > "''${RUNTIME_DIRECTORY}"/config.json.tmp
289 mv "''${RUNTIME_DIRECTORY}"/config.json.tmp "''${RUNTIME_DIRECTORY}"/config.json
290 ''}
291 ${lib.optionalString cfg.genUuid ''
292 ${lib.getExe pkgs.jq} --arg uuid "$(${lib.getExe' pkgs.util-linux "uuidgen"} --md5 -n @dns -N "${config.networking.fqdn}")" \
293 '. + { uuid: $uuid }' < "''${RUNTIME_DIRECTORY}"/config.json > "''${RUNTIME_DIRECTORY}"/config.json.tmp
294 mv "''${RUNTIME_DIRECTORY}"/config.json.tmp "''${RUNTIME_DIRECTORY}"/config.json
295 ''}
296 ${lib.getExe cfg.package} --config "''${RUNTIME_DIRECTORY}"/config.json
297 '';
298 wantedBy = [ "multi-user.target" ];
299 };
300 };
301}