1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 cfg = config.services.zwave-js;
9 mergedConfigFile = "/run/zwave-js/config.json";
10 settingsFormat = pkgs.formats.json { };
11in
12{
13 options.services.zwave-js = {
14 enable = lib.mkEnableOption "the zwave-js server on boot";
15
16 package = lib.mkPackageOption pkgs "zwave-js-server" { };
17
18 port = lib.mkOption {
19 type = lib.types.port;
20 default = 3000;
21 description = ''
22 Port for the server to listen on.
23 '';
24 };
25
26 serialPort = lib.mkOption {
27 type = lib.types.path;
28 description = ''
29 Serial port device path for Z-Wave controller.
30 '';
31 example = "/dev/ttyUSB0";
32 };
33
34 secretsConfigFile = lib.mkOption {
35 type = lib.types.path;
36 description = ''
37 JSON file containing secret keys. A dummy example:
38
39 ```
40 {
41 "securityKeys": {
42 "S0_Legacy": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
43 "S2_Unauthenticated": "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB",
44 "S2_Authenticated": "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC",
45 "S2_AccessControl": "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"
46 }
47 }
48 ```
49
50 See
51 <https://zwave-js.github.io/node-zwave-js/#/getting-started/security-s2>
52 for details. This file will be merged with the module-generated config
53 file (taking precedence).
54
55 Z-Wave keys can be generated with:
56
57 {command}`< /dev/urandom tr -dc A-F0-9 | head -c32 ;echo`
58
59
60 ::: {.warning}
61 A file in the nix store should not be used since it will be readable to
62 all users.
63 :::
64 '';
65 example = "/secrets/zwave-js-keys.json";
66 };
67
68 settings = lib.mkOption {
69 type = lib.types.submodule {
70 freeformType = settingsFormat.type;
71
72 options = {
73 storage = {
74 cacheDir = lib.mkOption {
75 type = lib.types.path;
76 default = "/var/cache/zwave-js";
77 readOnly = true;
78 description = "Cache directory";
79 };
80 };
81 };
82 };
83 default = { };
84 description = ''
85 Configuration settings for the generated config
86 file.
87 '';
88 };
89
90 extraFlags = lib.mkOption {
91 type = with lib.types; listOf str;
92 default = [ ];
93 example = [ "--mock-driver" ];
94 description = ''
95 Extra flags to pass to command
96 '';
97 };
98 };
99
100 config = lib.mkIf cfg.enable {
101 systemd.services.zwave-js =
102 let
103 configFile = settingsFormat.generate "zwave-js-config.json" cfg.settings;
104 in
105 {
106 wantedBy = [ "multi-user.target" ];
107 after = [ "network.target" ];
108 description = "Z-Wave JS Server";
109 serviceConfig = {
110 ExecStartPre = ''
111 /bin/sh -c "${pkgs.jq}/bin/jq -s '.[0] * .[1]' ${configFile} ${cfg.secretsConfigFile} > ${mergedConfigFile}"
112 '';
113 ExecStart = lib.concatStringsSep " " [
114 "${cfg.package}/bin/zwave-server"
115 "--config ${mergedConfigFile}"
116 "--port ${toString cfg.port}"
117 cfg.serialPort
118 (lib.escapeShellArgs cfg.extraFlags)
119 ];
120 Restart = "on-failure";
121 User = "zwave-js";
122 SupplementaryGroups = [ "dialout" ];
123 CacheDirectory = "zwave-js";
124 RuntimeDirectory = "zwave-js";
125
126 # Hardening
127 CapabilityBoundingSet = "";
128 DeviceAllow = [ cfg.serialPort ];
129 DevicePolicy = "closed";
130 DynamicUser = true;
131 LockPersonality = true;
132 MemoryDenyWriteExecute = false;
133 NoNewPrivileges = true;
134 PrivateUsers = true;
135 PrivateTmp = true;
136 ProtectClock = true;
137 ProtectControlGroups = true;
138 ProtectHome = true;
139 ProtectHostname = true;
140 ProtectKernelLogs = true;
141 ProtectKernelModules = true;
142 RemoveIPC = true;
143 RestrictNamespaces = true;
144 RestrictRealtime = true;
145 RestrictSUIDSGID = true;
146 SystemCallArchitectures = "native";
147 SystemCallFilter = [
148 "@system-service @pkey"
149 "~@privileged @resources"
150 ];
151 UMask = "0077";
152 };
153 };
154 };
155
156 meta.maintainers = with lib.maintainers; [ graham33 ];
157}