nixos/postgrest: init module

Changed files
+404
nixos
doc
manual
release-notes
modules
services
databases
tests
pkgs
development
haskell-modules
+2
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [Autotier](https://github.com/45Drives/autotier), a passthrough FUSE filesystem. Available as [services.autotierfs](options.html#opt-services.autotierfs.enable).
+
- [PostgREST](https://postgrest.org), a standalone web server that turns your PostgreSQL database directly into a RESTful API. Available as [services.postgrest](options.html#opt-services.postgrest.enable).
+
- [µStreamer](https://github.com/pikvm/ustreamer), a lightweight MJPEG-HTTP streamer. Available as [services.ustreamer](options.html#opt-services.ustreamer).
- [Whoogle Search](https://github.com/benbusby/whoogle-search), a self-hosted, ad-free, privacy-respecting metasearch engine. Available as [services.whoogle-search](options.html#opt-services.whoogle-search.enable).
+1
nixos/modules/module-list.nix
···
./services/databases/pgbouncer.nix
./services/databases/pgmanage.nix
./services/databases/postgresql.nix
+
./services/databases/postgrest.nix
./services/databases/redis.nix
./services/databases/surrealdb.nix
./services/databases/tigerbeetle.nix
+311
nixos/modules/services/databases/postgrest.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}
+
'';
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
postfix-raise-smtpd-tls-security-level = handleTest ./postfix-raise-smtpd-tls-security-level.nix {};
postfixadmin = handleTest ./postfixadmin.nix {};
postgresql = handleTest ./postgresql {};
+
postgrest = runTest ./postgrest.nix;
powerdns = handleTest ./powerdns.nix {};
powerdns-admin = handleTest ./powerdns-admin.nix {};
power-profiles-daemon = handleTest ./power-profiles-daemon.nix {};
+88
nixos/tests/postgrest.nix
···
+
{ lib, ... }:
+
{
+
name = "postgrest";
+
+
meta = {
+
maintainers = with lib.maintainers; [ wolfgangwalther ];
+
};
+
+
nodes.machine =
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
{
+
services.postgresql = {
+
enable = true;
+
initialScript = pkgs.writeText "init.sql" ''
+
CREATE ROLE postgrest LOGIN NOINHERIT;
+
CREATE ROLE anon ROLE postgrest;
+
+
CREATE ROLE postgrest_with_password LOGIN NOINHERIT PASSWORD 'password';
+
CREATE ROLE authenticated ROLE postgrest_with_password;
+
'';
+
};
+
+
services.postgrest = {
+
enable = true;
+
settings = {
+
admin-server-port = 3001;
+
db-anon-role = "anon";
+
db-uri.dbname = "postgres";
+
};
+
};
+
+
specialisation.withSecrets.configuration = {
+
services.postgresql.enableTCPIP = true;
+
services.postgrest = {
+
pgpassFile = "/run/secrets/.pgpass";
+
jwtSecretFile = "/run/secrets/jwt.secret";
+
settings.db-uri.host = "localhost";
+
settings.db-uri.user = "postgrest_with_password";
+
settings.server-port = 3000;
+
settings.server-unix-socket = null;
+
};
+
};
+
};
+
+
extraPythonPackages = p: [ p.pyjwt ];
+
+
testScript =
+
{ nodes, ... }:
+
let
+
withSecrets = "${nodes.machine.system.build.toplevel}/specialisation/withSecrets";
+
in
+
''
+
import jwt
+
+
machine.wait_for_unit("postgresql.service")
+
+
def wait_for_postgrest():
+
machine.wait_for_unit("postgrest.service")
+
machine.wait_until_succeeds("curl --fail -s http://localhost:3001/ready", timeout=30)
+
+
with subtest("anonymous access"):
+
wait_for_postgrest()
+
machine.succeed(
+
"curl --fail-with-body --no-progress-meter --unix-socket /run/postgrest/postgrest.sock http://localhost",
+
timeout=2
+
)
+
+
machine.execute("""
+
mkdir -p /run/secrets
+
echo "*:*:*:*:password" > /run/secrets/.pgpass
+
echo reallyreallyreallyreallyverysafe > /run/secrets/jwt.secret
+
""")
+
+
with subtest("authenticated access"):
+
machine.succeed("${withSecrets}/bin/switch-to-configuration test >&2")
+
wait_for_postgrest()
+
token = jwt.encode({ "role": "authenticated" }, "reallyreallyreallyreallyverysafe")
+
machine.succeed(
+
f"curl --fail-with-body --no-progress-meter -H 'Authorization: Bearer {token}' http://localhost:3000",
+
timeout=2
+
)
+
'';
+
}
+1
pkgs/development/haskell-modules/configuration-nix.nix
···
dontCheck
enableSeparateBinOutput
(self.generateOptparseApplicativeCompletions [ "postgrest" ])
+
(overrideCabal { passthru.tests = pkgs.nixosTests.postgrest; })
];
# Tries to mess with extended POSIX attributes, but can't in our chroot environment.