Secure Permissions for pgBackRest
The pgBackRest tool is a fantastic backup solution for Postgres, with many features including encryption, compression, automatic expiration, PITR, asynchronous archiving, and lots more. By default it runs as the Unix user "postgres" and connects to the database as the "postgres" superuser. In working with one of our finance clients on Crunchy High Availability Postgres, we needed to limit the access of the pgBackRest program for security and compliance on the database cluster. This article describes a method to allow pgBackRest to take a backup with the minimum of access rights for other like-minded security Postgres users. Using the Principle of Least Privilege (PoLP) we can set up pgBackRest to take backups but not have write access to the database itself.
Before we start, it is helpful to know a bit about how pgBackRest operates. There are two main pieces: WAL (write-ahead log) archiving and creating backups. For the WAL archiving, the Postgres backend itself will invoke pgBackRest, and move the WAL files to one or more pgBackRest repositories. Because this runs from within Postgres itself, it already has full permissions to the WAL files, so nothing needs to change.
When pgBackRest performs a backup, it connects to the Postgres server, gathers some information, tells Postgres to start a backup, then copies the physical files inside the Postgres data directory to one or more pgBackRest repositories. Our goal in this article is to have this backup run by a low-privilege user that can read, but not write, the Postgres data directory. Additionally, we need to connect to the database but only run a few functions needed to perform the backup. The account we connect as should not be able to read any data from the database!
This example focuses on an existing Postgres database that is not using pgBackRest yet. It also assumes that the backup storage is on the same machine as Postgres, but all the information here should be easy to modify for more advanced configurations.
Overview
The process will be:
- Create a new user at the Unix level
- Change group permissions of key directories used by pgBackRest
- Create a new user at the database level
- Give this new user the ability to only run a small handful of functions
Setup
The items below should work with minimal changes if you are modifying an existing Postgres and/or pgBackRest system; for this example we will create a complete standalone system. First step is to install Postgres, create a new database cluster, and then install pgBackRest. These commands are for a Red Hat / CentOS system, so your experience may differ:
$ sudo yum install -y postgresql14-server
## Put Postgres utilities such as initdb and pg_ctl into the postgres user's path:
$ echo 'export PATH=/usr/pgsql-14/bin/:$PATH' | sudo -iu postgres tee -a .bash_profile
$ sudo -iu postgres initdb -k
$ sudo yum install -y pgbackrest
Creating a local user to perform the backups
A package install of pgBackRest creates everything owned as either root or as the "postgres" user. We can use this to our advantage by creating a new unprivileged user that, like the "postgres" user, will belong to the "postgres" group:
sudo useradd pgbackrest --gid postgres_backup --create-home
If the backups are going to run from another server, we will need to create a pair of SSH keys. While these are not needed for this particular example, let's create some anyway:
sudo -u pgbackrest ssh-keygen -q -t ed25519 -N "" -C "pgbackrest key"
Adjust the Postgres data directory
The next step is to make sure this new user has read-only access to the entire Postgres data directory, as backups involve copying these files somewhere else. We also are going to make use of the Unix setgid feature to ensure that any files created in the future are also readable by our new "pgbackrest" user. First, let's check what the Postgres data directory permissions are:
$ sudo -iu postgres psql -tc 'show data_directory'
/var/lib/pgsql/14/data
$ sudo ls -la /var/lib/pgsql/14
total 4
drwx------. 1 postgres postgres 32 Jan 1 13:21 .
drwx------. 1 postgres postgres 32 Jan 1 13:21 ..
drwx------. 20 postgres postgres 4096 Jan 1 13:21 data
We want to make this directory, and all directories underneath it, readable and searchable for our new user. We do this by using the chmod command to grant group read (r), group search (x), and group setgid (s) for every directory:
sudo find /var/lib/pgsql/14/data -type d -exec chmod g+rxs {} \;
We also need to make sure that the complete path to the data directory is
searchable by anyone in the "postgres" group. While we could apply the chmod
above to /var/lib/pgsql/
, let's keep any setgid to the data directory, and
apply the read/search changes per directory:
sudo chmod g+rx /var/lib/pgsql /var/lib/pgsql/14
That takes care of the directories, but we also need to make sure all the files are group readable:
sudo find /var/lib/pgsql/14/data -type f -exec chmod g+r {} \;
That "s" in the g+rxs (also known as the setgid bit) ensures that any files created in those directories will be readable by the "postgres" group. This will not take effect for any processes that are already running and happen to have opened a directory. So, unless Postgres is already stopped, it will need restarting, and the permission changes applied again:
$ sudo -iu postgres /usr/pgsql-14/bin/pg_ctl restart
## Run these until both return an error from xargs about 'missing operand':
sudo -u pgbackrest find /var/lib/pgsql/14/data -not -readable -type d | xargs -n1 sudo chmod g+rxs
sudo -u pgbackrest find /var/lib/pgsql/14/data -not -readable -type f | xargs -n1 sudo chmod g+r
Let's make sure our new backup user can now read, but not write, to the files in the data directory:
$ sudo -u pgbackrest cat /var/lib/pgsql/14/data/postmaster.pid
29327
/var/lib/pgsql/14/data
1641332824
5432
/var/run/postgresql
localhost
16810335 6
ready
$ sudo -u pgbackrest touch /var/lib/pgsql/14/data/postmaster.pid
touch: cannot touch '/var/lib/pgsql/14/data/postmaster.pid': Permission denied
Adjust the pgBackRest lock directory
To prevent multiple backrest processes from stepping on each other's toes,
backrest implements a simple locking scheme, involving writing files in a common
directory, by default /tmp/pgbackrest
. Our new user needs to be able to create
files in this directory, and should be able to read files created by others. If
pgBackRest has never run, it is possible this directory does not exist yet, so
we'll add some code to create it just in case. Then we'll adjust the
permissions:
sudo mkdir /tmp/pgbackrest/
sudo chown postgres.postgres /tmp/pgbackrest/
sudo chmod g+rwxs /tmp/pgbackrest/
sudo find /tmp/pgbackrest/ -type f -exec chmod g+r {} \;
(Savvy users of pgBackRest may wonder about the spool-path. Because those files are not needed for backups, no special permission changes are needed for it.)
Adjust the pgBackRest configuration files
Both the "postgres" user, and this new backup-only user, need to be able to read
from the main pgBackRest configuration files, which by default are located in
/etc/pgbackrest/
, so let's adjust those as well. The new user has no need for
write access.
## Create this just in case it does not exist:
$ sudo mkdir /etc/pgbackrest/
$ sudo chown postgres.postgres /etc/pgbackrest/
$ sudo find /etc/pgbackrest/ -type d -exec chmod g+rxs {} \;
$ sudo find /etc/pgbackrest/ -type f -exec chmod g+r {} \;
## This file may also be in use, so adjust permissions if needed
## (this file may be root owned and mode 0644)
$ sudo -iu pgbackrest find /etc/pgbackrest.conf3 -not -readable | xargs sudo chmod g+r
Adjust the pgBackRest logging directory
Finally, this new user needs read and write access to the logging directory for pgBackRest, if file logging is in use:
## As before, create if it does not exist:
$ sudo mkdir -p /var/log/pgbackrest
$ sudo chown postgres.postgres /var/log/pgbackrest
$ sudo chmod g+rwxs /var/log/pgbackrest/
$ sudo find /var/log/pgbackrest/ -type f -exec chmod g+wr {} \;
Adjust the pgBackRest repository
The repository is the place where pgBackRest stores its backups, as well as
where is stores the WAL files that are created by Postgres. For this article,
our repository will be on the same server as Postgres, but the process is very
similar if performing backups from a remote server (a better option!). The
default location is /var/lib/pgbackrest
, so let's tweak the permissions there:
sudo find /var/lib/pgbackrest/ -type d -exec chmod g+rwxs {} \;
sudo find /var/lib/pgbackrest/ -type f -exec chmod g+r {} \;
Create a backrest stanza if needed
If you don't already have a stanza, create one now:
$ echo '[foobar]' | sudo tee -a /etc/pgbackrest/pgbackrest.conf
$ echo 'pg1-path=/var/lib/pgsql/14/data' | sudo tee -a /etc/pgbackrest/pgbackrest.conf
$ echo 'start-fast=y' | sudo tee -a /etc/pgbackrest/pgbackrest.conf
$ sudo -u postgres /bin/pgbackrest stanza-create --stanza foobar
## Make pgbackrest the owner of the backups directory:
$ sudo chown -R pgbackrest /var/lib/pgbackrest/backup
We can see the new stanza here with the correct permissions:
$ sudo find /var/lib/pgbackrest/ -ls
89111117 0 drwxrws--- 4 postgres postgres 35 Jan 30 00:43 /var/lib/pgbackrest/
32100105 0 drwxr-s--- 3 postgres postgres 20 Jan 30 00:43 /var/lib/pgbackrest/archive
11599111 0 drwxr-s--- 2 postgres postgres 51 Jan 30 00:43 /var/lib/pgbackrest/archive/foobar
11810111 4 -rw-r----- 1 postgres postgres 253 Jan 30 00:43 /var/lib/pgbackrest/archive/foobar/archive.info
43297321 4 -rw-r----- 1 postgres postgres 253 Jan 30 00:43 /var/lib/pgbackrest/archive/foobar/archive.info.copy
14101100 0 drwxr-s--- 3 pgbackrest postgres 20 Jan 30 00:43 /var/lib/pgbackrest/backup
32104101 0 drwxr-s--- 2 pgbackrest postgres 49 Jan 30 00:43 /var/lib/pgbackrest/backup/foobar
11411410 4 -rw-r----- 1 pgbackrest postgres 370 Jan 30 00:43 /var/lib/pgbackrest/backup/foobar/backup.info
5110103 4 -rw-r----- 1 pgbackrest postgres 370 Jan 30 00:43 /var/lib/pgbackrest/backup/foobar/backup.info.copy
Note that the new user does not need write access to the WAL files (which exist in the 'archive' directory), so you could do this if wanted:
sudo find /var/lib/pgbackrest/archive -exec chmod g-w {} \;
File permissions summary
Here's a summary of all the file permissions we need to set to have a second user perform backups using pgBackRest:
Item | Default | Config setting name | Group permissions |
---|---|---|---|
Backrest repository (archive) | /var/lib/pgbackrest/archive | repo1-path | Read only |
Backrest repository (backup) | /var/lib/pgbackrest/backup | repo1-path | Read and write |
Configuration files | /etc/pgbackrest | built-in | Read only |
Locking | /tmp/pgbackrest | lock-path | Read and write |
Logging | /var/log/pgbackrest | log-path | Write only* |
Spool for async WAL push | /var/spool/pgbackrest | spool-path | None |
Postgres data directory | Varies:SHOW data_directory |
pg1-path | Read only |
Postgres logs | Varies, often $DATADIR/log | N/A | Read (not needed but nice to have) |
* Not needed if log-level-file is set to off
Create a regular Postgres database user
Now that the file permissions are all in place, we need to create an account inside of Postgres itself. We want a regular, non-superuser account that has minimal privileges. To keep things simple, we will call this user "backrest".
If you are following along, give it a good password. Here's one way to make one:
$ dd if=/dev/urandom count=1 status=none | md5sum | awk '{print$1}' | tee mypass
1560a2dff5992750d9748cbda44b4c51
Create the new Postgres user, assign it the password generated above, then put that password into the "pgpass" file for the Unix user "pgbackrest":
$ sudo -iu postgres createuser backrest --pwprompt
## (enter password twice)
$ echo *:*:postgres:backrest:$(cat mypass) | sudo -iu pgbackrest tee -a .pgpass
$ sudo -iu pgbackrest chmod 600 .pgpass
Restrict what this new database user can do
We only want this new database user to connect to the "postgres" database and nowhere else, so we need to add these lines to the pg_hba.conf file, making sure they appear before any other "local" lines:
local postgres backrest scram-sha-256
local all backrest reject
Alas, there is currently no equivalent to ALTER SYSTEM for the pg_hba.conf file, but a little command-line trickery gets the job done:
$ sudo -iu postgres bash -c \
'sed -i "1i local postgres backrest scram-sha-256 \nlocal all backrest reject" $(psql -Atc "show hba_file")'
$ sudo -iu postgres psql -c 'select pg_reload_conf()'
Let's make sure the rules are in there:
$ sudo -iu postgres psql -c 'select * from pg_hba_file_rules limit 2'
line_number | type | database | user_name | address | netmask | auth_method | options | error
-------------+-------+------------+------------+---------+---------+---------------+---------+-------
1 | local | {postgres} | {backrest} | | | scram-sha-256 | |
2 | local | {all} | {backrest} | | | reject | |
(2 rows)
Even though this user can only connect to a single database, let's further limit what it can do by revoking all access to the 'public' schema:
sudo -iu postgres psql -c 'revoke all on schema public from backrest'
Grant permission to the 'backup' command
To be able to run backups, the new database user will need access to one role, and two functions:
$ sudo -iu postgres psql \
-c 'grant pg_read_all_settings to pgbackrest' \
-c 'grant execute on function pg_start_backup to pgbackrest' \
-c 'grant execute on function pg_stop_backup(bool,bool) to pgbackrest'
Our sample database is not archiving WAL file via pgBackRest yet, so let's add that in place now:
sudo -iu postgres psql -c "alter system set archive_mode=on"
sudo -iu postgres psql -c "alter system set archive_command='pgbackrest --stanza=foobar archive-push %p'"
sudo -iu postgres /usr/pgsql-14/bin/pg_ctl restart
Grant permission to the 'check' command
We can in theory do a backup now, but how about the pgBackRest "check" command?
$ sudo --user pgbackrest -i /bin/pgbackrest --stanza=foobar --log-level-console=detail check
ERROR: [057]: unable to execute query 'select pg_catalog.pg_create_restore_point('pgBackRest Archive Check')::text':
ERROR: permission denied for function pg_create_restore_point
Well, it turns out that the check command, and only the check command, requires permission for two more database functions:
$ sudo -iu postgres psql \
-c 'grant execute on function pg_create_restore_point to pgbackrest' \
-c 'grant execute on function pg_switch_wal to pgbackrest'
The check command is working as expected now:
$ sudo -iu pgbackrest /bin/pgbackrest --stanza foobar check --log-level-console=detail
2022-01-01 03:19:22.033 P00 INFO: check command begin 2.36: --exec-id=9921-e64ad021 --log-level-console=detail
--pg1-path=/var/lib/pgsql/14/data --stanza=foobar
2022-01-01 03:19:22.011 P00 INFO: check repo1 configuration (primary)
2022-01-01 03:19:22.005 P00 INFO: check repo1 archive for WAL (primary)
2022-01-01 03:19:22.025 P00 INFO: WAL segment 000000010000000000000002 successfully archived to
'/var/lib/pgbackrest/archive/foobar/14-1/0000000100000000
/000000010000000000000002-6273e062555e65ea850137e743f73fe941746F5A.gz' on repo1
2022-01-01 03:19:22.033 P00 INFO: check command end: completed successfully (1321ms)
Create a backup
Now that the check command is working, let's take our first backup!
$ sudo -iu pgbackrest /bin/pgbackrest --stanza=foobar --log-level-console=info backup
2022-01-01 03:19:23.116 P00 INFO: backup command begin 2.36: --exec-id=9996-6beecee3 --log-level-console=info
--pg1-path=/var/lib/pgsql/14/data --stanza=foobar --start-fast
WARN: option 'repo1-retention-full' is not set for 'repo1-retention-full-type=count', the repository may run out of space
HINT: to retain full backups indefinitely (without warning), set option 'repo1-retention-full' to the maximum.
WARN: no prior backup exists, incr backup has been changed to full
2022-01-01 03:19:23.104 P00 INFO: execute non-exclusive pg_start_backup(): backup begins after the requested immediate checkpoint completes
2022-01-01 03:19:23.101 P00 INFO: backup start archive = 000000010000000000000005, lsn = 0/5000028
2022-01-01 03:19:23.102 P00 INFO: execute non-exclusive pg_stop_backup() and wait for all WAL segments to archive
2022-01-01 03:19:23.117 P00 INFO: backup stop archive = 000000010000000000000005, lsn = 0/5003EE8
2022-01-01 03:19:23.108 P00 INFO: check archive for segment(s) 000000010000000000000005:000000010000000000000005
2022-01-01 03:19:23.108 P00 INFO: new backup label = 20220101-031923F
2022-01-01 03:19:23.116 P00 INFO: full backup size = 25.8MB, file total = 952
2022-01-01 03:19:23.105 P00 INFO: backup command end: completed successfully (4451ms)
2022-01-01 03:19:23.116 P00 INFO: expire command begin 2.36: --exec-id=9996-6beecee3 --log-level-console=info --stanza=foobar
2022-01-01 03:19:23.108 P00 INFO: option 'repo1-retention-archive' is not set - archive logs will not be expired
2022-01-01 03:19:23.101 P00 INFO: expire command end: completed successfully (6ms)
Those two warnings are not important for now. Let's check that the backup looks complete by using the pgBackRest "info" command:
Those two warnings are not important for now. Let's check that the backup looks complete by using the pgBackRest "info" command:
$ sudo -iu pgbackrest /bin/pgbackrest info
stanza: foobar
status: ok
cipher: none
db (current)
wal archive min/max (14): 000000010000000000000001/000000010000000000000007
full backup: 20220101-031923F
timestamp start/stop: 2022-01-01 03:19:23.000 / 2022-01-01 03:19:23.999
wal start/stop: 000000010000000000000003 / 000000010000000000000003
database size: 25.8MB, database backup size: 25.8MB
repo1: backup set size: 3.2MB, backup size: 3.2MB
Database permissions summary
Here's a summary of all the Postgres database permissions we need to use pgBackRest:
Item | Type | Needed for |
---|---|---|
pg_read_all_settings | role | backup |
pg_start_backup | function | backup |
pg_stop_backup | function | backup |
pg_create_restore_point | function | check |
pg_switch_wal | function | check |
That's it! We used the power of Unix groups, and selective EXECUTE privileges on a handful of functions, to make a user that can create Postgres backups through the pgBackRest program with the least permissions possible.
Final notes
- This new user should not do restores - for that, use the "postgres" user.
- Anything that may create new Postgres clusters (e.g. with
initdb
) will need to ensure that the new permissions in the data directory are set. For Patroni, a quick shell script attached to the "on_start" hook will suffice. - Using S3, GCS, or Azure (which are all supported by pgBackRest) will require further tweaks.
- If you tie this in with TDE (transparent date encryption), then you will have "blind" backups in which the files you are backing up are encrypted and cannot be decrypted by your backup user.
- For further Least Privilege considerations, ask about the Crunchy Hardened Superuser Lockdown feature.