nixpkgs/nixos/modules/services/databases/postgrest.nix
2025-03-30 14:24:16 +02:00

311 lines
9.5 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.postgrest;
# Turns an attrset of libpq connection params:
# {
# dbname = "postgres";
# user = "authenticator";
# }
# into a libpq connection string:
# dbname=postgres user=authenticator
db-uri = lib.pipe (cfg.settings.db-uri or { }) [
(lib.filterAttrs (_: v: v != null))
(lib.mapAttrsToList (k: v: "${k}=${v}"))
(lib.concatStringsSep " ")
];
# Writes a postgrest config file according to:
# https://hackage.haskell.org/package/configurator-0.3.0.0/docs/Data-Configurator.html
# Only a subset of the functionality is used by PostgREST.
configFile = lib.pipe (cfg.settings // { inherit db-uri; }) [
(lib.filterAttrs (_: v: v != null))
(lib.mapAttrs (
_: v:
if true == v then
"true"
else if false == v then
"false"
else if lib.isInt v then
toString v
else
"\"${lib.escape [ "\"" ] v}\""
))
(lib.mapAttrsToList (k: v: "${k} = ${v}"))
(lib.concatStringsSep "\n")
(pkgs.writeText "postgrest.conf")
];
in
{
meta = {
maintainers = with lib.maintainers; [ wolfgangwalther ];
};
options.services.postgrest = {
enable = lib.mkEnableOption "PostgREST";
pgpassFile = lib.mkOption {
type =
with lib.types;
nullOr (pathWith {
inStore = false;
absolute = true;
});
default = null;
example = "/run/keys/db_password";
description = ''
The password to authenticate to PostgreSQL with.
Not needed for peer or trust based authentication.
The file must be a valid `.pgpass` file as described in:
<https://www.postgresql.org/docs/current/libpq-pgpass.html>
In most cases, the following will be enough:
```
*:*:*:*:<password>
```
'';
};
jwtSecretFile = lib.mkOption {
type =
with lib.types;
nullOr (pathWith {
inStore = false;
absolute = true;
});
default = null;
example = "/run/keys/jwt_secret";
description = ''
The secret or JSON Web Key (JWK) (or set) used to decode JWT tokens clients provide for authentication.
For security the key must be at least 32 characters long.
If this parameter is not specified then PostgREST refuses authentication requests.
<https://docs.postgrest.org/en/stable/references/configuration.html#jwt-secret>
'';
};
settings = lib.mkOption {
type = lib.types.submodule {
freeformType =
with lib.types;
attrsOf (oneOf [
bool
ints.unsigned
str
]);
options = {
admin-server-port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
description = ''
Specifies the port for the admin server, which can be used for healthchecks.
<https://docs.postgrest.org/en/stable/references/admin_server.html#admin-server>
'';
};
db-config = lib.mkOption {
type = lib.types.bool;
default = false;
example = true;
description = ''
Enables the in-database configuration.
<https://docs.postgrest.org/en/stable/references/configuration.html#in-database-configuration>
::: {.note}
This is enabled by default upstream, but disabled by default in this module.
:::
'';
};
db-uri = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf str;
# This should not be used; use pgpassFile instead.
options.password = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
# This should not be used; use pgpassFile instead.
options.passfile = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
};
default = { };
description = ''
libpq connection parameters as documented in:
<https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS>
::: {.note}
The `settings.db-uri.password` and `settings.db-uri.passfile` options are blocked.
Use [`pgpassFile`](#opt-services.postgrest.pgpassFile) instead.
:::
'';
example = lib.literalExpression ''
{
host = "localhost";
dbname = "postgres";
}
'';
};
# This should not be used; use jwtSecretFile instead.
jwt-secret = lib.mkOption {
default = null;
readOnly = true;
internal = true;
};
server-host = lib.mkOption {
type = with lib.types; nullOr str;
default = "127.0.0.1";
description = ''
Where to bind the PostgREST web server.
::: {.note}
The admin server will also bind here, but potentially exposes sensitive information.
Make sure you turn off the admin server, when opening this to the public.
<https://github.com/PostgREST/postgrest/issues/3956>
:::
'';
};
server-port = lib.mkOption {
type = with lib.types; nullOr port;
default = null;
example = 3000;
description = ''
The TCP port to bind the web server.
'';
};
server-unix-socket = lib.mkOption {
type = with lib.types; nullOr path;
default = "/run/postgrest/postgrest.sock";
description = ''
Unix domain socket where to bind the PostgREST web server.
'';
};
};
};
default = { };
description = ''
PostgREST configuration as documented in:
<https://docs.postgrest.org/en/stable/references/configuration.html#list-of-parameters>
`db-uri` is represented as an attribute set, see [`settings.db-uri`](#opt-services.postgrest.settings.db-uri)
::: {.note}
The `settings.jwt-secret` option is blocked.
Use [`jwtSecretFile`](#opt-services.postgrest.jwtSecretFile) instead.
:::
'';
example = lib.literalExpression ''
{
db-anon-role = "anon";
db-uri.dbname = "postgres";
"app.settings.custom" = "value";
}
'';
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = (cfg.settings.server-port == null) != (cfg.settings.server-unix-socket == null);
message = ''
PostgREST can listen either on a TCP port or on a unix socket, but not both.
Please set one of `settings.server-port`](#opt-services.postgrest.jwtSecretFile) or `settings.server-unix-socket` to `null`.
<https://docs.postgrest.org/en/stable/references/configuration.html#server-unix-socket>
'';
}
];
warnings =
lib.optional (cfg.settings.admin-server-port != null && cfg.settings.server-host != "127.0.0.1")
"The PostgREST admin server is potentially listening on a public host. This may expose sensitive information via the `/config` endpoint.";
systemd.services.postgrest = {
description = "PostgREST";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [
"network-online.target"
"postgresql.service"
];
serviceConfig = {
CacheDirectory = "postgrest";
CacheDirectoryMode = "0700";
Environment =
lib.optional (cfg.pgpassFile != null) "PGPASSFILE=%C/postgrest/pgpass"
++ lib.optional (cfg.jwtSecretFile != null) "PGRST_JWT_SECRET=@%d/jwt_secret";
LoadCredential =
lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}"
++ lib.optional (cfg.jwtSecretFile != null) "jwt_secret:${cfg.jwtSecretFile}";
Restart = "always";
RuntimeDirectory = "postgrest";
User = "postgrest";
# Hardening
CapabilityBoundingSet = [ "" ];
DevicePolicy = "closed";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateIPC = true;
PrivateMounts = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "" ];
UMask = "0077";
};
# Copy the pgpass file to different location, to have it report mode 0400.
# Fixes: https://github.com/systemd/systemd/issues/29435
script = ''
if [ -f "$CREDENTIALS_DIRECTORY/pgpass" ]; then
cp -f "$CREDENTIALS_DIRECTORY/pgpass" "$CACHE_DIRECTORY/pgpass"
fi
exec ${lib.getExe pkgs.postgrest} ${configFile}
'';
};
};
}