From 26c62c68f5a71d275c548d0e48026061a76eb942 Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:19:00 +0100 Subject: [PATCH 01/12] feat: additional config for pgbackrest - pgdata-signal: add remove-pid action to remove stale postmaster.pid via the constrained wrapper rather than a broad sudo rm entry, keeping the sudoers scope limited to this script --- ansible/files/adminapi.sudoers.conf | 4 ++ .../files/pgbackrest_config/pgbackrest.conf | 7 +-- .../pgbackrest_config/pgbackrest.logrotate | 9 ++++ .../files/postgresql_config/pg_hba.conf.j2 | 2 +- .../supabase_admin_agent_config/pgdata-chown | 37 +++++++++++++++ .../supabase_admin_agent_config/pgdata-signal | 45 +++++++++++++++++++ .../tasks/internal/supabase-admin-agent.yml | 24 ++++++++++ ansible/tasks/setup-pgbackrest.yml | 19 +++++++- 8 files changed, 139 insertions(+), 8 deletions(-) create mode 100644 ansible/files/pgbackrest_config/pgbackrest.logrotate create mode 100644 ansible/files/supabase_admin_agent_config/pgdata-chown create mode 100644 ansible/files/supabase_admin_agent_config/pgdata-signal diff --git a/ansible/files/adminapi.sudoers.conf b/ansible/files/adminapi.sudoers.conf index 9ec7157440..90ca5b2cbf 100644 --- a/ansible/files/adminapi.sudoers.conf +++ b/ansible/files/adminapi.sudoers.conf @@ -15,6 +15,10 @@ Cmnd_Alias PGBOUNCER = /bin/systemctl start pgbouncer.service, /bin/systemctl st %adminapi ALL= NOPASSWD: /etc/adminapi/pg_upgrade_scripts/common.sh %adminapi ALL= NOPASSWD: /etc/adminapi/pg_upgrade_scripts/pgsodium_getkey.sh %adminapi ALL= NOPASSWD: /usr/bin/systemctl daemon-reload +%adminapi ALL= NOPASSWD: /usr/local/lib/supabase-admin-agent/pgdata-chown +%adminapi ALL=(postgres) NOPASSWD: /usr/local/lib/supabase-admin-agent/pgdata-signal +%adminapi ALL= NOPASSWD: /usr/bin/systemctl start postgresql.service +%adminapi ALL= NOPASSWD: /usr/bin/systemctl stop postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl reload postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl restart postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl show -p NRestarts postgresql.service diff --git a/ansible/files/pgbackrest_config/pgbackrest.conf b/ansible/files/pgbackrest_config/pgbackrest.conf index f11db6ed95..f94dde0e9e 100644 --- a/ansible/files/pgbackrest_config/pgbackrest.conf +++ b/ansible/files/pgbackrest_config/pgbackrest.conf @@ -4,15 +4,10 @@ archive-copy = y backup-standby = prefer compress-type = zst delta = y -expire-auto = n +expire-auto = y link-all = y log-level-console = info log-level-file = detail log-subprocess = y resume = n start-fast = y - -[supabase] -pg1-path = /var/lib/postgresql/data -pg1-socket-path = /run/postgresql -pg1-user = supabase_admin diff --git a/ansible/files/pgbackrest_config/pgbackrest.logrotate b/ansible/files/pgbackrest_config/pgbackrest.logrotate new file mode 100644 index 0000000000..333f7c90a1 --- /dev/null +++ b/ansible/files/pgbackrest_config/pgbackrest.logrotate @@ -0,0 +1,9 @@ +/var/log/pgbackrest/*.log { + daily + rotate 7 + compress + delaycompress + missingok + notifempty + create 0660 pgbackrest postgres +} diff --git a/ansible/files/postgresql_config/pg_hba.conf.j2 b/ansible/files/postgresql_config/pg_hba.conf.j2 index 9cafd4146e..5bcd2aaccb 100755 --- a/ansible/files/postgresql_config/pg_hba.conf.j2 +++ b/ansible/files/postgresql_config/pg_hba.conf.j2 @@ -79,7 +79,7 @@ # TYPE DATABASE USER ADDRESS METHOD # trust local connections -local all supabase_admin scram-sha-256 +local all supabase_admin trust local all all peer map=supabase_map host all all 127.0.0.1/32 trust host all all ::1/128 trust diff --git a/ansible/files/supabase_admin_agent_config/pgdata-chown b/ansible/files/supabase_admin_agent_config/pgdata-chown new file mode 100644 index 0000000000..74d979435d --- /dev/null +++ b/ansible/files/supabase_admin_agent_config/pgdata-chown @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +# pgdata-chown — transfers PGDATA ownership for pgBackRest restore operations. +# +# Called via sudo by supabase-admin-agent (running as adminapi). Only two +# actions are accepted, and the target path must resolve to /data/pgdata or a +# path beneath it. realpath(1) is used to expand symlinks before the check, +# which prevents directory-traversal attacks (e.g. /data/pgdata/../../etc/sudoers). +# +# Usage: pgdata-chown +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "usage: pgdata-chown " >&2 + exit 1 +fi + +ACTION="$1" +TARGET="$2" + +REAL=$(realpath "$TARGET") +if [[ "$REAL" != "/data/pgdata" && "$REAL" != /data/pgdata/* ]]; then + echo "error: '${TARGET}' resolves to '${REAL}', which is not under /data/pgdata" >&2 + exit 1 +fi + +case "$ACTION" in + to-pgbackrest) + exec /usr/bin/chown -R pgbackrest:pgbackrest "$REAL" + ;; + to-postgres) + exec /usr/bin/chown -R postgres:postgres "$REAL" + ;; + *) + echo "error: unknown action '${ACTION}'; expected to-pgbackrest or to-postgres" >&2 + exit 1 + ;; +esac diff --git a/ansible/files/supabase_admin_agent_config/pgdata-signal b/ansible/files/supabase_admin_agent_config/pgdata-signal new file mode 100644 index 0000000000..94a80a7024 --- /dev/null +++ b/ansible/files/supabase_admin_agent_config/pgdata-signal @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# pgdata-signal — creates or removes PostgreSQL signal files and stale pid files. +# Called via sudo (as postgres) by supabase-admin-agent (running as adminapi). +# +# All file paths are hardcoded to prevent path injection. No external +# path argument is accepted. +# +# Usage: pgdata-signal +# pgdata-signal remove-pid +set -euo pipefail + +# Special-case: remove-pid removes the stale postmaster.pid file that would +# prevent PostgreSQL from starting after a restore. Handled as a single-arg +# command to keep the sudoers entry scoped to this script rather than allowing +# a broad "rm" entry. +if [[ $# -eq 1 && "$1" == "remove-pid" ]]; then + exec /usr/bin/rm -f "/data/pgdata/postmaster.pid" +fi + +if [[ $# -ne 2 ]]; then + echo "usage: pgdata-signal " >&2 + echo " pgdata-signal remove-pid" >&2 + exit 1 +fi + +ACTION="$1" +SIGNAL_TYPE="$2" + +case "$SIGNAL_TYPE" in + recovery) FILE="/data/pgdata/recovery.signal" ;; + standby) FILE="/data/pgdata/standby.signal" ;; + *) + echo "error: unknown signal type '${SIGNAL_TYPE}'; expected recovery or standby" >&2 + exit 1 + ;; +esac + +case "$ACTION" in + create) exec /usr/bin/touch "$FILE" ;; + remove) exec /usr/bin/rm -f "$FILE" ;; + *) + echo "error: unknown action '${ACTION}'; expected create or remove" >&2 + exit 1 + ;; +esac diff --git a/ansible/tasks/internal/supabase-admin-agent.yml b/ansible/tasks/internal/supabase-admin-agent.yml index 0dfc4427ae..266b915aaf 100644 --- a/ansible/tasks/internal/supabase-admin-agent.yml +++ b/ansible/tasks/internal/supabase-admin-agent.yml @@ -31,6 +31,30 @@ dest: /etc/sudoers.d/supabase-admin-agent mode: "0440" +- name: supabase-admin-agent - pgbackrest helper scripts dir + file: + path: /usr/local/lib/supabase-admin-agent + state: directory + owner: root + group: root + mode: "0755" + +- name: supabase-admin-agent - pgdata-chown script + copy: + src: files/supabase_admin_agent_config/pgdata-chown + dest: /usr/local/lib/supabase-admin-agent/pgdata-chown + owner: root + group: root + mode: "0700" + +- name: supabase-admin-agent - pgdata-signal script + copy: + src: files/supabase_admin_agent_config/pgdata-signal + dest: /usr/local/lib/supabase-admin-agent/pgdata-signal + owner: root + group: root + mode: "0700" + - name: Setting arch (x86) set_fact: arch: "x86" diff --git a/ansible/tasks/setup-pgbackrest.yml b/ansible/tasks/setup-pgbackrest.yml index 53b4602813..6c44cfd51b 100644 --- a/ansible/tasks/setup-pgbackrest.yml +++ b/ansible/tasks/setup-pgbackrest.yml @@ -48,7 +48,6 @@ path: "{{ backrest_dir }}" state: directory loop: - - /etc/pgbackrest/conf.d - /var/lib/pgbackrest - /var/spool/pgbackrest - /var/log/pgbackrest @@ -57,6 +56,16 @@ when: - nixpkg_mode +- name: Create pgBackRest conf.d directory with setgid + ansible.legacy.file: + group: postgres + mode: '02770' + owner: pgbackrest + path: /etc/pgbackrest/conf.d + state: directory + when: + - nixpkg_mode + - name: Symlink pgbackrest.conf ansible.legacy.file: force: true @@ -82,6 +91,14 @@ when: - stage2_nix +- name: pgBackRest - logrotate config + ansible.legacy.copy: + src: files/pgbackrest_config/pgbackrest.logrotate + dest: /etc/logrotate.d/pgbackrest + owner: root + group: root + mode: '0644' + - name: Create pgBackRest wrapper script ansible.builtin.copy: content: | From 4e94a8d47343cf4b7cfbc4070836f27163ec113f Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:12:47 +0100 Subject: [PATCH 02/12] fix: address pgbackrest PR review feedback - pgdata-chown: simplify case; use group=postgres consistently for both ownership targets (pgbackrest:postgres and postgres:postgres) - pgdata-signal: consolidate recovery/standby case into single pattern - pgdata-signal: deploy at mode 0755 so postgres can execute via sudo -u - setup-pgbackrest.yml: combine dir creation into single task with dict loop; conf.d gets 02770 setgid, others get default 0770 - setup-pgbackrest.yml: sort logrotate task keys alphabetically --- .../supabase_admin_agent_config/pgdata-chown | 7 ++---- .../supabase_admin_agent_config/pgdata-signal | 3 +-- .../tasks/internal/supabase-admin-agent.yml | 2 +- ansible/tasks/setup-pgbackrest.yml | 25 ++++++------------- 4 files changed, 12 insertions(+), 25 deletions(-) diff --git a/ansible/files/supabase_admin_agent_config/pgdata-chown b/ansible/files/supabase_admin_agent_config/pgdata-chown index 74d979435d..05af5bd7d9 100644 --- a/ansible/files/supabase_admin_agent_config/pgdata-chown +++ b/ansible/files/supabase_admin_agent_config/pgdata-chown @@ -24,11 +24,8 @@ if [[ "$REAL" != "/data/pgdata" && "$REAL" != /data/pgdata/* ]]; then fi case "$ACTION" in - to-pgbackrest) - exec /usr/bin/chown -R pgbackrest:pgbackrest "$REAL" - ;; - to-postgres) - exec /usr/bin/chown -R postgres:postgres "$REAL" + to-pgbackrest|to-postgres) + exec /usr/bin/chown -R "${ACTION:3}:postgres" "$REAL" ;; *) echo "error: unknown action '${ACTION}'; expected to-pgbackrest or to-postgres" >&2 diff --git a/ansible/files/supabase_admin_agent_config/pgdata-signal b/ansible/files/supabase_admin_agent_config/pgdata-signal index 94a80a7024..15552b36f2 100644 --- a/ansible/files/supabase_admin_agent_config/pgdata-signal +++ b/ansible/files/supabase_admin_agent_config/pgdata-signal @@ -27,8 +27,7 @@ ACTION="$1" SIGNAL_TYPE="$2" case "$SIGNAL_TYPE" in - recovery) FILE="/data/pgdata/recovery.signal" ;; - standby) FILE="/data/pgdata/standby.signal" ;; + recovery|standby) FILE="/data/pgdata/${SIGNAL_TYPE}.signal" ;; *) echo "error: unknown signal type '${SIGNAL_TYPE}'; expected recovery or standby" >&2 exit 1 diff --git a/ansible/tasks/internal/supabase-admin-agent.yml b/ansible/tasks/internal/supabase-admin-agent.yml index 266b915aaf..365ef2ab0a 100644 --- a/ansible/tasks/internal/supabase-admin-agent.yml +++ b/ansible/tasks/internal/supabase-admin-agent.yml @@ -53,7 +53,7 @@ dest: /usr/local/lib/supabase-admin-agent/pgdata-signal owner: root group: root - mode: "0700" + mode: "0755" - name: Setting arch (x86) set_fact: diff --git a/ansible/tasks/setup-pgbackrest.yml b/ansible/tasks/setup-pgbackrest.yml index 6c44cfd51b..3f89b4e8c9 100644 --- a/ansible/tasks/setup-pgbackrest.yml +++ b/ansible/tasks/setup-pgbackrest.yml @@ -43,29 +43,20 @@ - name: Create needed directories for pgBackRest ansible.legacy.file: group: postgres - mode: '0770' + mode: "{{ backrest_dir['mode'] | default('0770', true) }}" owner: pgbackrest - path: "{{ backrest_dir }}" + path: "{{ backrest_dir['dir'] }}" state: directory loop: - - /var/lib/pgbackrest - - /var/spool/pgbackrest - - /var/log/pgbackrest + - {dir: /etc/pgbackrest/conf.d, mode: '02770'} + - {dir: /var/lib/pgbackrest} + - {dir: /var/spool/pgbackrest} + - {dir: /var/log/pgbackrest} loop_control: loop_var: backrest_dir when: - nixpkg_mode -- name: Create pgBackRest conf.d directory with setgid - ansible.legacy.file: - group: postgres - mode: '02770' - owner: pgbackrest - path: /etc/pgbackrest/conf.d - state: directory - when: - - nixpkg_mode - - name: Symlink pgbackrest.conf ansible.legacy.file: force: true @@ -93,11 +84,11 @@ - name: pgBackRest - logrotate config ansible.legacy.copy: - src: files/pgbackrest_config/pgbackrest.logrotate dest: /etc/logrotate.d/pgbackrest - owner: root group: root mode: '0644' + owner: root + src: files/pgbackrest_config/pgbackrest.logrotate - name: Create pgBackRest wrapper script ansible.builtin.copy: From 81774c730fa635a791e5e62ca773f212377cab4b Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:54:26 +0100 Subject: [PATCH 03/12] fix: add missing pgbackrest sudoers entries and pre-create log files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps found by cross-referencing SAA commands against Ansible: 1. adminapi.sudoers.conf: add two entries so adminapi can call the pgbackrest binary via the wrapper. - NewRunner() path: wrapper calls sudo -u pgbackrest , requires adminapi -> pgbackrest NOPASSWD for the real binary path. - NewRunnerAs("pgbackrest") path: SAA does sudo -n -u pgbackrest /usr/bin/pgbackrest, requires adminapi -> pgbackrest NOPASSWD for the wrapper path. 2. setup-pgbackrest.yml: add pgbackrest -> pgbackrest sudoers entry for the real binary. When NewRunnerAs runs the wrapper as the pgbackrest user, the wrapper still calls sudo -u pgbackrest internally; without this entry that inner sudo fails. 3. setup-pgbackrest.yml: pre-create the three SAA log files (saa-pgb.log, wal-push.log, wal-fetch.log) as pgbackrest:postgres 0660. SAA opens them with O_APPEND|O_WRONLY (no O_CREATE) — a missing file causes enable to fail immediately before any pgBackRest work. modification_time/access_time: preserve means the task is idempotent. --- ansible/files/adminapi.sudoers.conf | 2 ++ ansible/tasks/setup-pgbackrest.yml | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/ansible/files/adminapi.sudoers.conf b/ansible/files/adminapi.sudoers.conf index 90ca5b2cbf..8d267f4b49 100644 --- a/ansible/files/adminapi.sudoers.conf +++ b/ansible/files/adminapi.sudoers.conf @@ -17,6 +17,8 @@ Cmnd_Alias PGBOUNCER = /bin/systemctl start pgbouncer.service, /bin/systemctl st %adminapi ALL= NOPASSWD: /usr/bin/systemctl daemon-reload %adminapi ALL= NOPASSWD: /usr/local/lib/supabase-admin-agent/pgdata-chown %adminapi ALL=(postgres) NOPASSWD: /usr/local/lib/supabase-admin-agent/pgdata-signal +%adminapi ALL=(pgbackrest) NOPASSWD: /var/lib/pgbackrest/.nix-profile/bin/pgbackrest +%adminapi ALL=(pgbackrest) NOPASSWD: /usr/bin/pgbackrest %adminapi ALL= NOPASSWD: /usr/bin/systemctl start postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl stop postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl reload postgresql.service diff --git a/ansible/tasks/setup-pgbackrest.yml b/ansible/tasks/setup-pgbackrest.yml index 3f89b4e8c9..1ef4bf8f83 100644 --- a/ansible/tasks/setup-pgbackrest.yml +++ b/ansible/tasks/setup-pgbackrest.yml @@ -30,6 +30,7 @@ - 'postgres ALL=(pgbackrest) NOPASSWD: /usr/bin/bash' - 'postgres ALL=(pgbackrest) NOPASSWD: /usr/bin/nix' - 'pgbackrest ALL=(pgbackrest) NOPASSWD: /usr/bin/bash' + - 'pgbackrest ALL=(pgbackrest) NOPASSWD: /var/lib/pgbackrest/.nix-profile/bin/pgbackrest' - name: Install pgBackRest ansible.builtin.shell: | @@ -57,6 +58,22 @@ when: - nixpkg_mode +- name: Pre-create pgBackRest SAA log files + ansible.builtin.file: + access_time: preserve + group: postgres + mode: '0660' + modification_time: preserve + owner: pgbackrest + path: "{{ item }}" + state: touch + loop: + - /var/log/pgbackrest/saa-pgb.log + - /var/log/pgbackrest/wal-push.log + - /var/log/pgbackrest/wal-fetch.log + when: + - nixpkg_mode + - name: Symlink pgbackrest.conf ansible.legacy.file: force: true From 4552fb05c2cbee46b4213349eaf137b8fcb387e3 Mon Sep 17 00:00:00 2001 From: Douglas J Hunley Date: Tue, 7 Apr 2026 08:36:49 -0400 Subject: [PATCH 04/12] Update ansible/files/supabase_admin_agent_config/pgdata-signal Co-authored-by: Tom Ashley --- ansible/files/supabase_admin_agent_config/pgdata-signal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/files/supabase_admin_agent_config/pgdata-signal b/ansible/files/supabase_admin_agent_config/pgdata-signal index 15552b36f2..e2b329454e 100644 --- a/ansible/files/supabase_admin_agent_config/pgdata-signal +++ b/ansible/files/supabase_admin_agent_config/pgdata-signal @@ -35,7 +35,7 @@ case "$SIGNAL_TYPE" in esac case "$ACTION" in - create) exec /usr/bin/touch "$FILE" ;; + create) exec /usr/bin/touch "${FILE}" ;; remove) exec /usr/bin/rm -f "$FILE" ;; *) echo "error: unknown action '${ACTION}'; expected create or remove" >&2 From 8efab8c718dcd377d2140f04bd9f99b8166357c4 Mon Sep 17 00:00:00 2001 From: Douglas J Hunley Date: Tue, 7 Apr 2026 08:37:08 -0400 Subject: [PATCH 05/12] Update ansible/files/supabase_admin_agent_config/pgdata-signal Co-authored-by: Tom Ashley --- ansible/files/supabase_admin_agent_config/pgdata-signal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/files/supabase_admin_agent_config/pgdata-signal b/ansible/files/supabase_admin_agent_config/pgdata-signal index e2b329454e..2479dd6dc1 100644 --- a/ansible/files/supabase_admin_agent_config/pgdata-signal +++ b/ansible/files/supabase_admin_agent_config/pgdata-signal @@ -36,7 +36,7 @@ esac case "$ACTION" in create) exec /usr/bin/touch "${FILE}" ;; - remove) exec /usr/bin/rm -f "$FILE" ;; + remove) exec /usr/bin/rm -f "${FILE}" ;; *) echo "error: unknown action '${ACTION}'; expected create or remove" >&2 exit 1 From f5d5b34c273fb996e426da9f370965ce806a7000 Mon Sep 17 00:00:00 2001 From: Douglas J Hunley Date: Tue, 7 Apr 2026 08:37:28 -0400 Subject: [PATCH 06/12] Update ansible/tasks/internal/supabase-admin-agent.yml Co-authored-by: Tom Ashley --- ansible/tasks/internal/supabase-admin-agent.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/tasks/internal/supabase-admin-agent.yml b/ansible/tasks/internal/supabase-admin-agent.yml index 365ef2ab0a..266b915aaf 100644 --- a/ansible/tasks/internal/supabase-admin-agent.yml +++ b/ansible/tasks/internal/supabase-admin-agent.yml @@ -53,7 +53,7 @@ dest: /usr/local/lib/supabase-admin-agent/pgdata-signal owner: root group: root - mode: "0755" + mode: "0700" - name: Setting arch (x86) set_fact: From 24e4bfb728c6dca9dff6116bdb0d3008b0d5e9a6 Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:01:45 +0100 Subject: [PATCH 07/12] fix: use peer auth with saa_map for supabase_admin local connections Replace trust with peer map=saa_map for the supabase_admin pg_hba rule. Add saa_map entries in pg_ident.conf mapping adminapi and root OS users to the supabase_admin PG user, as required by supabase-admin-agent. --- ansible/files/postgresql_config/pg_hba.conf.j2 | 2 +- ansible/files/postgresql_config/pg_ident.conf.j2 | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ansible/files/postgresql_config/pg_hba.conf.j2 b/ansible/files/postgresql_config/pg_hba.conf.j2 index 5bcd2aaccb..237737b5d5 100755 --- a/ansible/files/postgresql_config/pg_hba.conf.j2 +++ b/ansible/files/postgresql_config/pg_hba.conf.j2 @@ -79,7 +79,7 @@ # TYPE DATABASE USER ADDRESS METHOD # trust local connections -local all supabase_admin trust +local all supabase_admin peer map=saa_map local all all peer map=supabase_map host all all 127.0.0.1/32 trust host all all ::1/128 trust diff --git a/ansible/files/postgresql_config/pg_ident.conf.j2 b/ansible/files/postgresql_config/pg_ident.conf.j2 index d8891f4166..8a6900103a 100755 --- a/ansible/files/postgresql_config/pg_ident.conf.j2 +++ b/ansible/files/postgresql_config/pg_ident.conf.j2 @@ -48,3 +48,7 @@ supabase_map ubuntu postgres supabase_map gotrue supabase_auth_admin supabase_map postgrest authenticator supabase_map adminapi postgres + +# supabase-admin-agent: adminapi and root connect as supabase_admin +saa_map adminapi supabase_admin +saa_map root supabase_admin From 74253319b4e09999a25405087e22298eb0b790b9 Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:17:49 +0100 Subject: [PATCH 08/12] fix: restore pgdata-signal mode to 0755 pgdata-signal is called via sudo as postgres (other user), so it must be world-executable. 0700 would prevent postgres from running it. --- ansible/tasks/internal/supabase-admin-agent.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ansible/tasks/internal/supabase-admin-agent.yml b/ansible/tasks/internal/supabase-admin-agent.yml index 266b915aaf..365ef2ab0a 100644 --- a/ansible/tasks/internal/supabase-admin-agent.yml +++ b/ansible/tasks/internal/supabase-admin-agent.yml @@ -53,7 +53,7 @@ dest: /usr/local/lib/supabase-admin-agent/pgdata-signal owner: root group: root - mode: "0700" + mode: "0755" - name: Setting arch (x86) set_fact: From 05195c55b0f328daaa3af92588bee09ee51179c8 Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:58:50 +0100 Subject: [PATCH 09/12] fix: revert supabase_admin auth to trust, remove saa_map from pg_ident MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit peer map=saa_map cannot be cleanly implemented in pg_hba.conf.j2 while the Dockerfiles copy it raw — pg_hba include_if_exists (PG 16+) is needed to separate Docker from production, and that work is in progress on a separate branch. Trust is appropriate for this local Unix-socket-only connection; the security boundary is OS-level access to the machine. Revisit once the include directive infrastructure lands. --- ansible/files/postgresql_config/pg_hba.conf.j2 | 2 +- ansible/files/postgresql_config/pg_ident.conf.j2 | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ansible/files/postgresql_config/pg_hba.conf.j2 b/ansible/files/postgresql_config/pg_hba.conf.j2 index 237737b5d5..5bcd2aaccb 100755 --- a/ansible/files/postgresql_config/pg_hba.conf.j2 +++ b/ansible/files/postgresql_config/pg_hba.conf.j2 @@ -79,7 +79,7 @@ # TYPE DATABASE USER ADDRESS METHOD # trust local connections -local all supabase_admin peer map=saa_map +local all supabase_admin trust local all all peer map=supabase_map host all all 127.0.0.1/32 trust host all all ::1/128 trust diff --git a/ansible/files/postgresql_config/pg_ident.conf.j2 b/ansible/files/postgresql_config/pg_ident.conf.j2 index 8a6900103a..d8891f4166 100755 --- a/ansible/files/postgresql_config/pg_ident.conf.j2 +++ b/ansible/files/postgresql_config/pg_ident.conf.j2 @@ -48,7 +48,3 @@ supabase_map ubuntu postgres supabase_map gotrue supabase_auth_admin supabase_map postgrest authenticator supabase_map adminapi postgres - -# supabase-admin-agent: adminapi and root connect as supabase_admin -saa_map adminapi supabase_admin -saa_map root supabase_admin From c01d48fc3b969f77009aa3ca44e323624c708729 Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:25:53 +0100 Subject: [PATCH 10/12] docs: add comment to supabase_admin trust rule in pg_hba.conf --- ansible/files/postgresql_config/pg_hba.conf.j2 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ansible/files/postgresql_config/pg_hba.conf.j2 b/ansible/files/postgresql_config/pg_hba.conf.j2 index 5bcd2aaccb..355dd4dc85 100755 --- a/ansible/files/postgresql_config/pg_hba.conf.j2 +++ b/ansible/files/postgresql_config/pg_hba.conf.j2 @@ -79,6 +79,9 @@ # TYPE DATABASE USER ADDRESS METHOD # trust local connections +# supabase_admin: trust over Unix socket only — no network exposure. Access +# requires OS-level shell access to the host, which is already a stronger +# security boundary than a database password. local all supabase_admin trust local all all peer map=supabase_map host all all 127.0.0.1/32 trust From 05b68f81309eef22b87af2d71aded81269e86e0d Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:30:32 +0100 Subject: [PATCH 11/12] docs: add explanatory comments to pgBackRest Ansible changes Clarify the intent and rationale behind the less-obvious changes: - adminapi.sudoers.conf: explain the two pgbackrest sudo chains (NewRunner vs NewRunnerAs) and the wrapper/real-binary split - supabase-admin-agent.yml: explain why pgdata-chown is 0700 and pgdata-signal is 0755 - setup-pgbackrest.yml: explain the pgbackrest self-sudo entry, why conf.d needs setgid (02770), and why SAA log files must be pre-created before the agent runs - pgbackrest.conf: explain expire-auto change and why the [supabase] stanza was removed (SAA owns it via conf.d to avoid error [031]) --- ansible/files/adminapi.sudoers.conf | 9 +++++++++ ansible/files/pgbackrest_config/pgbackrest.conf | 7 +++++++ ansible/tasks/internal/supabase-admin-agent.yml | 3 +++ ansible/tasks/setup-pgbackrest.yml | 11 +++++++++++ 4 files changed, 30 insertions(+) diff --git a/ansible/files/adminapi.sudoers.conf b/ansible/files/adminapi.sudoers.conf index 8d267f4b49..e6d27bd104 100644 --- a/ansible/files/adminapi.sudoers.conf +++ b/ansible/files/adminapi.sudoers.conf @@ -15,10 +15,19 @@ Cmnd_Alias PGBOUNCER = /bin/systemctl start pgbouncer.service, /bin/systemctl st %adminapi ALL= NOPASSWD: /etc/adminapi/pg_upgrade_scripts/common.sh %adminapi ALL= NOPASSWD: /etc/adminapi/pg_upgrade_scripts/pgsodium_getkey.sh %adminapi ALL= NOPASSWD: /usr/bin/systemctl daemon-reload +# pgBackRest wrapper scripts: constrained helpers called by supabase-admin-agent. +# pgdata-chown runs as root (default); pgdata-signal runs as postgres so it can +# create/remove signal files owned by that user. %adminapi ALL= NOPASSWD: /usr/local/lib/supabase-admin-agent/pgdata-chown %adminapi ALL=(postgres) NOPASSWD: /usr/local/lib/supabase-admin-agent/pgdata-signal +# pgBackRest binary entries support two sudo chains used by supabase-admin-agent: +# NewRunner() — adminapi → /usr/bin/pgbackrest wrapper → sudo -u pgbackrest real_binary +# NewRunnerAs() — adminapi → sudo -u pgbackrest /usr/bin/pgbackrest → sudo -u pgbackrest real_binary +# Both paths require the wrapper entry; NewRunner() additionally needs the real binary entry +# because the wrapper itself calls sudo to drop privileges to the pgbackrest user. %adminapi ALL=(pgbackrest) NOPASSWD: /var/lib/pgbackrest/.nix-profile/bin/pgbackrest %adminapi ALL=(pgbackrest) NOPASSWD: /usr/bin/pgbackrest +# pgBackRest restore stops PostgreSQL, restores PGDATA, then starts PostgreSQL. %adminapi ALL= NOPASSWD: /usr/bin/systemctl start postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl stop postgresql.service %adminapi ALL= NOPASSWD: /usr/bin/systemctl reload postgresql.service diff --git a/ansible/files/pgbackrest_config/pgbackrest.conf b/ansible/files/pgbackrest_config/pgbackrest.conf index f94dde0e9e..f54336c4e8 100644 --- a/ansible/files/pgbackrest_config/pgbackrest.conf +++ b/ansible/files/pgbackrest_config/pgbackrest.conf @@ -4,6 +4,9 @@ archive-copy = y backup-standby = prefer compress-type = zst delta = y +# expire-auto=y allows pgBackRest to expire old backups automatically after +# each new backup completes, keeping retention in sync without a separate +# expire command. Previously n; changed to work correctly with SAA-managed backups. expire-auto = y link-all = y log-level-console = info @@ -11,3 +14,7 @@ log-level-file = detail log-subprocess = y resume = n start-fast = y +# Note: the [supabase] stanza (pg1-path, pg1-socket-path, pg1-user) has been +# removed from this file. supabase-admin-agent owns that stanza and writes it +# to /etc/pgbackrest/conf.d/ at enable time. Having the stanza in both places +# causes pgBackRest error [031] (duplicate option). diff --git a/ansible/tasks/internal/supabase-admin-agent.yml b/ansible/tasks/internal/supabase-admin-agent.yml index 365ef2ab0a..db6c056071 100644 --- a/ansible/tasks/internal/supabase-admin-agent.yml +++ b/ansible/tasks/internal/supabase-admin-agent.yml @@ -45,6 +45,7 @@ dest: /usr/local/lib/supabase-admin-agent/pgdata-chown owner: root group: root + # 0700: always invoked via "sudo" (as root); no other user needs execute. mode: "0700" - name: supabase-admin-agent - pgdata-signal script @@ -53,6 +54,8 @@ dest: /usr/local/lib/supabase-admin-agent/pgdata-signal owner: root group: root + # 0755: invoked via "sudo -u postgres"; the postgres OS user needs the + # world-execute bit since it is neither owner nor group member. mode: "0755" - name: Setting arch (x86) diff --git a/ansible/tasks/setup-pgbackrest.yml b/ansible/tasks/setup-pgbackrest.yml index 1ef4bf8f83..a5f1e8562c 100644 --- a/ansible/tasks/setup-pgbackrest.yml +++ b/ansible/tasks/setup-pgbackrest.yml @@ -30,6 +30,10 @@ - 'postgres ALL=(pgbackrest) NOPASSWD: /usr/bin/bash' - 'postgres ALL=(pgbackrest) NOPASSWD: /usr/bin/nix' - 'pgbackrest ALL=(pgbackrest) NOPASSWD: /usr/bin/bash' + # Required for the NewRunnerAs() path: supabase-admin-agent runs the + # /usr/bin/pgbackrest wrapper as the pgbackrest user, but the wrapper + # internally calls "sudo -u pgbackrest ". Without this entry + # that inner sudo fails even though the outer caller is already pgbackrest. - 'pgbackrest ALL=(pgbackrest) NOPASSWD: /var/lib/pgbackrest/.nix-profile/bin/pgbackrest' - name: Install pgBackRest @@ -49,6 +53,9 @@ path: "{{ backrest_dir['dir'] }}" state: directory loop: + # conf.d is setgid (02770) so files written by adminapi (postgres group) + # automatically inherit the postgres group. Without setgid, pgbackrest + # (running as the pgbackrest user) cannot read conf files created by adminapi. - {dir: /etc/pgbackrest/conf.d, mode: '02770'} - {dir: /var/lib/pgbackrest} - {dir: /var/spool/pgbackrest} @@ -58,6 +65,10 @@ when: - nixpkg_mode +# supabase-admin-agent opens these log files with O_APPEND|O_WRONLY (no +# O_CREATE). They must exist before SAA runs; a missing file causes the +# pgBackRest enable command to fail before any backup work is attempted. +# access_time/modification_time: preserve keeps this task idempotent. - name: Pre-create pgBackRest SAA log files ansible.builtin.file: access_time: preserve From d530fa79394ad0b5e9a881ca71fbcbbe3116dbe4 Mon Sep 17 00:00:00 2001 From: Crispy1975 <12525875+Crispy1975@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:48:14 +0100 Subject: [PATCH 12/12] fix: move pgbackrest logrotate config to follow repo convention - Move from pgbackrest_config/ to logrotate_config/ to match the existing pattern for all other logrotate configs in the repo - Deploy via the finalize-ami.yml loop rather than setup-pgbackrest.yml - Add size 50M cap to prevent logs growing unbounded between daily rotations during large backup/restore operations --- .../logrotate-pgbackrest.conf} | 1 + ansible/tasks/finalize-ami.yml | 1 + ansible/tasks/setup-pgbackrest.yml | 8 -------- 3 files changed, 2 insertions(+), 8 deletions(-) rename ansible/files/{pgbackrest_config/pgbackrest.logrotate => logrotate_config/logrotate-pgbackrest.conf} (91%) diff --git a/ansible/files/pgbackrest_config/pgbackrest.logrotate b/ansible/files/logrotate_config/logrotate-pgbackrest.conf similarity index 91% rename from ansible/files/pgbackrest_config/pgbackrest.logrotate rename to ansible/files/logrotate_config/logrotate-pgbackrest.conf index 333f7c90a1..e9909fcb67 100644 --- a/ansible/files/pgbackrest_config/pgbackrest.logrotate +++ b/ansible/files/logrotate_config/logrotate-pgbackrest.conf @@ -1,5 +1,6 @@ /var/log/pgbackrest/*.log { daily + size 50M rotate 7 compress delaycompress diff --git a/ansible/tasks/finalize-ami.yml b/ansible/tasks/finalize-ami.yml index a02333e7de..a8531dd770 100644 --- a/ansible/tasks/finalize-ami.yml +++ b/ansible/tasks/finalize-ami.yml @@ -61,6 +61,7 @@ - { file: 'logrotate-postgres-auth.conf' } - { file: 'logrotate-postgres-csv.conf' } - { file: 'logrotate-walg.conf' } + - { file: 'logrotate-pgbackrest.conf' } loop_control: loop_var: 'logrotate_item' diff --git a/ansible/tasks/setup-pgbackrest.yml b/ansible/tasks/setup-pgbackrest.yml index a5f1e8562c..3015bf1e4e 100644 --- a/ansible/tasks/setup-pgbackrest.yml +++ b/ansible/tasks/setup-pgbackrest.yml @@ -110,14 +110,6 @@ when: - stage2_nix -- name: pgBackRest - logrotate config - ansible.legacy.copy: - dest: /etc/logrotate.d/pgbackrest - group: root - mode: '0644' - owner: root - src: files/pgbackrest_config/pgbackrest.logrotate - - name: Create pgBackRest wrapper script ansible.builtin.copy: content: |