diff --git a/ansible/files/adminapi.sudoers.conf b/ansible/files/adminapi.sudoers.conf index 904377c293..dbc353e711 100644 --- a/ansible/files/adminapi.sudoers.conf +++ b/ansible/files/adminapi.sudoers.conf @@ -15,6 +15,12 @@ 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=(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 %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..05af5bd7d9 --- /dev/null +++ b/ansible/files/supabase_admin_agent_config/pgdata-chown @@ -0,0 +1,34 @@ +#!/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|to-postgres) + exec /usr/bin/chown -R "${ACTION:3}: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..15552b36f2 --- /dev/null +++ b/ansible/files/supabase_admin_agent_config/pgdata-signal @@ -0,0 +1,44 @@ +#!/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|standby) FILE="/data/pgdata/${SIGNAL_TYPE}.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..365ef2ab0a 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: "0755" + - name: Setting arch (x86) set_fact: arch: "x86" diff --git a/ansible/tasks/setup-pgbackrest.yml b/ansible/tasks/setup-pgbackrest.yml index 53b4602813..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: | @@ -43,20 +44,36 @@ - 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: - - /etc/pgbackrest/conf.d - - /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: 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 @@ -82,6 +99,14 @@ 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: |