nixos/postgrest: init module
This commit is contained in:
parent
71740ceb69
commit
064432a519
@ -126,6 +126,8 @@
|
||||
|
||||
- [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).
|
||||
|
@ -513,6 +513,7 @@
|
||||
./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
Normal file
311
nixos/modules/services/databases/postgrest.nix
Normal file
@ -0,0 +1,311 @@
|
||||
{
|
||||
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}
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
@ -972,6 +972,7 @@ in {
|
||||
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
Normal file
88
nixos/tests/postgrest.nix
Normal file
@ -0,0 +1,88 @@
|
||||
{ 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
|
||||
)
|
||||
'';
|
||||
}
|
@ -439,6 +439,7 @@ self: super: builtins.intersectAttrs super {
|
||||
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.
|
||||
|
Loading…
Reference in New Issue
Block a user