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.ints.between 1 4;
102 default = 3;
103 description = ''
104 The interval between system status reportings.
105 The value must be an integer from 1 to 4.
106 '';
107 };
108 server = lib.mkOption {
109 type = lib.types.str;
110 example = "127.0.0.1:8008";
111 description = ''
112 Address to the dashboard.
113 '';
114 };
115 uuid = lib.mkOption {
116 type = with lib.types; nullOr str;
117 # pre-defined uuid of Dns in RFC 4122
118 example = "6ba7b810-9dad-11d1-80b4-00c04fd430c8";
119 default = null;
120 description = ''
121 Must be set to a unique identifier, preferably a UUID according to
122 RFC 4122. UUIDs can be generated with `uuidgen` command, found in
123 the `util-linux` package.
124
125 Set {option}`services.nezha-agent.genUuid` to true to generate uuid
126 from {option}`networking.fqdn` automatically.
127 '';
128 };
129 };
130 };
131 };
132
133 genUuid = lib.mkOption {
134 type = lib.types.bool;
135 default = false;
136 description = ''
137 Whether to generate uuid from fqdn automatically.
138 Please note that changes in hostname/domain will result in different uuid.
139 '';
140 };
141
142 clientSecretFile = lib.mkOption {
143 type = with lib.types; nullOr path;
144 default = null;
145 description = ''
146 Path to the file contained the client_secret of the dashboard.
147 '';
148 };
149 };
150 };
151
152 imports = with lib; [
153 (mkRenamedOptionModule
154 [ "services" "nezha-agent" "disableCommandExecute" ]
155 [ "services" "nezha-agent" "settings" "disable_command_execute" ]
156 )
157 (mkRenamedOptionModule
158 [ "services" "nezha-agent" "disableNat" ]
159 [ "services" "nezha-agent" "settings" "disable_nat" ]
160 )
161 (mkRenamedOptionModule
162 [ "services" "nezha-agent" "disableSendQuery" ]
163 [ "services" "nezha-agent" "settings" "disable_send_query" ]
164 )
165 (mkRenamedOptionModule
166 [ "services" "nezha-agent" "gpu" ]
167 [ "services" "nezha-agent" "settings" "gpu" ]
168 )
169 (mkRenamedOptionModule
170 [ "services" "nezha-agent" "tls" ]
171 [ "services" "nezha-agent" "settings" "tls" ]
172 )
173 (mkRenamedOptionModule
174 [ "services" "nezha-agent" "temperature" ]
175 [ "services" "nezha-agent" "settings" "temperature" ]
176 )
177 (mkRenamedOptionModule
178 [ "services" "nezha-agent" "useIPv6CountryCode" ]
179 [ "services" "nezha-agent" "settings" "use_ipv6_country_code" ]
180 )
181 (mkRenamedOptionModule
182 [ "services" "nezha-agent" "skipConnection" ]
183 [ "services" "nezha-agent" "settings" "skip_connection_count" ]
184 )
185 (mkRenamedOptionModule
186 [ "services" "nezha-agent" "skipProcess" ]
187 [ "services" "nezha-agent" "settings" "skip_procs_count" ]
188 )
189 (mkRenamedOptionModule
190 [ "services" "nezha-agent" "reportDelay" ]
191 [ "services" "nezha-agent" "settings" "report_delay" ]
192 )
193 (mkRenamedOptionModule
194 [ "services" "nezha-agent" "server" ]
195 [ "services" "nezha-agent" "settings" "server" ]
196 )
197 (lib.mkRemovedOptionModule [ "services" "nezha-agent" "extraFlags" ] ''
198 Use `services.nezha-agent.settings` instead.
199
200 Nezha-agent v1 is no longer configured via command line flags.
201 '')
202 (lib.mkRemovedOptionModule [ "services" "nezha-agent" "passwordFile" ] ''
203 Use `services.nezha-agent.clientSecretFile` instead.
204
205 Nezha-agent v1 uses the client secret from the dashboard to connect.
206 '')
207 ];
208
209 config = lib.mkIf cfg.enable {
210 assertions = [
211 {
212 assertion = cfg.settings.uuid == null -> cfg.genUuid;
213 message = "Please set `service.nezha-agent.settings.uuid` while `genUuid` is false.";
214 }
215 {
216 assertion = cfg.settings.uuid != null -> !cfg.genUuid;
217 message = "When `service.nezha-agent.genUuid = true`, `settings.uuid` cannot be set.";
218 }
219 ];
220
221 services.nezha-agent.settings = {
222 debug = cfg.debug;
223 # Automatic updates should never be enabled in NixOS.
224 disable_auto_update = true;
225 disable_force_update = true;
226 };
227
228 systemd.services.nezha-agent = {
229 serviceConfig = {
230 Restart = "on-failure";
231 StateDirectory = "nezha-agent";
232 RuntimeDirectory = "nezha-agent";
233 WorkingDirectory = "/var/lib/nezha-agent";
234 ReadWritePaths = "/var/lib/nezha-agent";
235
236 LoadCredential = lib.optionalString (
237 cfg.clientSecretFile != null
238 ) "client-secret:${cfg.clientSecretFile}";
239
240 # Hardening
241 ProcSubset = "all"; # Needed to get host information
242 DynamicUser = true;
243 RemoveIPC = true;
244 LockPersonality = true;
245 ProtectClock = true;
246 MemoryDenyWriteExecute = true;
247 PrivateUsers = true;
248 ProtectHostname = true;
249 RestrictSUIDSGID = true;
250 AmbientCapabilities = [ ];
251 CapabilityBoundingSet = "";
252 NoNewPrivileges = true;
253 PrivateTmp = true;
254 ProtectControlGroups = true;
255 ProtectHome = true;
256 ProtectKernelLogs = true;
257 ProtectKernelModules = true;
258 ProtectKernelTunables = true;
259 ProtectProc = "invisible";
260 ProtectSystem = "strict";
261 RestrictNamespaces = true;
262 RestrictRealtime = true;
263 SystemCallArchitectures = "native";
264 UMask = "0066";
265 SystemCallFilter = [
266 "@system-service"
267 "~@privileged"
268 ];
269 RestrictAddressFamilies = [
270 "AF_INET"
271 "AF_INET6"
272 ];
273 PrivateDevices = "yes";
274 };
275 environment.HOME = "/var/lib/nezha-agent";
276 enableStrictShellChecks = true;
277 startLimitIntervalSec = 10;
278 startLimitBurst = 3;
279 script = ''
280 cp "${configFile}" "''${RUNTIME_DIRECTORY}"/config.json
281 ${lib.optionalString (cfg.clientSecretFile != null) ''
282 ${lib.getExe pkgs.jq} --arg client_secret "$(<"''${CREDENTIALS_DIRECTORY}"/client-secret)" \
283 '. + { client_secret: $client_secret }' < "''${RUNTIME_DIRECTORY}"/config.json > "''${RUNTIME_DIRECTORY}"/config.json.tmp
284 mv "''${RUNTIME_DIRECTORY}"/config.json.tmp "''${RUNTIME_DIRECTORY}"/config.json
285 ''}
286 ${lib.optionalString cfg.genUuid ''
287 ${lib.getExe pkgs.jq} --arg uuid "$(${lib.getExe' pkgs.util-linux "uuidgen"} --md5 -n @dns -N "${config.networking.fqdn}")" \
288 '. + { uuid: $uuid }' < "''${RUNTIME_DIRECTORY}"/config.json > "''${RUNTIME_DIRECTORY}"/config.json.tmp
289 mv "''${RUNTIME_DIRECTORY}"/config.json.tmp "''${RUNTIME_DIRECTORY}"/config.json
290 ''}
291 ${lib.getExe cfg.package} --config "''${RUNTIME_DIRECTORY}"/config.json
292 '';
293 wantedBy = [ "multi-user.target" ];
294 };
295 };
296}