1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.postgrest;
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 db-uri = lib.pipe (cfg.settings.db-uri or { }) [
19 (lib.filterAttrs (_: v: v != null))
20 (lib.mapAttrsToList (k: v: "${k}=${v}"))
21 (lib.concatStringsSep " ")
22 ];
23
24 # Writes a postgrest config file according to:
25 # https://hackage.haskell.org/package/configurator-0.3.0.0/docs/Data-Configurator.html
26 # Only a subset of the functionality is used by PostgREST.
27 configFile = lib.pipe (cfg.settings // { inherit db-uri; }) [
28 (lib.filterAttrs (_: v: v != null))
29
30 (lib.mapAttrs (
31 _: v:
32 if true == v then
33 "true"
34 else if false == v then
35 "false"
36 else if lib.isInt v then
37 toString v
38 else
39 "\"${lib.escape [ "\"" ] v}\""
40 ))
41
42 (lib.mapAttrsToList (k: v: "${k} = ${v}"))
43 (lib.concatStringsSep "\n")
44 (pkgs.writeText "postgrest.conf")
45 ];
46in
47
48{
49 meta = {
50 maintainers = with lib.maintainers; [ wolfgangwalther ];
51 };
52
53 options.services.postgrest = {
54 enable = lib.mkEnableOption "PostgREST";
55
56 pgpassFile = lib.mkOption {
57 type =
58 with lib.types;
59 nullOr (pathWith {
60 inStore = false;
61 absolute = true;
62 });
63 default = null;
64 example = "/run/keys/db_password";
65 description = ''
66 The password to authenticate to PostgreSQL with.
67 Not needed for peer or trust based authentication.
68
69 The file must be a valid `.pgpass` file as described in:
70 <https://www.postgresql.org/docs/current/libpq-pgpass.html>
71
72 In most cases, the following will be enough:
73 ```
74 *:*:*:*:<password>
75 ```
76 '';
77 };
78
79 jwtSecretFile = lib.mkOption {
80 type =
81 with lib.types;
82 nullOr (pathWith {
83 inStore = false;
84 absolute = true;
85 });
86 default = null;
87 example = "/run/keys/jwt_secret";
88 description = ''
89 The secret or JSON Web Key (JWK) (or set) used to decode JWT tokens clients provide for authentication.
90 For security the key must be at least 32 characters long.
91 If this parameter is not specified then PostgREST refuses authentication requests.
92
93 <https://docs.postgrest.org/en/stable/references/configuration.html#jwt-secret>
94 '';
95 };
96
97 settings = lib.mkOption {
98 type = lib.types.submodule {
99 freeformType =
100 with lib.types;
101 attrsOf (oneOf [
102 bool
103 ints.unsigned
104 str
105 ]);
106
107 options = {
108 admin-server-port = lib.mkOption {
109 type = with lib.types; nullOr port;
110 default = null;
111 description = ''
112 Specifies the port for the admin server, which can be used for healthchecks.
113
114 <https://docs.postgrest.org/en/stable/references/admin_server.html#admin-server>
115 '';
116 };
117
118 db-config = lib.mkOption {
119 type = lib.types.bool;
120 default = false;
121 example = true;
122 description = ''
123 Enables the in-database configuration.
124
125 <https://docs.postgrest.org/en/stable/references/configuration.html#in-database-configuration>
126
127 ::: {.note}
128 This is enabled by default upstream, but disabled by default in this module.
129 :::
130 '';
131 };
132
133 db-uri = lib.mkOption {
134 type = lib.types.submodule {
135 freeformType = with lib.types; attrsOf str;
136
137 # This should not be used; use pgpassFile instead.
138 options.password = lib.mkOption {
139 default = null;
140 readOnly = true;
141 internal = true;
142 };
143 # This should not be used; use pgpassFile instead.
144 options.passfile = lib.mkOption {
145 default = null;
146 readOnly = true;
147 internal = true;
148 };
149 };
150 default = { };
151 description = ''
152 libpq connection parameters as documented in:
153
154 <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>
155
156 ::: {.note}
157 The `settings.db-uri.password` and `settings.db-uri.passfile` options are blocked.
158 Use [`pgpassFile`](#opt-services.postgrest.pgpassFile) instead.
159 :::
160 '';
161 example = lib.literalExpression ''
162 {
163 host = "localhost";
164 dbname = "postgres";
165 }
166 '';
167 };
168
169 # This should not be used; use jwtSecretFile instead.
170 jwt-secret = lib.mkOption {
171 default = null;
172 readOnly = true;
173 internal = true;
174 };
175
176 server-host = lib.mkOption {
177 type = with lib.types; nullOr str;
178 default = "127.0.0.1";
179 description = ''
180 Where to bind the PostgREST web server.
181
182 ::: {.note}
183 The admin server will also bind here, but potentially exposes sensitive information.
184 Make sure you turn off the admin server, when opening this to the public.
185
186 <https://github.com/PostgREST/postgrest/issues/3956>
187 :::
188 '';
189 };
190
191 server-port = lib.mkOption {
192 type = with lib.types; nullOr port;
193 default = null;
194 example = 3000;
195 description = ''
196 The TCP port to bind the web server.
197 '';
198 };
199
200 server-unix-socket = lib.mkOption {
201 type = with lib.types; nullOr path;
202 default = "/run/postgrest/postgrest.sock";
203 description = ''
204 Unix domain socket where to bind the PostgREST web server.
205 '';
206 };
207 };
208 };
209 default = { };
210 description = ''
211 PostgREST configuration as documented in:
212 <https://docs.postgrest.org/en/stable/references/configuration.html#list-of-parameters>
213
214 `db-uri` is represented as an attribute set, see [`settings.db-uri`](#opt-services.postgrest.settings.db-uri)
215
216 ::: {.note}
217 The `settings.jwt-secret` option is blocked.
218 Use [`jwtSecretFile`](#opt-services.postgrest.jwtSecretFile) instead.
219 :::
220 '';
221 example = lib.literalExpression ''
222 {
223 db-anon-role = "anon";
224 db-uri.dbname = "postgres";
225 "app.settings.custom" = "value";
226 }
227 '';
228 };
229 };
230
231 config = lib.mkIf cfg.enable {
232 assertions = [
233 {
234 assertion = (cfg.settings.server-port == null) != (cfg.settings.server-unix-socket == null);
235 message = ''
236 PostgREST can listen either on a TCP port or on a unix socket, but not both.
237 Please set one of `settings.server-port`](#opt-services.postgrest.jwtSecretFile) or `settings.server-unix-socket` to `null`.
238
239 <https://docs.postgrest.org/en/stable/references/configuration.html#server-unix-socket>
240 '';
241 }
242 ];
243
244 warnings =
245 lib.optional (cfg.settings.admin-server-port != null && cfg.settings.server-host != "127.0.0.1")
246 "The PostgREST admin server is potentially listening on a public host. This may expose sensitive information via the `/config` endpoint.";
247
248 # Since we're using DynamicUser, we can't add the e.g. nginx user to
249 # a postgrest group, so the unix socket must be world-readable to make it useful.
250 services.postgrest.settings.service-unix-socket-mode = "666";
251
252 systemd.services.postgrest = {
253 description = "PostgREST";
254
255 wantedBy = [ "multi-user.target" ];
256 wants = [ "network-online.target" ];
257 after = [
258 "network-online.target"
259 "postgresql.service"
260 ];
261
262 serviceConfig = {
263 CacheDirectory = "postgrest";
264 CacheDirectoryMode = "0700";
265 Environment =
266 lib.optional (cfg.pgpassFile != null) "PGPASSFILE=%C/postgrest/pgpass"
267 ++ lib.optional (cfg.jwtSecretFile != null) "PGRST_JWT_SECRET=@%d/jwt_secret";
268 LoadCredential =
269 lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}"
270 ++ lib.optional (cfg.jwtSecretFile != null) "jwt_secret:${cfg.jwtSecretFile}";
271 Restart = "always";
272 RuntimeDirectory = "postgrest";
273 User = "postgrest";
274
275 # Hardening
276 CapabilityBoundingSet = [ "" ];
277 DevicePolicy = "closed";
278 DynamicUser = true;
279 LockPersonality = true;
280 MemoryDenyWriteExecute = true;
281 NoNewPrivileges = true;
282 PrivateDevices = true;
283 PrivateIPC = true;
284 PrivateMounts = true;
285 ProcSubset = "pid";
286 ProtectClock = true;
287 ProtectControlGroups = true;
288 ProtectHostname = true;
289 ProtectKernelLogs = true;
290 ProtectKernelModules = true;
291 ProtectKernelTunables = true;
292 ProtectProc = "invisible";
293 RestrictAddressFamilies = [
294 "AF_INET"
295 "AF_INET6"
296 "AF_UNIX"
297 ];
298 RestrictNamespaces = true;
299 RestrictRealtime = true;
300 SystemCallArchitectures = "native";
301 SystemCallFilter = [ "" ];
302 UMask = "0077";
303 };
304
305 # Copy the pgpass file to different location, to have it report mode 0400.
306 # Fixes: https://github.com/systemd/systemd/issues/29435
307 script = ''
308 if [ -f "$CREDENTIALS_DIRECTORY/pgpass" ]; then
309 cp -f "$CREDENTIALS_DIRECTORY/pgpass" "$CACHE_DIRECTORY/pgpass"
310 fi
311 exec ${lib.getExe pkgs.postgrest} ${configFile}
312 '';
313 };
314 };
315}