From dbc745ebd2e99e991d3f020cb6d8442edbeb3433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Fri, 12 Sep 2025 13:22:37 +0200 Subject: [PATCH 01/11] feat: deploy postgresql using system-manager --- ansible/tests/conftest.py | 17 +- ansible/tests/test_nix.py | 1 + flake.lock | 7 +- flake.nix | 3 +- nix/checks.nix | 18 +- nix/config.nix | 72 ++++- nix/packages/default.nix | 7 +- nix/packages/pgsodium_getkey_readonly.nix | 20 ++ nix/systemConfigs.nix | 5 + nix/systemModules/default.nix | 4 +- nix/systemModules/postgres/default.nix | 339 ++++++++++++++++++++++ nix/systemModules/tests/test_postgres.py | 180 ++++++++++++ 12 files changed, 647 insertions(+), 26 deletions(-) create mode 100644 nix/packages/pgsodium_getkey_readonly.nix create mode 100644 nix/systemModules/postgres/default.nix create mode 100644 nix/systemModules/tests/test_postgres.py diff --git a/ansible/tests/conftest.py b/ansible/tests/conftest.py index 7e97be0cc..36df4dad9 100644 --- a/ansible/tests/conftest.py +++ b/ansible/tests/conftest.py @@ -59,13 +59,15 @@ def _run_playbook(playbook_name, verbose=False): ] if verbose: cmd.append("-vvv") - cmd.extend([ - "-i", - "localhost,", - "--extra-vars", - "@/flake/ansible/vars.yml", - f"/flake/ansible/tests/{playbook_name}", - ]) + cmd.extend( + [ + "-i", + "localhost,", + "--extra-vars", + "@/flake/ansible/vars.yml", + f"/flake/ansible/tests/{playbook_name}", + ] + ) result = host.run(" ".join(cmd)) if result.failed: console.log(result.stdout) @@ -73,4 +75,5 @@ def _run_playbook(playbook_name, verbose=False): raise pytest.fail( f"Ansible playbook {playbook_name} failed with return code {result.rc}" ) + return _run_playbook diff --git a/ansible/tests/test_nix.py b/ansible/tests/test_nix.py index fe900664d..ad85410b1 100644 --- a/ansible/tests/test_nix.py +++ b/ansible/tests/test_nix.py @@ -9,5 +9,6 @@ def run_ansible(run_ansible_playbook): def test_nix_service(host): assert host.service("nix-daemon.service").is_running + def test_envoy_service(host): assert host.service("envoy.service").is_running diff --git a/flake.lock b/flake.lock index ae6563b60..9b67032d5 100644 --- a/flake.lock +++ b/flake.lock @@ -248,15 +248,16 @@ ] }, "locked": { - "lastModified": 1754135474, - "narHash": "sha256-tZ8SXR80gcy8lqa1DD6/CNH9oQ1kOFw/cJihcn5/1M0=", + "lastModified": 1757363884, + "narHash": "sha256-lGG/LBllfniQvA6RrW2IRspX7p6lxM0NS8gKdQj7ilU=", "owner": "numtide", "repo": "system-manager", - "rev": "7865b4a207e46afa5c2e264de550730f8e281176", + "rev": "f35f51dc902264c2ccca010c313099bc53838601", "type": "github" }, "original": { "owner": "numtide", + "ref": "users", "repo": "system-manager", "type": "github" } diff --git a/flake.nix b/flake.nix index 813ffc69f..ecea0a91d 100644 --- a/flake.nix +++ b/flake.nix @@ -33,7 +33,8 @@ inputs.nixpkgs.follows = "nixpkgs"; }; system-manager = { - url = "github:numtide/system-manager"; + url = "github:numtide/system-manager/users"; + #url = "git+file:///home/jfroche/projects/numtide/system-manager/fix/return-tmpfile-error"; inputs.nixpkgs.follows = "nixpkgs"; }; }; diff --git a/nix/checks.nix b/nix/checks.nix index e2cf96c3e..1217a0312 100644 --- a/nix/checks.nix +++ b/nix/checks.nix @@ -13,7 +13,7 @@ psql_17 = self'.packages."psql_17/bin"; psql_orioledb-17 = self'.packages."psql_orioledb-17/bin"; pgroonga = self'.packages."psql_15/exts/pgroonga"; - inherit (self.supabase) defaults; + inherit (self.supabase.postgres) defaults; }; in { @@ -206,7 +206,7 @@ exit 1 } for i in {1..60}; do - if pg_isready -h ${self.supabase.defaults.host} -p ${pgPort}; then + if pg_isready -h ${self.supabase.postgres.defaults.host} -p ${pgPort}; then echo "PostgreSQL is ready" break fi @@ -220,8 +220,8 @@ exit 1 fi done - createdb -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin testing - if ! psql -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin -d testing -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then + createdb -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --username=supabase_admin testing + if ! psql -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --username=supabase_admin -d testing -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then echo "Error executing SQL file. PostgreSQL log content:" cat "$PGTAP_CLUSTER"/postgresql.log pg_ctl -D "$PGTAP_CLUSTER" stop @@ -229,7 +229,7 @@ fi SORTED_DIR=$(mktemp -d) for t in $(printf "%s\n" ${builtins.concatStringsSep " " sortedTestList}); do - psql -p ${pgPort} -h ${self.supabase.defaults.host} --username=supabase_admin -d testing -f "${./tests/sql}/$t.sql" || true + psql -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --username=supabase_admin -d testing -f "${./tests/sql}/$t.sql" || true done rm -rf "$SORTED_DIR" pg_ctl -D "$PGTAP_CLUSTER" stop @@ -244,7 +244,7 @@ ${start-postgres-server-bin}/bin/start-postgres-server ${getVersionArg pgpkg} --daemonize for i in {1..60}; do - if pg_isready -h ${self.supabase.defaults.host} -p ${pgPort} -U supabase_admin -q; then + if pg_isready -h ${self.supabase.postgres.defaults.host} -p ${pgPort} -U supabase_admin -q; then echo "PostgreSQL is ready" break fi @@ -255,7 +255,7 @@ fi done - if ! psql -p ${pgPort} -h ${self.supabase.defaults.host} --no-password --username=supabase_admin -d postgres -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then + if ! psql -p ${pgPort} -h ${self.supabase.postgres.defaults.host} --no-password --username=supabase_admin -d postgres -v ON_ERROR_STOP=1 -Xf ${./tests/prime.sql}; then echo "Error executing SQL file" exit 1 fi @@ -266,7 +266,7 @@ --dbname=postgres \ --inputdir=${./tests} \ --outputdir=$out/regression_output \ - --host=${self.supabase.defaults.host} \ + --host=${self.supabase.postgres.defaults.host} \ --port=${pgPort} \ --user=supabase_admin \ ${builtins.concatStringsSep " " sortedTestList}; then @@ -276,7 +276,7 @@ fi echo "Running migrations tests" - pg_prove -p ${pgPort} -U supabase_admin -h ${self.supabase.defaults.host} -d postgres -v ${../migrations/tests}/test.sql + pg_prove -p ${pgPort} -U supabase_admin -h ${self.supabase.postgres.defaults.host} -d postgres -v ${../migrations/tests}/test.sql # Copy logs to output for logfile in $(find /tmp -name postgresql.log -type f); do diff --git a/nix/config.nix b/nix/config.nix index 4076f0cea..aa2d89b2c 100644 --- a/nix/config.nix +++ b/nix/config.nix @@ -14,6 +14,74 @@ let type = lib.types.str; default = "supabase_admin"; }; + settings = lib.mkOption { + type = lib.types.attrs; + default = { + authentication_timeout = "1min"; + "auto_explain.log_min_duration" = "10s"; + checkpoint_completion_target = "0.5"; + checkpoint_flush_after = "256kB"; + cluster_name = "main"; + "cron.database_name" = "postgres"; + default_text_search_config = "pg_catalog.english"; + effective_cache_size = "128MB"; + extra_float_digits = "0"; + include = "/etc/postgresql-custom/read-replica.conf"; + jit = "off"; + jit_provider = "llvmjit"; + lc_messages = "en_US.UTF-8"; + lc_monetary = "en_US.UTF-8"; + lc_numeric = "en_US.UTF-8"; + lc_time = "en_US.UTF-8"; + listen_addresses = "*"; + log_destination = "stderr"; + log_line_prefix = "%h %m [%p] %q%u@%d "; + log_statement = "ddl"; + log_timezone = "UTC"; + max_replication_slots = "5"; + max_slot_wal_keep_size = "4096"; + max_wal_senders = "10"; + password_encryption = "scram-sha-256"; + port = 5432; + row_security = "on"; + shared_buffers = "128MB"; + ssl = "off"; + ssl_ca_file = ""; + ssl_cert_file = ""; + ssl_ciphers = "HIGH:MEDIUM:+3DES:!aNULL"; + ssl_crl_dir = ""; + ssl_crl_file = ""; + ssl_dh_params_file = ""; + ssl_ecdh_curve = "prime256v1"; + ssl_key_file = ""; + ssl_max_protocol_version = ""; + ssl_min_protocol_version = "TLSv1.2"; + ssl_passphrase_command = ""; + ssl_passphrase_command_supports_reload = "off"; + ssl_prefer_server_ciphers = "on"; + timezone = "UTC"; + wal_level = "logical"; + }; + }; + authentication = lib.mkOption { + type = lib.types.lines; + default = '' + # trust local connections + local all supabase_admin scram-sha-256 + local all all peer map=supabase_map + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + + # IPv4 external connections + host all all 10.0.0.0/8 scram-sha-256 + host all all 172.16.0.0/12 scram-sha-256 + host all all 192.168.0.0/16 scram-sha-256 + host all all 0.0.0.0/0 scram-sha-256 + + # IPv6 external connections + host all all ::0/0 scram-sha-256 + ''; + }; }; }; postgresqlVersion = lib.types.submodule { @@ -24,7 +92,7 @@ let }; supabaseSubmodule = lib.types.submodule { options = { - defaults = lib.mkOption { type = postgresqlDefaults; }; + postgres.defaults = lib.mkOption { type = postgresqlDefaults; }; supportedPostgresVersions = lib.mkOption { type = lib.types.attrsOf (lib.types.attrsOf postgresqlVersion); default = { }; @@ -38,7 +106,7 @@ in supabase = lib.mkOption { type = supabaseSubmodule; }; }; config.supabase = { - defaults = { }; + postgres.defaults = { }; supportedPostgresVersions = { postgres = { "15" = { diff --git a/nix/packages/default.nix b/nix/packages/default.nix index bdb5cd112..3acf98663 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -24,7 +24,7 @@ psql_17 = self'.packages."psql_17/bin"; psql_orioledb-17 = self'.packages."psql_orioledb-17/bin"; pgroonga = self'.packages."psql_15/exts/pgroonga"; - inherit (self.supabase) defaults; + inherit (self.supabase.postgres) defaults; }; in { @@ -35,7 +35,7 @@ inherit (self'.packages) docker-image-ubuntu; }; cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; - dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; + dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase.postgres) defaults; }; docker-image-ubuntu = pkgs.callPackage ./docker-ubuntu.nix { }; docs = pkgs.callPackage ./docs.nix { }; supabase-groonga = pkgs.callPackage ./groonga { }; @@ -46,13 +46,14 @@ pg-restore = pkgs.callPackage ./pg-restore.nix { psql_15 = self'.packages."psql_15/bin"; }; pg_prove = pkgs.perlPackages.TAPParserSourceHandlerpgTAP; pg_regress = makePgRegress activeVersion; + pgsodium_getkey_readonly = pkgs.callPackage ./pgsodium_getkey_readonly.nix { }; run-testinfra = pkgs.callPackage ./run-testinfra.nix { }; show-commands = pkgs.callPackage ./show-commands.nix { }; start-client = pkgs.callPackage ./start-client.nix { psql_15 = self'.packages."psql_15/bin"; psql_17 = self'.packages."psql_17/bin"; psql_orioledb-17 = self'.packages."psql_orioledb-17/bin"; - inherit (self.supabase) defaults; + inherit (self.supabase.postgres) defaults; }; start-replica = pkgs.callPackage ./start-replica.nix { psql_15 = self'.packages."psql_15/bin"; diff --git a/nix/packages/pgsodium_getkey_readonly.nix b/nix/packages/pgsodium_getkey_readonly.nix new file mode 100644 index 000000000..2efd855da --- /dev/null +++ b/nix/packages/pgsodium_getkey_readonly.nix @@ -0,0 +1,20 @@ +{ + coreutils, + writeShellApplication, +}: +writeShellApplication { + name = "pgsodium-getkey-readonly"; + runtimeInputs = [ coreutils ]; + text = '' + KEY_FILE=/etc/postgresql-custom/pgsodium_root.key + + # On the hosted platform, the root key is generated and managed for each project + # If for some reason the key is missing, we want to fail loudly, + # rather than generating a new one. + if [[ ! -f "''${KEY_FILE}" ]]; then + echo "Key file ''${KEY_FILE} does not exist." >&2 + exit 1 + fi + cat "$KEY_FILE" + ''; +} diff --git a/nix/systemConfigs.nix b/nix/systemConfigs.nix index 7f50ded93..ac83e9acb 100644 --- a/nix/systemConfigs.nix +++ b/nix/systemConfigs.nix @@ -1,9 +1,14 @@ { self, inputs, ... }: let mkModules = system: [ + self.systemModules.postgres ({ services.nginx.enable = true; nixpkgs.hostPlatform = system; + supabase.services.postgres = { + enable = true; + package = self.packages.${system}."psql_17/bin"; + }; }) ]; diff --git a/nix/systemModules/default.nix b/nix/systemModules/default.nix index 14b459255..4dd9eb6cd 100644 --- a/nix/systemModules/default.nix +++ b/nix/systemModules/default.nix @@ -4,6 +4,8 @@ { imports = [ ./tests ]; flake = { - systemModules = { }; + systemModules = { + postgres = ./postgres; + }; }; } diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix new file mode 100644 index 000000000..fa8acc184 --- /dev/null +++ b/nix/systemModules/postgres/default.nix @@ -0,0 +1,339 @@ +{ + lib, + config, + pkgs, + self, + ... +}: +let + cfg = config.supabase.services.postgres; + defaultUser = "postgres"; + defaultGroup = "postgres"; + + isOrioleDB = (builtins.match "[0-9][0-9]_.*" cfg.package.version) != null; + + toStr = + value: + if true == value then + "yes" + else if false == value then + "no" + else if builtins.isString value then + "'${lib.replaceStrings [ "'" ] [ "''" ] value}'" + else + builtins.toString value; + + # The main PostgreSQL configuration file. + configFile = pkgs.writeText "postgresql.conf" ( + lib.concatStringsSep "\n" ( + lib.mapAttrsToList (n: v: "${n} = ${toStr v}") ( + lib.filterAttrs (lib.const (x: x != null)) cfg.settings + ) + ) + ); + pg_hba = pkgs.writeText "pg_hba.conf" ( + cfg.authentication + self.supabase.postgres.defaults.authentication + ); + + read-replica-conf = pkgs.writeText "read-replica.conf" '' + # hot_standby = on + # restore_command = '/usr/bin/admin-mgr wal-fetch %f %p >> /var/log/wal-g/wal-fetch.log 2>&1' + # recovery_target_timeline = 'latest' + + # primary_conninfo = 'host=localhost port=6543 user=replication' + ''; + +in +{ + options = { + supabase.services.postgres = { + enable = lib.mkEnableOption "Postgres"; + package = lib.mkOption { + type = lib.types.package; + description = '' + The package being used by postgresql. + ''; + }; + settings = lib.mkOption { + type = + with lib.types; + submodule { + freeformType = attrsOf (oneOf [ + bool + float + int + str + ]); + options = { + shared_preload_libraries = lib.mkOption { + type = nullOr (coercedTo (listOf str) (lib.concatStringsSep ",") commas); + default = null; + example = literalExpression ''[ "auto_explain" "anon" ]''; + description = '' + List of libraries to be preloaded. + ''; + }; + }; + }; + default = { }; + description = '' + PostgreSQL configuration. Refer to + + for an overview of `postgresql.conf`. + + ::: {.note} + String values will automatically be enclosed in single quotes. Single quotes will be + escaped with two single quotes as described by the upstream documentation linked above. + ::: + ''; + }; + + authentication = lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + Defines how users authenticate themselves to the server. See the + [PostgreSQL documentation for pg_hba.conf](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) + for details on the expected format of this option. By default, + peer based authentication will be used for users connecting + via the Unix socket, and md5 password authentication will be + used for users connecting via TCP. Any added rules will be + inserted above the default rules. If you'd like to replace the + default rules entirely, you can use `lib.mkForce` in your + module. + ''; + }; + + environmentVariables = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { + LANG = "en_US.UTF-8"; + LANGUAGE = "en_US.UTF-8"; + LC_ALL = "en_US.UTF-8"; + LC_CTYPE = "en_US.UTF-8"; + }; + description = '' + A set of environment variables to be exported in the global + environment for all users. These will be set in `/etc/profile.d/postgresql.sh`. + ''; + }; + + dataDir = lib.mkOption { + type = lib.types.path; + default = "/data/pgdata"; + description = '' + The data directory for PostgreSQL. If left as the default value + this directory will automatically be created before the PostgreSQL server starts, otherwise + the sysadmin is responsible for ensuring the directory exists with appropriate ownership + and permissions. + ''; + }; + + superUser = lib.mkOption { + type = lib.types.str; + default = "postgres"; + internal = true; + readOnly = true; + description = '' + PostgreSQL superuser account to use for various operations. Internal since changing + this value would lead to breakage while setting up databases. + ''; + }; + + initdbArgs = lib.mkOption { + type = with lib.types; listOf str; + default = [ + "--allow-group-access" + "--username=${cfg.superUser}" + ] + ++ lib.optional isOrioleDB [ + "--locale-provider=icu" + "--encoding=UTF-8" + "--icu-locale=en_US.UTF-8" + ]; + description = '' + Additional arguments passed to `initdb` during data dir + initialisation. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + + services.userborn.enable = true; + + users.groups.postgres = { }; + users.users.postgres = { + isSystemUser = true; + uid = config.ids.uids.postgres; + group = "postgres"; + home = "${cfg.dataDir}"; + useDefaultShell = true; + }; + + systemd.tmpfiles.rules = [ + "d /home/postgres 0755 ${defaultUser} ${defaultGroup} -" + "d /var/log/postgresql 0755 ${defaultUser} ${defaultGroup} -" + "d /var/lib/postgresql 0755 ${defaultUser} ${defaultGroup} -" + "d /etc/postgresql 0775 ${defaultUser} ${defaultGroup} -" + "d /etc/postgresql-custom 0775 ${defaultUser} ${defaultGroup} -" + "d ${cfg.dataDir} 0750 ${defaultUser} ${defaultGroup} -" + "d /usr/lib/postgresql 0755 root root -" + + # Create symlinks + "L+ /var/lib/postgresql/data - - - - ${cfg.dataDir}" + "L+ /usr/lib/postgresql/bin - - - - ${cfg.package}/bin" + "L+ /usr/lib/postgresql/share - - - - ${cfg.package}/share" + "L+ /usr/lib/postgresql/lib - - - - ${cfg.package}/lib" + + # Copy configuration files + "C /etc/postgresql/pg_hba.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_hba}" + "C /etc/postgresql/postgresql.conf 0440 ${defaultUser} ${defaultGroup} - ${configFile}" + "C /etc/postgresql-custom/read-replica.conf 0440 ${defaultUser} ${defaultGroup} - ${read-replica-conf}" + ]; + + environment = { + systemPackages = [ cfg.package ]; + + etc = { + "profile.d/postgresql.sh".text = builtins.concatStringsSep "\n" ( + lib.mapAttrsToList (key: value: ''export ${key}="${value}"'') (cfg.environmentVariables) + ); + }; + }; + + supabase.services.postgres.settings = lib.mkMerge [ + { + hba_file = "/etc/postgresql/pg_hba.conf"; + log_destination = "stderr"; + "pgsodium.getkey_script" = lib.getExe self.packages.${pkgs.system}.pgsodium_getkey_readonly; + "vault.getkey_script" = lib.getExe self.packages.${pkgs.system}.pgsodium_getkey_readonly; + shared_preload_libraries = [ + "pg_stat_statements" + "pgaudit" + "plpgsql" + "plpgsql_check" + "pg_cron" + "pg_net" + "pgsodium" + "auto_explain" + "pg_tle" + "plan_filter" + "supabase_vault" + ]; + } + (lib.mkIf ((lib.toInt (lib.versions.major cfg.package.version)) < 16) { + db_user_namespace = "off"; + shared_preload_libraries = [ "timescaledb" ]; + }) + self.supabase.postgres.defaults.settings + ]; + + systemd.targets.postgresql = { + description = "PostgreSQL"; + wantedBy = [ "system-manager.target" ]; + requires = [ + "postgresql.service" + "postgresql-setup.service" + ]; + }; + + systemd.services.postgresql = { + description = "PostgreSQL Server"; + + after = [ "network.target" ]; + + # To trigger the .target also on "systemctl start postgresql" as well as on + # restarts & stops. + # Please note that postgresql.service & postgresql.target binding to + # each other makes the Restart=always rule racy and results + # in sometimes the service not being restarted. + wants = [ "postgresql.target" ]; + partOf = [ "postgresql.target" ]; + wantedBy = [ "system-manager.target" ]; + + environment.PGDATA = cfg.dataDir; + + path = [ cfg.package ]; + + preStart = '' + if ! test -e ${cfg.dataDir}/PG_VERSION; then + # Initialise the database. + initdb ${lib.escapeShellArgs cfg.initdbArgs} + fi + if [ ! -f /etc/postgresql-custom/pgsodium_root.key ]; then + umask 077 + echo "0000000000000000000000000000000000000000000000000000000000000000" > /etc/postgresql-custom/pgsodium_root.key + fi + ''; + + serviceConfig = { + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + User = "postgres"; + Group = "postgres"; + RuntimeDirectory = "postgresql"; + Type = "notify"; + + # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See + # https://www.postgresql.org/docs/current/server-shutdown.html + KillSignal = "SIGINT"; + KillMode = "mixed"; + + # Give Postgres a decent amount of time to clean up after + # receiving systemd's SIGINT. + TimeoutSec = 120; + + ExecStart = "${cfg.package}/bin/postgres -c config_file=/etc/postgresql/postgresql.conf"; + + Restart = "always"; + + # Hardening + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + MemoryDenyWriteExecute = true; # might be a problem for plv8 ? + NoNewPrivileges = true; + LockPersonality = true; + PrivateDevices = true; + PrivateMounts = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" # used for network interface enumeration + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + "@pkey" + ]; + UMask = "0077"; + ReadWritePaths = [ + cfg.dataDir + "/etc/postgresql-custom" + ]; + ReadOnlyPaths = [ + "/etc/postgresql" + ]; + }; + }; + + }; +} diff --git a/nix/systemModules/tests/test_postgres.py b/nix/systemModules/tests/test_postgres.py new file mode 100644 index 000000000..7b95f1ba4 --- /dev/null +++ b/nix/systemModules/tests/test_postgres.py @@ -0,0 +1,180 @@ +def test_postgres_directories(host): + """Test that all required PostgreSQL directories are created with correct permissions""" + directories = [ + ("/home/postgres", "755"), + ("/var/log/postgresql", "755"), + ("/var/lib/postgresql", "755"), + ("/etc/postgresql", "775"), + ("/etc/postgresql-custom", "775"), + ("/data/pgdata", "750"), + ("/usr/lib/postgresql", "755"), + ] + + for directory, expected_mode in directories: + dir_file = host.file(directory) + assert dir_file.is_directory, f"Directory {directory} should exist" + assert oct(dir_file.mode)[-3:] == expected_mode, ( + f"Directory {directory} should have mode {expected_mode}" + ) + + +def test_postgres_symlinks(host): + """Test that PostgreSQL symlinks are created correctly""" + symlinks = [ + "/var/lib/postgresql/data", + "/usr/lib/postgresql/bin", + ] + + for symlink in symlinks: + link_file = host.file(symlink) + assert link_file.is_symlink, f"File {symlink} should be a symlink" + + +def test_postgres_configuration_files(host): + """Test that PostgreSQL configuration files exist and have correct permissions""" + config_files = [ + ("/etc/postgresql/pg_hba.conf", "440"), + ("/etc/postgresql/postgresql.conf", "440"), + ] + + for config_file, expected_mode in config_files: + file_obj = host.file(config_file) + assert file_obj.is_file, f"Configuration file {config_file} should exist" + if expected_mode: + assert oct(file_obj.mode)[-3:] == expected_mode, ( + f"File {config_file} should have mode {expected_mode}" + ) + + +def test_postgres_package_installed(host): + """Test that PostgreSQL package is installed and available""" + # Check if postgres binaries are available + postgres_binaries = [ + "postgres", + "psql", + "createdb", + "dropdb", + "pg_dump", + "pg_restore", + ] + + for binary in postgres_binaries: + result = host.run(f"bash --login -c 'which {binary}'") + assert result.rc == 0, f"PostgreSQL binary {binary} should be available in PATH" + + +def test_postgres_environment_profile(host): + """Test that PostgreSQL environment variables are set in profile""" + profile_file = host.file("/etc/profile.d/postgresql.sh") + assert profile_file.is_file, "PostgreSQL profile script should exist" + + expected_vars = ["LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE"] + + for var in expected_vars: + result = host.run(f"bash --login -c 'echo ${var}'") + assert result.stdout.strip() != "", f"Environment variable {var} should be set" + + +def test_postgres_systemd_target(host): + """Test that PostgreSQL systemd target is configured""" + result = host.run("systemctl cat postgresql.target") + assert result.rc == 0, "PostgreSQL systemd target should exist" + + target_content = result.stdout + assert "Requires=postgresql.service postgresql-setup.service" in target_content + + system_manager_link = host.file( + "/etc/systemd/system/system-manager.target.wants/postgresql.target" + ) + assert system_manager_link.is_symlink, "System manager should have a symlink" + assert "postgresql.target" in system_manager_link.linked_to, ( + "System manager should have a symlink to postgresql.target" + ) + + +def test_postgres_data_directory_symlink(host): + """Test that the data directory symlink points to the correct location""" + data_link = host.file("/var/lib/postgresql/data") + assert data_link.is_symlink, "Data directory should be a symlink" + assert data_link.linked_to == "/data/pgdata", ( + "Data directory should link to /data/pgdata" + ) + + +def test_usr_lib_postgresql(host): + """Test that the bin directory symlink exists""" + for path in ["bin", "share", "lib"]: + dir_path = f"/usr/lib/postgresql/{path}" + dir_file = host.file(dir_path) + assert dir_file.is_symlink, f"Symlink {dir_path} should exist" + + +def test_postgres_configuration_content(host): + """Test that PostgreSQL configuration contains expected settings""" + config_file = host.file("/etc/postgresql/postgresql.conf") + assert config_file.is_file, "PostgreSQL configuration file should exist" + + content = config_file.content_string + assert "hba_file = '/etc/postgresql/pg_hba.conf'" in content + assert "log_destination = 'stderr'" in content + + +def test_postgres_hba_configuration(host): + """Test that pg_hba.conf file exists and is readable""" + hba_file = host.file("/etc/postgresql/pg_hba.conf") + assert hba_file.is_file, "pg_hba.conf file should exist" + assert hba_file.user == "postgres", "pg_hba.conf should be owned by root" + assert hba_file.group == "postgres", "pg_hba.conf should be owned by root group" + + +def test_postgres_directory_ownership(host): + """Test that PostgreSQL directories have correct ownership""" + directories = [ + "/home/postgres", + "/var/log/postgresql", + "/var/lib/postgresql", + "/etc/postgresql", + "/etc/postgresql-custom", + "/data/pgdata", + ] + + for directory in directories: + dir_file = host.file(directory) + assert dir_file.user == "postgres", ( + f"Directory {directory} should be owned by root" + ) + assert dir_file.group == "postgres", ( + f"Directory {directory} should be owned by root group" + ) + + +def test_postgres_configuration(host): + """Test that PostgreSQL configuration file exists and has correct permissions""" + config_file = host.file("/etc/postgresql/postgresql.conf") + assert config_file.is_file, "PostgreSQL configuration file should exist" + assert oct(config_file.mode)[-3:] == "440", ( + "PostgreSQL configuration file should have mode 440" + ) + assert config_file.user == "postgres", ( + "PostgreSQL configuration file should be owned by postgres" + ) + assert config_file.group == "postgres", ( + "PostgreSQL configuration file should be owned by postgres group" + ) + + # db_user_namespace doesn't exist in postgres >= 16 + assert not config_file.contains("db_user_namespace") + + +def test_postgres_service_running(host): + assert host.service("postgresql.service").is_valid + assert host.service("postgresql.service").is_running + + required_logs = [ + 'The default database encoding has accordingly been set to "UTF8"', + 'The database cluster will be initialized with locale "C.UTF-8"', + "pg_cron scheduler started", + ] + logs = host.run("journalctl -u postgresql.service --no-pager").stdout + for log in required_logs: + assert log in logs, f"Log '{log}' should be present in PostgreSQL logs" From 2652d54336518b59717526bd2172bb954dd3f175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 12:12:24 +0200 Subject: [PATCH 02/11] feat: configure pg_ident --- nix/config.nix | 1 + nix/systemModules/postgres/default.nix | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/nix/config.nix b/nix/config.nix index aa2d89b2c..407e51fe3 100644 --- a/nix/config.nix +++ b/nix/config.nix @@ -27,6 +27,7 @@ let effective_cache_size = "128MB"; extra_float_digits = "0"; include = "/etc/postgresql-custom/read-replica.conf"; + ident_file = "/etc/postgresql/pg_ident.conf"; jit = "off"; jit_provider = "llvmjit"; lc_messages = "en_US.UTF-8"; diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index fa8acc184..ff022f7cb 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -34,6 +34,17 @@ let pg_hba = pkgs.writeText "pg_hba.conf" ( cfg.authentication + self.supabase.postgres.defaults.authentication ); + pg_ident = pkgs.writeText "pg_ident.conf" '' + # MAPNAME SYSTEM-USERNAME PG-USERNAME + supabase_map postgres postgres + supabase_map root postgres + supabase_map ubuntu postgres + + # supabase-specific users + supabase_map gotrue supabase_auth_admin + supabase_map postgrest authenticator + supabase_map adminapi postgres + ''; read-replica-conf = pkgs.writeText "read-replica.conf" '' # hot_standby = on @@ -189,6 +200,7 @@ in # Copy configuration files "C /etc/postgresql/pg_hba.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_hba}" + "C /etc/postgresql/pg_ident.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_ident}" "C /etc/postgresql/postgresql.conf 0440 ${defaultUser} ${defaultGroup} - ${configFile}" "C /etc/postgresql-custom/read-replica.conf 0440 ${defaultUser} ${defaultGroup} - ${read-replica-conf}" ]; From 1d398bee124a07df5b49d288fac87536268981f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 12:12:24 +0200 Subject: [PATCH 03/11] feat: setup locales on the system and use en_US.UTF-8 --- nix/systemModules/postgres/default.nix | 217 +++++++++++++---------- nix/systemModules/tests/test_postgres.py | 15 +- 2 files changed, 136 insertions(+), 96 deletions(-) diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index ff022f7cb..52260a7d7 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -11,6 +11,7 @@ let defaultGroup = "postgres"; isOrioleDB = (builtins.match "[0-9][0-9]_.*" cfg.package.version) != null; + is17 = (builtins.substring 0 2 cfg.package.version) == "17"; toStr = value: @@ -122,6 +123,7 @@ in LANGUAGE = "en_US.UTF-8"; LC_ALL = "en_US.UTF-8"; LC_CTYPE = "en_US.UTF-8"; + LOCALE_ARCHIVE = "/usr/lib/locale/locale-archive"; }; description = '' A set of environment variables to be exported in the global @@ -157,11 +159,11 @@ in "--allow-group-access" "--username=${cfg.superUser}" ] - ++ lib.optional isOrioleDB [ + ++ (lib.optionals (isOrioleDB || is17) [ "--locale-provider=icu" "--encoding=UTF-8" "--icu-locale=en_US.UTF-8" - ]; + ]); description = '' Additional arguments passed to `initdb` during data dir initialisation. @@ -209,6 +211,10 @@ in systemPackages = [ cfg.package ]; etc = { + "locale.gen".text = '' + C.UTF-8 UTF-8 + en_US.UTF-8 UTF-8 + ''; "profile.d/postgresql.sh".text = builtins.concatStringsSep "\n" ( lib.mapAttrsToList (key: value: ''export ${key}="${value}"'') (cfg.environmentVariables) ); @@ -251,99 +257,122 @@ in ]; }; - systemd.services.postgresql = { - description = "PostgreSQL Server"; - - after = [ "network.target" ]; - - # To trigger the .target also on "systemctl start postgresql" as well as on - # restarts & stops. - # Please note that postgresql.service & postgresql.target binding to - # each other makes the Restart=always rule racy and results - # in sometimes the service not being restarted. - wants = [ "postgresql.target" ]; - partOf = [ "postgresql.target" ]; - wantedBy = [ "system-manager.target" ]; + systemd.services = { + postgresql = { + description = "PostgreSQL Server"; + + after = [ "network.target" ]; + + # To trigger the .target also on "systemctl start postgresql" as well as on + # restarts & stops. + # Please note that postgresql.service & postgresql.target binding to + # each other makes the Restart=always rule racy and results + # in sometimes the service not being restarted. + wants = [ "postgresql.target" ]; + partOf = [ "postgresql.target" ]; + wantedBy = [ "system-manager.target" ]; + + environment = { + PGDATA = cfg.dataDir; + } + // cfg.environmentVariables; + + path = [ cfg.package ]; + + preStart = '' + if ! test -e ${cfg.dataDir}/PG_VERSION; then + # Initialise the database. + initdb ${lib.escapeShellArgs cfg.initdbArgs} + fi + if [ ! -f /etc/postgresql-custom/pgsodium_root.key ]; then + umask 077 + echo "0000000000000000000000000000000000000000000000000000000000000000" > /etc/postgresql-custom/pgsodium_root.key + fi + + # TODO postgres_prestart.sh logic here + ''; - environment.PGDATA = cfg.dataDir; - - path = [ cfg.package ]; - - preStart = '' - if ! test -e ${cfg.dataDir}/PG_VERSION; then - # Initialise the database. - initdb ${lib.escapeShellArgs cfg.initdbArgs} - fi - if [ ! -f /etc/postgresql-custom/pgsodium_root.key ]; then - umask 077 - echo "0000000000000000000000000000000000000000000000000000000000000000" > /etc/postgresql-custom/pgsodium_root.key - fi - ''; - - serviceConfig = { - ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; - User = "postgres"; - Group = "postgres"; - RuntimeDirectory = "postgresql"; - Type = "notify"; - - # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See - # https://www.postgresql.org/docs/current/server-shutdown.html - KillSignal = "SIGINT"; - KillMode = "mixed"; - - # Give Postgres a decent amount of time to clean up after - # receiving systemd's SIGINT. - TimeoutSec = 120; - - ExecStart = "${cfg.package}/bin/postgres -c config_file=/etc/postgresql/postgresql.conf"; - - Restart = "always"; - - # Hardening - CapabilityBoundingSet = [ "" ]; - DevicePolicy = "closed"; - PrivateTmp = true; - ProtectHome = true; - ProtectSystem = "strict"; - MemoryDenyWriteExecute = true; # might be a problem for plv8 ? - NoNewPrivileges = true; - LockPersonality = true; - PrivateDevices = true; - PrivateMounts = true; - ProcSubset = "pid"; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - RemoveIPC = true; - RestrictAddressFamilies = [ - "AF_INET" - "AF_INET6" - "AF_NETLINK" # used for network interface enumeration - "AF_UNIX" - ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ - "@system-service" - "~@privileged" - "~@resources" - "@pkey" - ]; - UMask = "0077"; - ReadWritePaths = [ - cfg.dataDir - "/etc/postgresql-custom" - ]; - ReadOnlyPaths = [ - "/etc/postgresql" - ]; + serviceConfig = { + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + User = "postgres"; + Group = "postgres"; + RuntimeDirectory = "postgresql"; + Type = "notify"; + + # Shut down Postgres using SIGINT ("Fast Shutdown mode"). See + # https://www.postgresql.org/docs/current/server-shutdown.html + KillSignal = "SIGINT"; + KillMode = "mixed"; + + # Give Postgres a decent amount of time to clean up after + # receiving systemd's SIGINT. + TimeoutSec = 120; + + ExecStart = "${cfg.package}/bin/postgres -c config_file=/etc/postgresql/postgresql.conf"; + + Restart = "always"; + + # Hardening + CapabilityBoundingSet = [ "" ]; + DevicePolicy = "closed"; + PrivateTmp = true; + ProtectHome = true; + ProtectSystem = "strict"; + MemoryDenyWriteExecute = true; # might be a problem for plv8 ? + NoNewPrivileges = true; + LockPersonality = true; + PrivateDevices = true; + PrivateMounts = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" # used for network interface enumeration + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + "@pkey" + ]; + UMask = "0077"; + ReadWritePaths = [ + cfg.dataDir + "/etc/postgresql-custom" + ]; + ReadOnlyPaths = [ + "/etc/postgresql" + ]; + }; + }; + "setup-locales" = { + description = "Setup locales on the system"; + + before = [ "sysinit-reactivation.target" ]; + wantedBy = [ "sysinit-reactivation.target" ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = lib.getExe (pkgs.writeShellScriptBin "setup-locales" '' + PATH=/usr/sbin:/usr/bin + /usr/sbin/locale-gen + /usr/sbin/update-locale + ''); + }; }; }; diff --git a/nix/systemModules/tests/test_postgres.py b/nix/systemModules/tests/test_postgres.py index 7b95f1ba4..f5c33a4cb 100644 --- a/nix/systemModules/tests/test_postgres.py +++ b/nix/systemModules/tests/test_postgres.py @@ -165,14 +165,25 @@ def test_postgres_configuration(host): # db_user_namespace doesn't exist in postgres >= 16 assert not config_file.contains("db_user_namespace") +def test_locales(host): + installed_locales = host.run("localectl list-locales").stdout + assert "C.UTF-8" in installed_locales, "C.UTF-8 locale should be installed" + assert "en_US.UTF-8" in installed_locales, "en_US.UTF-8 locale should be installed" def test_postgres_service_running(host): assert host.service("postgresql.service").is_valid assert host.service("postgresql.service").is_running required_logs = [ - 'The default database encoding has accordingly been set to "UTF8"', - 'The database cluster will be initialized with locale "C.UTF-8"', + "Using language tag \"en-US\" for ICU locale \"en_US.UTF-8\"", + "locale provider: icu", + "default collation: en-US", + "LC_COLLATE: en_US.UTF-8", + "LC_CTYPE: en_US.UTF-8", + "LC_MESSAGES: en_US.UTF-8", + "LC_MONETARY: en_US.UTF-8", + "LC_NUMERIC: en_US.UTF-8", + "vault primary server secret key loaded", "pg_cron scheduler started", ] logs = host.run("journalctl -u postgresql.service --no-pager").stdout From 90ade3184de9713ab7f44c1fa1f5fb883cf455f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 12:12:24 +0200 Subject: [PATCH 04/11] chore: sort extensions --- nix/systemModules/postgres/default.nix | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index 52260a7d7..835612e00 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -228,16 +228,16 @@ in "pgsodium.getkey_script" = lib.getExe self.packages.${pkgs.system}.pgsodium_getkey_readonly; "vault.getkey_script" = lib.getExe self.packages.${pkgs.system}.pgsodium_getkey_readonly; shared_preload_libraries = [ - "pg_stat_statements" - "pgaudit" - "plpgsql" - "plpgsql_check" + "auto_explain" "pg_cron" "pg_net" - "pgsodium" - "auto_explain" + "pg_stat_statements" "pg_tle" + "pgaudit" + "pgsodium" "plan_filter" + "plpgsql" + "plpgsql_check" "supabase_vault" ]; } @@ -375,6 +375,5 @@ in }; }; }; - }; } From e8c2ac687438167315825a1b24f9ed67881a0bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 12:12:24 +0200 Subject: [PATCH 05/11] feat: add ssl-cert group --- nix/systemModules/postgres/default.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index 835612e00..8bd48e324 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -177,6 +177,7 @@ in services.userborn.enable = true; users.groups.postgres = { }; + users.groups.ssl-cert = { }; users.users.postgres = { isSystemUser = true; uid = config.ids.uids.postgres; From f7ff69bdf4c3adaaed5b0c5c604688c9114c3a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 12:12:24 +0200 Subject: [PATCH 06/11] feat: add write permissions to postgres for /etc/postgresql-custom/read-replica.conf --- nix/systemModules/postgres/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index 8bd48e324..02115e589 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -205,7 +205,7 @@ in "C /etc/postgresql/pg_hba.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_hba}" "C /etc/postgresql/pg_ident.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_ident}" "C /etc/postgresql/postgresql.conf 0440 ${defaultUser} ${defaultGroup} - ${configFile}" - "C /etc/postgresql-custom/read-replica.conf 0440 ${defaultUser} ${defaultGroup} - ${read-replica-conf}" + "C /etc/postgresql-custom/read-replica.conf 0664 ${defaultUser} ${defaultGroup} - ${read-replica-conf}" ]; environment = { From 6f01e90f2f8cef8dbbc92ec759c4c28469928737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 12:12:24 +0200 Subject: [PATCH 07/11] chore: remove migrated ansible tasks for postgres setup --- ansible/tasks/setup-postgres.yml | 295 ------------------------------- 1 file changed, 295 deletions(-) diff --git a/ansible/tasks/setup-postgres.yml b/ansible/tasks/setup-postgres.yml index 2fe302488..c801935f8 100644 --- a/ansible/tasks/setup-postgres.yml +++ b/ansible/tasks/setup-postgres.yml @@ -1,123 +1,3 @@ -- name: Postgres - copy package - copy: - src: files/postgres/ - dest: /tmp/build/ - when: debpkg_mode - -- name: Postgres - add PPA - apt_repository: - repo: "deb [ trusted=yes ] file:///tmp/build ./" - state: present - when: debpkg_mode - -- name: Postgres - install commons - apt: - name: postgresql-common - install_recommends: no - when: debpkg_mode - -- name: Do not create main cluster - shell: - cmd: sed -ri 's/#(create_main_cluster) .*$/\1 = false/' /etc/postgresql-common/createcluster.conf - when: debpkg_mode - -- name: Postgres - install server - apt: - name: postgresql-{{ postgresql_major }}={{ postgresql_release }}-1.pgdg24.04+1 - install_recommends: no - when: debpkg_mode - -- name: Postgres - remove PPA - apt_repository: - repo: "deb [ trusted=yes ] file:///tmp/build ./" - state: absent - when: debpkg_mode - -- name: Postgres - cleanup package - file: - path: /tmp/build - state: absent - when: debpkg_mode - -- name: install locales - apt: - name: locales - state: present - become: yes - when: stage2_nix - -- name: configure locales - command: echo "C.UTF-8 UTF-8" > /etc/locale.gen && echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen - become: yes - when: stage2_nix - -- name: locale-gen - command: sudo locale-gen - when: stage2_nix - -- name: update-locale - command: sudo update-locale - when: stage2_nix - -- name: Create symlink to /usr/lib/postgresql/bin - shell: - cmd: ln -s /usr/lib/postgresql/{{ postgresql_major }}/bin /usr/lib/postgresql/bin - when: debpkg_mode - -- name: create ssl-cert group - group: - name: ssl-cert - state: present - when: nixpkg_mode -# the old method of installing from debian creates this group, but we must create it explicitly -# for the nix built version - -- name: create postgres group - group: - name: postgres - state: present - when: nixpkg_mode - -- name: create postgres user - shell: adduser --system --home /var/lib/postgresql --no-create-home --shell /bin/bash --group --gecos "PostgreSQL administrator" postgres - args: - executable: /bin/bash - become: yes - when: nixpkg_mode - -- name: add postgres user to postgres group - shell: usermod -a -G ssl-cert postgres - args: - executable: /bin/bash - become: yes - when: nixpkg_mode - -- name: Create relevant directories - file: - path: '{{ item }}' - recurse: yes - state: directory - owner: postgres - group: postgres - with_items: - - '/home/postgres' - - '/var/log/postgresql' - - '/var/lib/postgresql' - when: debpkg_mode or nixpkg_mode - -- name: Allow adminapi to write custom config - file: - path: '{{ item }}' - recurse: yes - state: directory - owner: postgres - group: postgres - mode: 0775 - with_items: - - '/etc/postgresql' - - '/etc/postgresql-custom' - when: debpkg_mode or nixpkg_mode - - name: create placeholder config files file: path: '/etc/postgresql-custom/{{ item }}' @@ -130,41 +10,6 @@ - 'custom-overrides.conf' when: debpkg_mode or nixpkg_mode -# Move Postgres configuration files into /etc/postgresql -# Add postgresql.conf -- name: import postgresql.conf - template: - src: files/postgresql_config/postgresql.conf.j2 - dest: /etc/postgresql/postgresql.conf - group: postgres - when: debpkg_mode or nixpkg_mode - -# Add pg_hba.conf -- name: import pg_hba.conf - template: - src: files/postgresql_config/pg_hba.conf.j2 - dest: /etc/postgresql/pg_hba.conf - group: postgres - when: debpkg_mode or nixpkg_mode - -# Add pg_ident.conf -- name: import pg_ident.conf - template: - src: files/postgresql_config/pg_ident.conf.j2 - dest: /etc/postgresql/pg_ident.conf - group: postgres - when: debpkg_mode or nixpkg_mode - -# Add custom config for read replicas set up -- name: Move custom read-replica.conf file to /etc/postgresql-custom/read-replica.conf - template: - src: "files/postgresql_config/custom_read_replica.conf.j2" - dest: /etc/postgresql-custom/read-replica.conf - mode: 0664 - owner: postgres - group: postgres - when: debpkg_mode or nixpkg_mode - # Install extensions before init - name: Install Postgres extensions import_tasks: tasks/setup-docker.yml @@ -175,148 +20,8 @@ import_tasks: tasks/stage2-setup-postgres.yml when: stage2_nix -# init DB -- name: Create directory on data volume - file: - path: '{{ item }}' - recurse: yes - state: directory - owner: postgres - group: postgres - mode: 0750 - with_items: - - "/data/pgdata" - when: debpkg_mode or nixpkg_mode - -- name: Link database data_dir to data volume directory - file: - src: "/data/pgdata" - path: "/var/lib/postgresql/data" - state: link - force: yes - when: debpkg_mode or nixpkg_mode - -- name: Initialize the database - become: yes - become_user: postgres - shell: /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data initdb -o "--allow-group-access" -o "--username=supabase_admin" - vars: - ansible_command_timeout: 60 - when: debpkg_mode - -- name: Make sure .bashrc exists - file: - path: /var/lib/postgresql/.bashrc - state: touch - owner: postgres - group: postgres - when: nixpkg_mode - -- name: Check psql_version and modify supautils.conf and postgresql.conf if necessary - block: - - name: Check if psql_version is psql_orioledb - set_fact: - is_psql_oriole: "{{ psql_version in ['psql_orioledb-17'] }}" - is_psql_17: "{{ psql_version in ['psql_17'] }}" - - - name: Initialize the database stage2_nix (non-orioledb) - become: yes - become_user: postgres - shell: source /var/lib/postgresql/.bashrc && /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data initdb -o "--allow-group-access" -o "--username=supabase_admin" - args: - executable: /bin/bash - environment: - LANG: en_US.UTF-8 - LANGUAGE: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - LC_CTYPE: en_US.UTF-8 - LOCALE_ARCHIVE: /usr/lib/locale/locale-archive - vars: - ansible_command_timeout: 60 - when: stage2_nix and not is_psql_oriole and not is_psql_17 - - - name: Initialize the database stage2_nix (orioledb) - become: yes - become_user: postgres - shell: > - source /var/lib/postgresql/.bashrc && /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data initdb - -o "--allow-group-access" - -o "--username=supabase_admin" - -o "--locale-provider=icu" - -o "--encoding=UTF-8" - -o "--icu-locale=en_US.UTF-8" - args: - executable: /bin/bash - environment: - LANG: en_US.UTF-8 - LANGUAGE: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - LC_CTYPE: en_US.UTF-8 - LOCALE_ARCHIVE: /usr/lib/locale/locale-archive - vars: - ansible_command_timeout: 60 - when: stage2_nix and (is_psql_oriole or is_psql_17) - -- name: copy PG systemd unit - template: - src: files/postgresql_config/postgresql.service.j2 - dest: /etc/systemd/system/postgresql.service - when: debpkg_mode or stage2_nix - - name: copy optimizations systemd unit template: src: files/database-optimizations.service.j2 dest: /etc/systemd/system/database-optimizations.service when: debpkg_mode or stage2_nix - -- name: initialize pg required state - become: yes - shell: | - mkdir -p /run/postgresql - chown -R postgres:postgres /run/postgresql - when: stage2_nix and qemu_mode is defined - -- name: Restart Postgres Database without Systemd - become: yes - become_user: postgres - shell: | - source /var/lib/postgresql/.bashrc - /usr/lib/postgresql/bin/pg_ctl -D /var/lib/postgresql/data start - environment: - LANG: en_US.UTF-8 - LANGUAGE: en_US.UTF-8 - LC_ALL: en_US.UTF-8 - LC_CTYPE: en_US.UTF-8 - LOCALE_ARCHIVE: /usr/lib/locale/locale-archive - when: stage2_nix - - -# Reload -- name: System - systemd reload - systemd: - enabled: yes - name: postgresql - daemon_reload: yes - when: debpkg_mode or stage2_nix - - -- name: Add LOCALE_ARCHIVE to .bashrc - lineinfile: - dest: "/var/lib/postgresql/.bashrc" - line: 'export LOCALE_ARCHIVE=/usr/lib/locale/locale-archive' - create: yes - become: yes - when: nixpkg_mode - -- name: Add LANG items to .bashrc - lineinfile: - dest: "/var/lib/postgresql/.bashrc" - line: "{{ item }}" - loop: - - 'export LANG="en_US.UTF-8"' - - 'export LANGUAGE="en_US.UTF-8"' - - 'export LC_ALL="en_US.UTF-8"' - - 'export LANG="en_US.UTF-8"' - - 'export LC_CTYPE="en_US.UTF-8"' - become: yes - when: nixpkg_mode From ddd9802399b45c1efc9cb88fc3809aaf33e678bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 15 Sep 2025 14:05:18 +0200 Subject: [PATCH 08/11] feat: configure supautils extension --- ansible/tasks/setup-docker.yml | 80 ------------------- ansible/tasks/setup-postgres.yml | 5 -- nix/config.nix | 1 - nix/systemModules/postgres/default.nix | 35 ++++++-- .../before-create.sql | 0 .../dblink/after-create.sql | 0 .../pg_cron/after-create.sql | 0 .../pg_repack/after-create.sql | 0 .../pg_tle/after-create.sql | 0 .../pgmq/after-create.sql | 0 .../pgsodium/after-create.sql | 0 .../pgsodium/before-create.sql | 0 .../postgis_tiger_geocoder/after-create.sql | 0 .../postgres_fdw/after-create.sql | 0 .../supabase_vault/after-create.sql | 0 nix/systemModules/postgres/supautils.conf | 15 ++++ nix/systemModules/tests/test_postgres.py | 12 ++- 17 files changed, 52 insertions(+), 96 deletions(-) delete mode 100644 ansible/tasks/setup-docker.yml rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/before-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/dblink/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/pg_cron/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/pg_repack/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/pg_tle/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/pgmq/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/pgsodium/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/pgsodium/before-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/postgis_tiger_geocoder/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/postgres_fdw/after-create.sql (100%) rename {ansible/files/postgresql_extension_custom_scripts => nix/systemModules/postgres/extension-custom-scripts}/supabase_vault/after-create.sql (100%) create mode 100644 nix/systemModules/postgres/supautils.conf diff --git a/ansible/tasks/setup-docker.yml b/ansible/tasks/setup-docker.yml deleted file mode 100644 index 7b37f70cc..000000000 --- a/ansible/tasks/setup-docker.yml +++ /dev/null @@ -1,80 +0,0 @@ -- name: Copy extension packages - copy: - src: files/extensions/ - dest: /tmp/extensions/ - when: debpkg_mode - -# Builtin apt module does not support wildcard for deb paths -- name: Install extensions - shell: | - set -e - apt-get update - apt-get install -y --no-install-recommends /tmp/extensions/*.deb - when: debpkg_mode - -- name: pgsodium - determine postgres bin directory - shell: pg_config --bindir - register: pg_bindir_output - when: debpkg_mode - -- set_fact: - pg_bindir: "{{ pg_bindir_output.stdout }}" - when: debpkg_mode - -- name: pgsodium - set pgsodium.getkey_script - become: yes - lineinfile: - path: /etc/postgresql/postgresql.conf - state: present - # script is expected to be placed by finalization tasks for different target platforms - line: pgsodium.getkey_script= '{{ pg_bindir }}/pgsodium_getkey.sh' - when: debpkg_mode - -# supautils -- name: supautils - add supautils to session_preload_libraries - become: yes - replace: - path: /etc/postgresql/postgresql.conf - regexp: "#session_preload_libraries = ''" - replace: session_preload_libraries = 'supautils' - when: debpkg_mode or stage2_nix - -- name: supautils - write custom supautils.conf - template: - src: "files/postgresql_config/supautils.conf.j2" - dest: /etc/postgresql-custom/supautils.conf - mode: 0664 - owner: postgres - group: postgres - when: debpkg_mode or stage2_nix - -- name: supautils - copy extension custom scripts - copy: - src: files/postgresql_extension_custom_scripts/ - dest: /etc/postgresql-custom/extension-custom-scripts - become: yes - when: debpkg_mode or stage2_nix - -- name: supautils - chown extension custom scripts - file: - mode: 0775 - owner: postgres - group: postgres - path: /etc/postgresql-custom/extension-custom-scripts - recurse: yes - become: yes - when: debpkg_mode or stage2_nix - -- name: supautils - include /etc/postgresql-custom/supautils.conf in postgresql.conf - become: yes - replace: - path: /etc/postgresql/postgresql.conf - regexp: "#include = '/etc/postgresql-custom/supautils.conf'" - replace: "include = '/etc/postgresql-custom/supautils.conf'" - when: debpkg_mode or stage2_nix - -- name: Cleanup - extension packages - file: - path: /tmp/extensions - state: absent - when: debpkg_mode diff --git a/ansible/tasks/setup-postgres.yml b/ansible/tasks/setup-postgres.yml index c801935f8..1a9b1a3f8 100644 --- a/ansible/tasks/setup-postgres.yml +++ b/ansible/tasks/setup-postgres.yml @@ -10,11 +10,6 @@ - 'custom-overrides.conf' when: debpkg_mode or nixpkg_mode -# Install extensions before init -- name: Install Postgres extensions - import_tasks: tasks/setup-docker.yml - when: debpkg_mode or stage2_nix - #stage 2 postgres tasks - name: stage2 postgres tasks import_tasks: tasks/stage2-setup-postgres.yml diff --git a/nix/config.nix b/nix/config.nix index 407e51fe3..0ef36f52d 100644 --- a/nix/config.nix +++ b/nix/config.nix @@ -26,7 +26,6 @@ let default_text_search_config = "pg_catalog.english"; effective_cache_size = "128MB"; extra_float_digits = "0"; - include = "/etc/postgresql-custom/read-replica.conf"; ident_file = "/etc/postgresql/pg_ident.conf"; jit = "off"; jit_provider = "llvmjit"; diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index 02115e589..55eec3453 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -28,9 +28,11 @@ let configFile = pkgs.writeText "postgresql.conf" ( lib.concatStringsSep "\n" ( lib.mapAttrsToList (n: v: "${n} = ${toStr v}") ( - lib.filterAttrs (lib.const (x: x != null)) cfg.settings + lib.filterAttrs (n: v: n != "includes" && v != null) cfg.settings ) ) + + "\n" + + lib.concatStringsSep "\n" (lib.map (f: "include '${f}'") cfg.settings.includes) ); pg_hba = pkgs.writeText "pg_hba.conf" ( cfg.authentication + self.supabase.postgres.defaults.authentication @@ -55,6 +57,7 @@ let # primary_conninfo = 'host=localhost port=6543 user=replication' ''; + supautils-conf = ./supautils.conf; in { options = { @@ -85,6 +88,17 @@ in List of libraries to be preloaded. ''; }; + + includes = lib.mkOption { + type = listOf str; + default = [ + "/etc/postgresql-custom/read-replica.conf" + "/etc/postgresql-custom/supautils.conf" + ]; + description = '' + List of additional postgresql.conf files to be included. + ''; + }; }; }; default = { }; @@ -144,7 +158,7 @@ in superUser = lib.mkOption { type = lib.types.str; - default = "postgres"; + default = "supabase_admin"; internal = true; readOnly = true; description = '' @@ -183,7 +197,7 @@ in uid = config.ids.uids.postgres; group = "postgres"; home = "${cfg.dataDir}"; - useDefaultShell = true; + shell = "/usr/bin/bash"; }; systemd.tmpfiles.rules = [ @@ -200,12 +214,14 @@ in "L+ /usr/lib/postgresql/bin - - - - ${cfg.package}/bin" "L+ /usr/lib/postgresql/share - - - - ${cfg.package}/share" "L+ /usr/lib/postgresql/lib - - - - ${cfg.package}/lib" + "L+ /etc/postgresql-custom/extension-custom-scripts - - - - ${./extension-custom-scripts}" # Copy configuration files "C /etc/postgresql/pg_hba.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_hba}" "C /etc/postgresql/pg_ident.conf 0440 ${defaultUser} ${defaultGroup} - ${pg_ident}" "C /etc/postgresql/postgresql.conf 0440 ${defaultUser} ${defaultGroup} - ${configFile}" "C /etc/postgresql-custom/read-replica.conf 0664 ${defaultUser} ${defaultGroup} - ${read-replica-conf}" + "C /etc/postgresql-custom/supautils.conf 0664 ${defaultUser} ${defaultGroup} - ${supautils-conf}" ]; environment = { @@ -240,6 +256,7 @@ in "plpgsql" "plpgsql_check" "supabase_vault" + "supautils" ]; } (lib.mkIf ((lib.toInt (lib.versions.major cfg.package.version)) < 16) { @@ -368,11 +385,13 @@ in serviceConfig = { Type = "oneshot"; RemainAfterExit = true; - ExecStart = lib.getExe (pkgs.writeShellScriptBin "setup-locales" '' - PATH=/usr/sbin:/usr/bin - /usr/sbin/locale-gen - /usr/sbin/update-locale - ''); + ExecStart = lib.getExe ( + pkgs.writeShellScriptBin "setup-locales" '' + PATH=/usr/sbin:/usr/bin + /usr/sbin/locale-gen + /usr/sbin/update-locale + '' + ); }; }; }; diff --git a/ansible/files/postgresql_extension_custom_scripts/before-create.sql b/nix/systemModules/postgres/extension-custom-scripts/before-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/before-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/before-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/dblink/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/dblink/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/dblink/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/dblink/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pg_cron/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pg_cron/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pg_cron/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pg_cron/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pg_repack/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pg_repack/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pg_repack/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pg_repack/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pg_tle/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pg_tle/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pg_tle/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pg_tle/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pgmq/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pgmq/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pgmq/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pgsodium/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pgsodium/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pgsodium/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pgsodium/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/pgsodium/before-create.sql b/nix/systemModules/postgres/extension-custom-scripts/pgsodium/before-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/pgsodium/before-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/pgsodium/before-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/postgis_tiger_geocoder/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/postgis_tiger_geocoder/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/postgis_tiger_geocoder/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/postgis_tiger_geocoder/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/postgres_fdw/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/postgres_fdw/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/postgres_fdw/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/postgres_fdw/after-create.sql diff --git a/ansible/files/postgresql_extension_custom_scripts/supabase_vault/after-create.sql b/nix/systemModules/postgres/extension-custom-scripts/supabase_vault/after-create.sql similarity index 100% rename from ansible/files/postgresql_extension_custom_scripts/supabase_vault/after-create.sql rename to nix/systemModules/postgres/extension-custom-scripts/supabase_vault/after-create.sql diff --git a/nix/systemModules/postgres/supautils.conf b/nix/systemModules/postgres/supautils.conf new file mode 100644 index 000000000..a3456f699 --- /dev/null +++ b/nix/systemModules/postgres/supautils.conf @@ -0,0 +1,15 @@ +supautils.extensions_parameter_overrides = '{"pg_cron":{"schema":"pg_catalog"}}' +supautils.policy_grants = '{"postgres":["auth.audit_log_entries","auth.identities","auth.mfa_factors","auth.refresh_tokens","auth.sessions","auth.users","realtime.messages","storage.buckets","storage.migrations","storage.objects","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' +supautils.drop_trigger_grants = '{"postgres":["auth.audit_log_entries","auth.identities","auth.mfa_factors","auth.refresh_tokens","auth.sessions","auth.users","realtime.messages","storage.buckets","storage.migrations","storage.objects","storage.s3_multipart_uploads","storage.s3_multipart_uploads_parts"]}' +# full list: address_standardizer, address_standardizer_data_us, adminpack, amcheck, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, file_fdw, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intagg, intarray, isn, lo, ltree, moddatetime, old_snapshot, orioledb, pageinspect, pg_buffercache, pg_cron, pg_freespacemap, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_surgery, pg_tle, pg_trgm, pg_visibility, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgmq, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers, xml2 +# omitted because may be unsafe: adminpack, amcheck, file_fdw, lo, old_snapshot, pageinspect, pg_freespacemap, pg_surgery, pg_visibility +# omitted because deprecated: intagg, xml2 +# omitted because doesn't require superuser: pgmq +# omitted because protected: plpgsql +supautils.privileged_extensions = 'address_standardizer, address_standardizer_data_us, autoinc, bloom, btree_gin, btree_gist, citext, cube, dblink, dict_int, dict_xsyn, earthdistance, fuzzystrmatch, hstore, http, hypopg, index_advisor, insert_username, intarray, isn, ltree, moddatetime, orioledb, pg_buffercache, pg_cron, pg_graphql, pg_hashids, pg_jsonschema, pg_net, pg_prewarm, pg_repack, pg_stat_monitor, pg_stat_statements, pg_tle, pg_trgm, pg_walinspect, pgaudit, pgcrypto, pgjwt, pgroonga, pgroonga_database, pgrouting, pgrowlocks, pgsodium, pgstattuple, pgtap, plcoffee, pljava, plls, plpgsql_check, plv8, postgis, postgis_raster, postgis_sfcgal, postgis_tiger_geocoder, postgis_topology, postgres_fdw, refint, rum, seg, sslinfo, supabase_vault, supautils, tablefunc, tcn, timescaledb, tsm_system_rows, tsm_system_time, unaccent, uuid-ossp, vector, wrappers' +supautils.extension_custom_scripts_path = '/etc/postgresql-custom/extension-custom-scripts' +supautils.privileged_extensions_superuser = 'supabase_admin' +supautils.privileged_role = 'postgres' +supautils.privileged_role_allowed_configs = 'auto_explain.*, log_lock_waits, log_min_duration_statement, log_min_messages, log_replication_commands, log_statement, log_temp_files, pg_net.batch_size, pg_net.ttl, pg_stat_statements.*, pgaudit.log, pgaudit.log_catalog, pgaudit.log_client, pgaudit.log_level, pgaudit.log_relation, pgaudit.log_rows, pgaudit.log_statement, pgaudit.log_statement_once, pgaudit.role, pgrst.*, plan_filter.*, safeupdate.enabled, session_replication_role, track_io_timing, wal_compression' +supautils.reserved_memberships = 'pg_read_server_files, pg_write_server_files, pg_execute_server_program, supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, dashboard_user, pgbouncer, authenticator' +supautils.reserved_roles = 'supabase_admin, supabase_auth_admin, supabase_storage_admin, supabase_read_only_user, supabase_realtime_admin, supabase_replication_admin, supabase_etl_admin, dashboard_user, pgbouncer, service_role*, authenticator*, authenticated*, anon*' diff --git a/nix/systemModules/tests/test_postgres.py b/nix/systemModules/tests/test_postgres.py index f5c33a4cb..bb7ff6758 100644 --- a/nix/systemModules/tests/test_postgres.py +++ b/nix/systemModules/tests/test_postgres.py @@ -165,17 +165,25 @@ def test_postgres_configuration(host): # db_user_namespace doesn't exist in postgres >= 16 assert not config_file.contains("db_user_namespace") + def test_locales(host): installed_locales = host.run("localectl list-locales").stdout assert "C.UTF-8" in installed_locales, "C.UTF-8 locale should be installed" assert "en_US.UTF-8" in installed_locales, "en_US.UTF-8 locale should be installed" + def test_postgres_service_running(host): assert host.service("postgresql.service").is_valid - assert host.service("postgresql.service").is_running + assert host.service("postgresql.service").is_running, ( + "postgresql service should be running but failed: {}".format( + host.run( + "systemctl status postgresql.service; journalctl -n 100 -u postgresql.service" + ).stdout + ) + ) required_logs = [ - "Using language tag \"en-US\" for ICU locale \"en_US.UTF-8\"", + 'Using language tag "en-US" for ICU locale "en_US.UTF-8"', "locale provider: icu", "default collation: en-US", "LC_COLLATE: en_US.UTF-8", From 7d578f0cc3ea2d403d293c95746d57081cf0dca6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 22 Sep 2025 15:45:52 +0200 Subject: [PATCH 09/11] fix: extensions scripts location We moved the scripts into systemModules/postgres --- nix/packages/lib.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nix/packages/lib.nix b/nix/packages/lib.nix index fd3e29d76..d96765b61 100644 --- a/nix/packages/lib.nix +++ b/nix/packages/lib.nix @@ -58,7 +58,7 @@ }; postgresqlExtensionCustomScriptsPath = builtins.path { name = "extension-custom-scripts"; - path = ../../ansible/files/postgresql_extension_custom_scripts; + path = ../systemModules/postgres/extension-custom-scripts; }; getkeyScript = builtins.path { name = "pgsodium_getkey.sh"; From 1e5255c43f5ffc900a6aac6d39bcd6b74a376633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 22 Sep 2025 16:28:51 +0200 Subject: [PATCH 10/11] feat: add initialScript option to postgres module Add an initialScript option to the postgres module that allows users to specify a SQL script to be executed on the first startup of the PostgreSQL service. This is useful for setting up initial users, roles, or databases. --- nix/systemConfigs.nix | 22 ++++++---- nix/systemModules/postgres/default.nix | 52 ++++++++++++++++++++++++ nix/systemModules/tests/test_postgres.py | 11 +++++ 3 files changed, 77 insertions(+), 8 deletions(-) diff --git a/nix/systemConfigs.nix b/nix/systemConfigs.nix index ac83e9acb..356e591f0 100644 --- a/nix/systemConfigs.nix +++ b/nix/systemConfigs.nix @@ -2,14 +2,20 @@ let mkModules = system: [ self.systemModules.postgres - ({ - services.nginx.enable = true; - nixpkgs.hostPlatform = system; - supabase.services.postgres = { - enable = true; - package = self.packages.${system}."psql_17/bin"; - }; - }) + ( + { pkgs, ... }: + { + services.nginx.enable = true; + nixpkgs.hostPlatform = system; + supabase.services.postgres = { + enable = true; + package = self.packages.${system}."psql_17/bin"; + initialScript = pkgs.writeText "init-script.sql" '' + CREATE USER supabase_auth_admin NOINHERIT CREATEROLE LOGIN NOREPLICATION PASSWORD 'secret'; + ''; + }; + } + ) ]; systems = [ diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index 55eec3453..8d8cfa8f2 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -183,6 +183,19 @@ in initialisation. ''; }; + + initialScript = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = lib.literalExpression '' + pkgs.writeText "init-sql-script" ''' + alter user postgres with password 'myPassword'; + ''';''; + + description = '' + A file containing SQL statements to execute on first startup. + ''; + }; }; }; @@ -376,6 +389,45 @@ in ]; }; }; + postgresql-setup = { + description = "PostgreSQL Setup Scripts"; + + requires = [ "postgresql.service" ]; + after = [ "postgresql.service" ]; + + serviceConfig = { + User = "postgres"; + Group = "postgres"; + Type = "oneshot"; + RemainAfterExit = true; + }; + + path = [ cfg.package ]; + environment.PGPORT = builtins.toString cfg.settings.port; + + # Wait for PostgreSQL to be ready to accept connections. + script = '' + check-connection() { + psql -U ${cfg.superUser} -h localhost -d postgres -v ON_ERROR_STOP=1 <<-' EOF' + SELECT pg_is_in_recovery() \gset + \if :pg_is_in_recovery + \i still-recovering + \endif + EOF + } + while ! check-connection 2> /dev/null; do + if ! systemctl is-active --quiet postgresql.service; then exit 1; fi + sleep 0.1 + done + + if test -e "${cfg.dataDir}/.first_startup"; then + ${lib.optionalString (cfg.initialScript != null) '' + psql -U ${cfg.superUser} -h localhost -f "${cfg.initialScript}" -d postgres + ''} + rm -f "${cfg.dataDir}/.first_startup" + fi + ''; + }; "setup-locales" = { description = "Setup locales on the system"; diff --git a/nix/systemModules/tests/test_postgres.py b/nix/systemModules/tests/test_postgres.py index bb7ff6758..385bf1f8c 100644 --- a/nix/systemModules/tests/test_postgres.py +++ b/nix/systemModules/tests/test_postgres.py @@ -197,3 +197,14 @@ def test_postgres_service_running(host): logs = host.run("journalctl -u postgresql.service --no-pager").stdout for log in required_logs: assert log in logs, f"Log '{log}' should be present in PostgreSQL logs" + + +def test_postgres_setup_service_running(host): + assert host.service("postgresql-setup.service").is_valid + assert host.service("postgresql-setup.service").is_running, ( + "postgresql-setup service should be running but failed: {}".format( + host.run( + "systemctl status postgresql-setup.service; journalctl -n 100 -u postgresql-setup.service" + ).stdout + ) + ) From 26a39761051211815aaaea6b2bb76c47f883e4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Roche?= Date: Mon, 22 Sep 2025 16:58:21 +0200 Subject: [PATCH 11/11] fix(postgres): create .first_startup file after initdb This file is used by the postgresql-setup script to determine if it's the first time the database is being started. --- nix/systemModules/postgres/default.nix | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nix/systemModules/postgres/default.nix b/nix/systemModules/postgres/default.nix index 8d8cfa8f2..0ccda4830 100644 --- a/nix/systemModules/postgres/default.nix +++ b/nix/systemModules/postgres/default.nix @@ -315,6 +315,10 @@ in # Initialise the database. initdb ${lib.escapeShellArgs cfg.initdbArgs} fi + + # See postgresql-setup + touch "${cfg.dataDir}/.first_startup" + if [ ! -f /etc/postgresql-custom/pgsodium_root.key ]; then umask 077 echo "0000000000000000000000000000000000000000000000000000000000000000" > /etc/postgresql-custom/pgsodium_root.key