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} %d/secrets.json > ${mergedConfigFile}"
112 '';
113 LoadCredential = "secrets.json:${cfg.secretsConfigFile}";
114 ExecStart = lib.concatStringsSep " " [
115 "${cfg.package}/bin/zwave-server"
116 "--config ${mergedConfigFile}"
117 "--port ${toString cfg.port}"
118 cfg.serialPort
119 (lib.escapeShellArgs cfg.extraFlags)
120 ];
121 Restart = "on-failure";
122 User = "zwave-js";
123 SupplementaryGroups = [ "dialout" ];
124 CacheDirectory = "zwave-js";
125 RuntimeDirectory = "zwave-js";
126
127 # Hardening
128 CapabilityBoundingSet = "";
129 DeviceAllow = [ cfg.serialPort ];
130 DevicePolicy = "closed";
131 DynamicUser = true;
132 LockPersonality = true;
133 MemoryDenyWriteExecute = false;
134 NoNewPrivileges = true;
135 PrivateUsers = true;
136 PrivateTmp = true;
137 ProtectClock = true;
138 ProtectControlGroups = true;
139 ProtectHome = true;
140 ProtectHostname = true;
141 ProtectKernelLogs = true;
142 ProtectKernelModules = true;
143 RemoveIPC = true;
144 RestrictNamespaces = true;
145 RestrictRealtime = true;
146 RestrictSUIDSGID = true;
147 SystemCallArchitectures = "native";
148 SystemCallFilter = [
149 "@system-service @pkey"
150 "~@privileged @resources"
151 ];
152 UMask = "0077";
153 };
154 };
155 };
156
157 meta.maintainers = with lib.maintainers; [ graham33 ];
158}