4.6 Ensure the set_user extension is installed

Information

PostgreSQL access to the superuser database role must be controlled and audited to prevent unauthorized access.
Rationale:
Even when reducing and limiting the access to the superuser role as described earlier in this benchmark, it is still difficult to determine who accessed the superuser role and what actions were taken using that role. As such, it is ideal to prevent anyone from logging in as the superuser and forcing them to escalate their role. This model is used at the OS level by the use of sudo and should be emulated in the database. The set_user extension allows for this setup.

Solution

At the time this benchmark is being written, set_user is not available as a package in the PGDG repository. As such, we will build it from source:
$ whoami
root
$ yum -y install postgresql96-devel
Loaded plugins: fastestmirror
Setting up Install Process
Loading mirror speeds from cached hostfile
* base: mirror.cisp.com
* extras: packages.oit.ncsu.edu
* updates: mirror.cisp.com
Resolving Dependencies
--> Running transaction check
---> Package postgresql96-devel.x86_64 0:9.6.10-1PGDG.rhel6 will be installed
--> Finished Dependency Resolution

Dependencies Resolved

===========================================================================================================
Package Arch Version Repository Size
===========================================================================================================
Installing:
postgresql96-devel x86_64 9.6.10-1PGDG.rhel6 pgdg96 1.7 M

Transaction Summary
===========================================================================================================
Install 1 Package(s)

Total download size: 1.7 M
Installed size: 7.9 M
Downloading Packages:
postgresql96-devel-9.6.10-1PGDG.rhel6.x86_64.rpm | 1.7 MB 00:01
Running rpm_check_debug
Running Transaction Test
Transaction Test Succeeded
Running Transaction
Warning: RPMDB altered outside of yum.
Installing : postgresql96-devel-9.6.10-1PGDG.rhel6.x86_64 1/1
Verifying : postgresql96-devel-9.6.10-1PGDG.rhel6.x86_64 1/1

Installed:
postgresql96-devel.x86_64 0:9.6.10-1PGDG.rhel6

Complete!
$
$ curl https://codeload.github.com/pgaudit/set_user/tar.gz/REL1_6_1 > set_user-1.6.1.tgz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 14916 0 14916 0 0 57215 0 --:--:-- --:--:-- --:--:-- 184k
$
$ tar xf set_user-1.6.1tgz
$ cd set_user-REL1_6_1
$ export PATH=/usr/pgsql-9.6/bin:$PATH
[root@centos6 set_user-REL1_6_1]# make USE_PGXS=1
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -fPIC -I. -I./ -I/usr/pgsql-9.6/include/server -I/usr/pgsql-9.6/include/internal -D_GNU_SOURCE -I/usr/include/libxml2 -I/usr/include -c -o set_user.o set_user.c
gcc -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wmissing-format-attribute -Wformat-security -fno-strict-aliasing -fwrapv -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -fPIC -L/usr/pgsql-9.6/lib -Wl,--as-needed -L/usr/lib64 -Wl,--as-needed -Wl,-rpath,'/usr/pgsql-9.6/lib',--enable-new-dtags -lm -shared -o set_user.so set_user.o
[root@centos6 set_user-REL1_6_1]# make USE_PGXS=1 install
/bin/mkdir -p '/usr/pgsql-9.6/share/extension'
/bin/mkdir -p '/usr/pgsql-9.6/share/extension'
/bin/mkdir -p '/usr/pgsql-9.6/lib'
/usr/bin/install -c -m 644 'set_user.h' /usr/pgsql-9.6/include
/usr/bin/install -c -m 644 .//set_user.control '/usr/pgsql-9.6/share/extension/'
/usr/bin/install -c -m 644 .//set_user--1.6.sql .//set_user--1.5--1.6.sql .//set_user--1.4--1.5.sql .//set_user--1.1--1.4.sql .//set_user--1.0--1.1.sql '/usr/pgsql-9.6/share/extension/'
/usr/bin/install -c -m 755 set_user.so '/usr/pgsql-9.6/lib/'
Now that set_user is installed, we need to tell PostgreSQL to load its library:
$ whoami
root
$ vi ~postgres/9.6/data/postgresql.conf
# load set_user libs before anything else
shared_preload_libraries = 'set_user, other_libs'
$ service postgresql-9.6 restart
Stopping postgresql-9.6 service: [ OK ]
Starting postgresql-9.6 service: [ OK ]
And now, we can install the extension from with SQL:
postgres=# select * from pg_available_extensions where name = 'set_user';
name | default_version | installed_version | comment
----------+-----------------+-------------------+--------------------------------------------
set_user | 1.6 | | similar to SET ROLE but with added logging
(1 row)

postgres=# create extension set_user;
CREATE EXTENSION
postgres=# select * from pg_available_extensions where name = 'set_user';
name | default_version | installed_version | comment
----------+-----------------+-------------------+--------------------------------------------
set_user | 1.6 | 1.6 | similar to SET ROLE but with added logging
(1 row)
Now, we use GRANT to configure each DBA role to allow it to use the set_user functions. In the example below, we will configure my db user doug. (You would do this for each DBA's normal user role.)
postgres=# grant execute on function set_user(text) to doug;
GRANT
postgres=# grant execute on function set_user_u(text) to doug;
GRANT
Connect to PostgreSQL as yourself and verify it works as expected:
$ whoami
psql
$ psql -U doug -d postgres
postgres=> select set_user('postgres');
ERROR: switching to superuser not allowed
HINT: Use 'set_user_u' to escalate.
postgres=> select set_user_u('postgres');
set_user_u
------------
OK
(1 row)

postgres=# select current_user, session_user;
current_user | session_user
--------------+--------------
postgres | doug
(1 row)

postgres=# select reset_user();
reset_user
------------
OK
(1 row)

postgres=> select current_user, session_user;
current_user | session_user
--------------+--------------
doug | doug
(1 row)
Once all DBA's normal user accounts have been GRANTed permission, revoke the ability to login as the postgres (superuser) user:
postgres=# alter user postgres NOLOGIN;
ALTER ROLE
Which results in:
$ psql
psql: FATAL: role 'postgres' is not permitted to log in
$ psql -U doug -d postgres
psql (9.6.10)
Make sure there are no other roles that are superuser's and can still login:
postgres=# SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin;
rolname
---------
(0 rows)
Verify there are no unprivileged roles that can login directly that are granted a superuser role even if it is multiple layers removed:
postgres=# DROP VIEW IF EXISTS roletree;
NOTICE: view 'roletree' does not exist, skipping
DROP VIEW
postgres=# CREATE OR REPLACE VIEW roletree AS
postgres-# WITH RECURSIVE
postgres-# roltree AS (
postgres(# SELECT u.rolname AS rolname,
postgres(# u.oid AS roloid,
postgres(# u.rolcanlogin,
postgres(# u.rolsuper,
postgres(# '{}'::name[] AS rolparents,
postgres(# NULL::oid AS parent_roloid,
postgres(# NULL::name AS parent_rolname
postgres(# FROM pg_catalog.pg_authid u
postgres(# LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member
postgres(# LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid
postgres(# WHERE g.oid IS NULL
postgres(# UNION ALL
postgres(# SELECT u.rolname AS rolname,
postgres(# u.oid AS roloid,
postgres(# u.rolcanlogin,
postgres(# u.rolsuper,
postgres(# t.rolparents || g.rolname AS rolparents,
postgres(# g.oid AS parent_roloid,
postgres(# g.rolname AS parent_rolname
postgres(# FROM pg_catalog.pg_authid u
postgres(# JOIN pg_catalog.pg_auth_members m on u.oid = m.member
postgres(# JOIN pg_catalog.pg_authid g on m.roleid = g.oid
postgres(# JOIN roltree t on t.roloid = g.oid
postgres(# )
postgres-# SELECT
postgres-# r.rolname,
postgres-# r.roloid,
postgres-# r.rolcanlogin,
postgres-# r.rolsuper,
postgres-# r.rolparents
postgres-# FROM roltree r
postgres-# ORDER BY 1;
CREATE VIEW
postgres=# SELECT
postgres-# ro.rolname,
postgres-# ro.roloid,
postgres-# ro.rolcanlogin,
postgres-# ro.rolsuper,
postgres-# ro.rolparents
postgres-# FROM roletree ro
postgres-# WHERE (ro.rolcanlogin AND ro.rolsuper)
postgres-# OR
postgres-# (
postgres(# ro.rolcanlogin AND EXISTS
postgres(# (
postgres(# SELECT TRUE FROM roletree ri
postgres(# WHERE ri.rolname = ANY (ro.rolparents)
postgres(# AND ri.rolsuper
postgres(# )
postgres(# );
rolname | roloid | rolcanlogin | rolsuper | rolparents
---------+--------+-------------+----------+------------
(0 rows)
If any roles are identified by this query, use REVOKE to correct.
Impact:
Much like the venerable sudo does for the OS, set_user manages superuser access for PostgreSQL. Complete configuration of set_user is documented at the extension's website and should be reviewed to ensure the logging entries that your organization cares about are properly configured.
Note that some external tools assume they can connect as the postgres user by default and this is no longer true. You may find some tools need different options, reconfigured, or even abandoned to compensate for this.

See Also

https://workbench.cisecurity.org/files/2235

Item Details

Category: ACCESS CONTROL

References: 800-53|AC-2(9), CSCv6|5.1, CSCv6|5.8, CSCv7|4.3

Plugin: PostgreSQLDB

Control ID: 746521858afac8fff8ff669b490180a1560d9d77ca1c784d7b65c4d223ddaaa0