1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.postgres-websockets;
10
11 # Turns an attrset of libpq connection params:
12 # {
13 # dbname = "postgres";
14 # user = "authenticator";
15 # }
16 # into a libpq connection string:
17 # dbname=postgres user=authenticator
18 PGWS_DB_URI = lib.pipe cfg.environment.PGWS_DB_URI [
19 (lib.filterAttrs (_: v: v != null))
20 (lib.mapAttrsToList (k: v: "${k}='${lib.escape [ "'" "\\" ] v}'"))
21 (lib.concatStringsSep " ")
22 ];
23in
24
25{
26 meta = {
27 maintainers = with lib.maintainers; [ wolfgangwalther ];
28 };
29
30 options.services.postgres-websockets = {
31 enable = lib.mkEnableOption "postgres-websockets";
32
33 pgpassFile = lib.mkOption {
34 type =
35 with lib.types;
36 nullOr (pathWith {
37 inStore = false;
38 absolute = true;
39 });
40 default = null;
41 example = "/run/keys/db_password";
42 description = ''
43 The password to authenticate to PostgreSQL with.
44 Not needed for peer or trust based authentication.
45
46 The file must be a valid `.pgpass` file as described in:
47 <https://www.postgresql.org/docs/current/libpq-pgpass.html>
48
49 In most cases, the following will be enough:
50 ```
51 *:*:*:*:<password>
52 ```
53 '';
54 };
55
56 jwtSecretFile = lib.mkOption {
57 type =
58 with lib.types;
59 nullOr (pathWith {
60 inStore = false;
61 absolute = true;
62 });
63 example = "/run/keys/jwt_secret";
64 description = ''
65 Secret used to sign JWT tokens used to open communications channels.
66 '';
67 };
68
69 environment = lib.mkOption {
70 type = lib.types.submodule {
71 freeformType = with lib.types; attrsOf str;
72
73 options = {
74 PGWS_DB_URI = lib.mkOption {
75 type = lib.types.submodule {
76 freeformType = with lib.types; attrsOf str;
77
78 # This should not be used; use pgpassFile instead.
79 options.password = lib.mkOption {
80 default = null;
81 readOnly = true;
82 internal = true;
83 };
84 # This should not be used; use pgpassFile instead.
85 options.passfile = lib.mkOption {
86 default = null;
87 readOnly = true;
88 internal = true;
89 };
90 };
91 default = { };
92 description = ''
93 libpq connection parameters as documented in:
94
95 <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>
96
97 ::: {.note}
98 The `environment.PGWS_DB_URI.password` and `environment.PGWS_DB_URI.passfile` options are blocked.
99 Use [`pgpassFile`](#opt-services.postgres-websockets.pgpassFile) instead.
100 :::
101 '';
102 example = lib.literalExpression ''
103 {
104 host = "localhost";
105 dbname = "postgres";
106 }
107 '';
108 };
109
110 # This should not be used; use jwtSecretFile instead.
111 PGWS_JWT_SECRET = lib.mkOption {
112 default = null;
113 readOnly = true;
114 internal = true;
115 };
116
117 PGWS_HOST = lib.mkOption {
118 type = with lib.types; nullOr str;
119 default = "127.0.0.1";
120 description = ''
121 Address the server will listen for websocket connections.
122 '';
123 };
124 };
125 };
126 default = { };
127 description = ''
128 postgres-websockets configuration as defined in:
129 <https://github.com/diogob/postgres-websockets/blob/master/src/PostgresWebsockets/Config.hs#L71-L87>
130
131 `PGWS_DB_URI` is represented as an attribute set, see [`environment.PGWS_DB_URI`](#opt-services.postgres-websockets.environment.PGWS_DB_URI)
132
133 ::: {.note}
134 The `environment.PGWS_JWT_SECRET` option is blocked.
135 Use [`jwtSecretFile`](#opt-services.postgres-websockets.jwtSecretFile) instead.
136 :::
137 '';
138 example = lib.literalExpression ''
139 {
140 PGWS_LISTEN_CHANNEL = "my_channel";
141 PGWS_DB_URI.dbname = "postgres";
142 }
143 '';
144 };
145 };
146
147 config = lib.mkIf cfg.enable {
148 services.postgres-websockets.environment.PGWS_DB_URI.application_name =
149 with pkgs.postgres-websockets;
150 "${pname} ${version}";
151
152 systemd.services.postgres-websockets = {
153 description = "postgres-websockets";
154
155 wantedBy = [ "multi-user.target" ];
156 wants = [ "network-online.target" ];
157 after = [
158 "network-online.target"
159 "postgresql.target"
160 ];
161
162 environment =
163 cfg.environment
164 // {
165 inherit PGWS_DB_URI;
166 PGWS_JWT_SECRET = "@%d/jwt_secret";
167 }
168 // lib.optionalAttrs (cfg.pgpassFile != null) {
169 PGPASSFILE = "%C/postgres-websockets/pgpass";
170 };
171
172 serviceConfig = {
173 CacheDirectory = "postgres-websockets";
174 CacheDirectoryMode = "0700";
175 LoadCredential = [
176 "jwt_secret:${cfg.jwtSecretFile}"
177 ]
178 ++ lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}";
179 Restart = "always";
180 User = "postgres-websockets";
181
182 # Hardening
183 CapabilityBoundingSet = [ "" ];
184 DevicePolicy = "closed";
185 DynamicUser = true;
186 LockPersonality = true;
187 MemoryDenyWriteExecute = true;
188 NoNewPrivileges = true;
189 PrivateDevices = true;
190 PrivateIPC = true;
191 PrivateMounts = true;
192 ProcSubset = "pid";
193 ProtectClock = true;
194 ProtectControlGroups = true;
195 ProtectHostname = true;
196 ProtectKernelLogs = true;
197 ProtectKernelModules = true;
198 ProtectKernelTunables = true;
199 ProtectProc = "invisible";
200 RestrictAddressFamilies = [
201 "AF_INET"
202 "AF_INET6"
203 "AF_UNIX"
204 ];
205 RestrictNamespaces = true;
206 RestrictRealtime = true;
207 SystemCallArchitectures = "native";
208 SystemCallFilter = [ "" ];
209 UMask = "0077";
210 };
211
212 # Copy the pgpass file to different location, to have it report mode 0400.
213 # Fixes: https://github.com/systemd/systemd/issues/29435
214 script = ''
215 if [ -f "$CREDENTIALS_DIRECTORY/pgpass" ]; then
216 cp -f "$CREDENTIALS_DIRECTORY/pgpass" "$CACHE_DIRECTORY/pgpass"
217 fi
218 exec ${lib.getExe pkgs.postgres-websockets}
219 '';
220 };
221 };
222}