1{
2 config,
3 pkgs,
4 lib,
5 ...
6}:
7let
8 inherit (lib)
9 mkOption
10 types
11 mkIf
12 mkMerge
13 mkDefault
14 mkEnableOption
15 mkPackageOption
16 maintainers
17 ;
18 cfg = config.services.db-rest;
19in
20{
21 options = {
22 services.db-rest = {
23 enable = mkEnableOption "db-rest service";
24
25 user = mkOption {
26 type = types.str;
27 default = "db-rest";
28 description = "User account under which db-rest runs.";
29 };
30
31 group = mkOption {
32 type = types.str;
33 default = "db-rest";
34 description = "Group under which db-rest runs.";
35 };
36
37 host = mkOption {
38 type = types.str;
39 default = "127.0.0.1";
40 description = "The host address the db-rest server should listen on.";
41 };
42
43 port = mkOption {
44 type = types.port;
45 default = 3000;
46 description = "The port the db-rest server should listen on.";
47 };
48
49 redis = {
50 enable = mkOption {
51 type = types.bool;
52 default = false;
53 description = "Enable caching with redis for db-rest.";
54 };
55
56 createLocally = mkOption {
57 type = types.bool;
58 default = true;
59 description = "Configure a local redis server for db-rest.";
60 };
61
62 host = mkOption {
63 type = with types; nullOr str;
64 default = null;
65 description = "Redis host.";
66 };
67
68 port = mkOption {
69 type = with types; nullOr port;
70 default = null;
71 description = "Redis port.";
72 };
73
74 user = mkOption {
75 type = with types; nullOr str;
76 default = null;
77 description = "Optional username used for authentication with redis.";
78 };
79
80 passwordFile = mkOption {
81 type = with types; nullOr path;
82 default = null;
83 example = "/run/keys/db-rest/pasword-redis-db";
84 description = "Path to a file containing the redis password.";
85 };
86
87 useSSL = mkOption {
88 type = types.bool;
89 default = true;
90 description = "Use SSL if using a redis network connection.";
91 };
92 };
93
94 package = mkPackageOption pkgs "db-rest" { };
95 };
96 };
97
98 config = mkIf cfg.enable {
99 assertions = [
100 {
101 assertion =
102 (cfg.redis.enable && !cfg.redis.createLocally)
103 -> (cfg.redis.host != null && cfg.redis.port != null);
104 message = ''
105 {option}`services.db-rest.redis.createLocally` and redis network connection ({option}`services.db-rest.redis.host` or {option}`services.db-rest.redis.port`) enabled. Disable either of them.
106 '';
107 }
108 {
109 assertion = (cfg.redis.enable && !cfg.redis.createLocally) -> (cfg.redis.passwordFile != null);
110 message = ''
111 {option}`services.db-rest.redis.createLocally` is disabled, but {option}`services.db-rest.redis.passwordFile` is not set.
112 '';
113 }
114 ];
115
116 systemd.services.db-rest = mkMerge [
117 {
118 description = "db-rest service";
119 after = [ "network.target" ] ++ lib.optional cfg.redis.createLocally "redis-db-rest.service";
120 requires = lib.optional cfg.redis.createLocally "redis-db-rest.service";
121 wantedBy = [ "multi-user.target" ];
122 serviceConfig = {
123 Type = "simple";
124 Restart = "always";
125 RestartSec = 5;
126 WorkingDirectory = cfg.package;
127 User = cfg.user;
128 Group = cfg.group;
129 RestrictAddressFamilies = [
130 "AF_UNIX"
131 "AF_INET"
132 "AF_INET6"
133 ];
134 MemoryDenyWriteExecute = false;
135 LoadCredential = lib.optional (
136 cfg.redis.enable && cfg.redis.passwordFile != null
137 ) "REDIS_PASSWORD:${cfg.redis.passwordFile}";
138 ExecStart = mkDefault "${cfg.package}/bin/db-rest";
139
140 RemoveIPC = true;
141 NoNewPrivileges = true;
142 PrivateDevices = true;
143 ProtectClock = true;
144 ProtectKernelLogs = true;
145 ProtectControlGroups = true;
146 ProtectKernelModules = true;
147 PrivateMounts = true;
148 SystemCallArchitectures = "native";
149 ProtectHostname = true;
150 LockPersonality = true;
151 ProtectKernelTunables = true;
152 RestrictRealtime = true;
153 RestrictSUIDSGID = true;
154 RestrictNamespaces = true;
155 ProtectSystem = "strict";
156 ProtectProc = "invisible";
157 ProcSubset = "pid";
158 ProtectHome = true;
159 PrivateUsers = true;
160 PrivateTmp = true;
161 CapabilityBoundingSet = "";
162 };
163 environment = {
164 NODE_ENV = "production";
165 NODE_EXTRA_CA_CERTS = config.security.pki.caBundle;
166 HOSTNAME = cfg.host;
167 PORT = toString cfg.port;
168 };
169 }
170 (mkIf cfg.redis.enable (
171 if cfg.redis.createLocally then
172 { environment.REDIS_URL = config.services.redis.servers.db-rest.unixSocket; }
173 else
174 {
175 script =
176 let
177 username = lib.optionalString (cfg.redis.user != null) (cfg.redis.user);
178 host = cfg.redis.host;
179 port = toString cfg.redis.port;
180 protocol = if cfg.redis.useSSL then "rediss" else "redis";
181 in
182 ''
183 export REDIS_URL="${protocol}://${username}:$(${config.systemd.package}/bin/systemd-creds cat REDIS_PASSWORD)@${host}:${port}"
184 exec ${cfg.package}/bin/db-rest
185 '';
186 }
187 ))
188 ];
189
190 users.users = lib.mkMerge [
191 (lib.mkIf (cfg.user == "db-rest") {
192 db-rest = {
193 isSystemUser = true;
194 group = cfg.group;
195 };
196 })
197 (lib.mkIf cfg.redis.createLocally { ${cfg.user}.extraGroups = [ "redis-db-rest" ]; })
198 ];
199
200 users.groups = lib.mkIf (cfg.group == "db-rest") { db-rest = { }; };
201
202 services.redis.servers.db-rest.enable = cfg.redis.enable && cfg.redis.createLocally;
203 };
204 meta.maintainers = with maintainers; [ marie ];
205}