nixos/postgres-websockets: init

Changed files
+313 -1
nixos
doc
manual
release-notes
modules
tests
pkgs
development
haskell-modules
+2
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [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).
+
- [postgres-websockets](https://github.com/diogob/postgres-websockets), a middleware that adds websockets capabilites on top of PostgreSQL's asynchronous notifications using LISTEN and NOTIFY commands. Available as [services.postgres-websockets](options.html#opt-services.postgres-websockets.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/opentsdb.nix
./services/databases/pgbouncer.nix
./services/databases/pgmanage.nix
+
./services/databases/postgres-websockets.nix
./services/databases/postgresql.nix
./services/databases/postgrest.nix
./services/databases/redis.nix
+221
nixos/modules/services/databases/postgres-websockets.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
+
let
+
cfg = config.services.postgres-websockets;
+
+
# Turns an attrset of libpq connection params:
+
# {
+
# dbname = "postgres";
+
# user = "authenticator";
+
# }
+
# into a libpq connection string:
+
# dbname=postgres user=authenticator
+
PGWS_DB_URI = lib.pipe cfg.environment.PGWS_DB_URI [
+
(lib.filterAttrs (_: v: v != null))
+
(lib.mapAttrsToList (k: v: "${k}='${lib.escape [ "'" "\\" ] v}'"))
+
(lib.concatStringsSep " ")
+
];
+
in
+
+
{
+
meta = {
+
maintainers = with lib.maintainers; [ wolfgangwalther ];
+
};
+
+
options.services.postgres-websockets = {
+
enable = lib.mkEnableOption "postgres-websockets";
+
+
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;
+
});
+
example = "/run/keys/jwt_secret";
+
description = ''
+
Secret used to sign JWT tokens used to open communications channels.
+
'';
+
};
+
+
environment = lib.mkOption {
+
type = lib.types.submodule {
+
freeformType = with lib.types; attrsOf str;
+
+
options = {
+
PGWS_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 `environment.PGWS_DB_URI.password` and `environment.PGWS_DB_URI.passfile` options are blocked.
+
Use [`pgpassFile`](#opt-services.postgres-websockets.pgpassFile) instead.
+
:::
+
'';
+
example = lib.literalExpression ''
+
{
+
host = "localhost";
+
dbname = "postgres";
+
}
+
'';
+
};
+
+
# This should not be used; use jwtSecretFile instead.
+
PGWS_JWT_SECRET = lib.mkOption {
+
default = null;
+
readOnly = true;
+
internal = true;
+
};
+
+
PGWS_HOST = lib.mkOption {
+
type = with lib.types; nullOr str;
+
default = "127.0.0.1";
+
description = ''
+
Address the server will listen for websocket connections.
+
'';
+
};
+
};
+
};
+
default = { };
+
description = ''
+
postgres-websockets configuration as defined in:
+
<https://github.com/diogob/postgres-websockets/blob/master/src/PostgresWebsockets/Config.hs#L71-L87>
+
+
`PGWS_DB_URI` is represented as an attribute set, see [`environment.PGWS_DB_URI`](#opt-services.postgres-websockets.environment.PGWS_DB_URI)
+
+
::: {.note}
+
The `environment.PGWS_JWT_SECRET` option is blocked.
+
Use [`jwtSecretFile`](#opt-services.postgres-websockets.jwtSecretFile) instead.
+
:::
+
'';
+
example = lib.literalExpression ''
+
{
+
PGWS_LISTEN_CHANNEL = "my_channel";
+
PGWS_DB_URI.dbname = "postgres";
+
}
+
'';
+
};
+
};
+
+
config = lib.mkIf cfg.enable {
+
services.postgres-websockets.environment.PGWS_DB_URI.application_name =
+
with pkgs.postgres-websockets;
+
"${pname} ${version}";
+
+
systemd.services.postgres-websockets = {
+
description = "postgres-websockets";
+
+
wantedBy = [ "multi-user.target" ];
+
wants = [ "network-online.target" ];
+
after = [
+
"network-online.target"
+
"postgresql.service"
+
];
+
+
environment =
+
cfg.environment
+
// {
+
inherit PGWS_DB_URI;
+
PGWS_JWT_SECRET = "@%d/jwt_secret";
+
}
+
// lib.optionalAttrs (cfg.pgpassFile != null) {
+
PGPASSFILE = "%C/postgres-websockets/pgpass";
+
};
+
+
serviceConfig = {
+
CacheDirectory = "postgres-websockets";
+
CacheDirectoryMode = "0700";
+
LoadCredential = [
+
"jwt_secret:${cfg.jwtSecretFile}"
+
] ++ lib.optional (cfg.pgpassFile != null) "pgpass:${cfg.pgpassFile}";
+
Restart = "always";
+
User = "postgres-websockets";
+
+
# 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.postgres-websockets}
+
'';
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
handleTest ./postfix-raise-smtpd-tls-security-level.nix
{ };
postfixadmin = handleTest ./postfixadmin.nix { };
+
postgres-websockets = runTest ./postgres-websockets.nix;
postgresql = handleTest ./postgresql { };
postgrest = runTest ./postgrest.nix;
powerdns = handleTest ./powerdns.nix { };
+84
nixos/tests/postgres-websockets.nix
···
+
{ lib, ... }:
+
{
+
name = "postgres-websockets";
+
+
meta = {
+
maintainers = with lib.maintainers; [ wolfgangwalther ];
+
};
+
+
nodes.machine =
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
{
+
environment.systemPackages = [ pkgs.websocat ];
+
+
services.postgresql = {
+
enable = true;
+
initialScript = pkgs.writeText "init.sql" ''
+
CREATE ROLE "postgres-websockets" LOGIN NOINHERIT;
+
CREATE ROLE "postgres-websockets_with_password" LOGIN NOINHERIT PASSWORD 'password';
+
'';
+
};
+
+
services.postgres-websockets = {
+
enable = true;
+
jwtSecretFile = "/run/secrets/jwt.secret";
+
environment.PGWS_DB_URI.dbname = "postgres";
+
environment.PGWS_LISTEN_CHANNEL = "websockets-listener";
+
};
+
+
specialisation.withPassword.configuration = {
+
services.postgresql.enableTCPIP = true;
+
services.postgres-websockets = {
+
pgpassFile = "/run/secrets/.pgpass";
+
environment.PGWS_DB_URI.host = "localhost";
+
environment.PGWS_DB_URI.user = "postgres-websockets_with_password";
+
};
+
};
+
};
+
+
extraPythonPackages = p: [ p.pyjwt ];
+
+
testScript =
+
{ nodes, ... }:
+
let
+
withPassword = "${nodes.machine.system.build.toplevel}/specialisation/withPassword";
+
in
+
''
+
machine.execute("""
+
mkdir -p /run/secrets
+
echo reallyreallyreallyreallyverysafe > /run/secrets/jwt.secret
+
""")
+
+
import jwt
+
token = jwt.encode({ "mode": "rw" }, "reallyreallyreallyreallyverysafe")
+
+
def test():
+
machine.wait_for_unit("postgresql.service")
+
machine.wait_for_unit("postgres-websockets.service")
+
+
machine.succeed(f"echo 'hi there' | websocat --no-close 'ws://localhost:3000/test/{token}' > output &")
+
machine.sleep(1)
+
machine.succeed("grep 'hi there' output")
+
+
machine.succeed("""
+
sudo -u postgres psql -c "SELECT pg_notify('websockets-listener', json_build_object('channel', 'test', 'event', 'message', 'payload', 'Hello World')::text);" >/dev/null
+
""")
+
machine.sleep(1)
+
machine.succeed("grep 'Hello World' output")
+
+
with subtest("without password"):
+
test()
+
+
with subtest("with password"):
+
machine.execute("""
+
echo "*:*:*:*:password" > /run/secrets/.pgpass
+
""")
+
machine.succeed("${withPassword}/bin/switch-to-configuration test >&2")
+
test()
+
'';
+
}
+4 -1
pkgs/development/haskell-modules/configuration-nix.nix
···
hasql-transaction = dontCheck super.hasql-transaction;
# Avoid compiling twice by providing executable as a separate output (with small closure size),
-
postgres-websockets = enableSeparateBinOutput super.postgres-websockets;
+
postgres-websockets = lib.pipe super.postgres-websockets [
+
enableSeparateBinOutput
+
(overrideCabal { passthru.tests = pkgs.nixosTests.postgres-websockets; })
+
];
# Test suite requires a running postgresql server,
# avoid compiling twice by providing executable as a separate output (with small closure size),