set_user
1. 概述
set_user 是 PostgreSQL 安全审计扩展,由 pgaudit 项目维护,优化原生用户切换能力。支持普通用户间互切,也可受控切换至超级用户;可配置白名单限定允许切换的用户,所有切换操作留存审计日志,切换到超级用户时强制完整记录 SQL,拦截修改数据库配置、调用系统命令等高风险操作,规范临时提权,部署需预加载插件并重启数据库,再创建扩展使用。
版本:REL4_2_0
开源协议:PostgreSQL License
2. 原理介绍
PostgreSQL原生的 SET ROLE / SET SESSION AUTHORIZATION 有两个安全短板:一是提权后可以随手 SET log_statement = 'none' 关掉日志、RESET ROLE 悄悄切回来,审计留不下痕迹;二是要让 DBA 能干超级用户的活,通常得给他们能直接登录 superuser 账号,权限过大且不可控。
set_user 的思路是"不禁止提权,但让提权全程留痕、无法抵赖"。部署后,所有超级用户账号可设为 NOLOGIN,DBA 以普通账号登录,需要时调用 set_user_u('postgres') 提权。从提权到 reset_user() 恢复的整个窗口内:用户切换被记入日志,log_statement 被强制改为 all 使每条 SQL 落盘,日志前缀自动追加 AUDIT 标签便于过滤告警;同时 ALTER SYSTEM、COPY PROGRAM、SET log_statement、SET ROLE 及 set_config() 后门等所有可能破坏审计或逃逸身份的通道被全部封锁。由于 session_user 始终保持真实登录者,"谁在什么时候以什么身份做了什么"在日志中一目了然。
实现上,它以 C 扩展形式通过 shared_preload_libraries 加载,核心依赖三个内核机制:ProcessUtility_hook 拦截危险语句,object_access_hook 封堵函数级后门,事务提交回调保证切换的事务安全。权限控制采用双闸设计——SQL 层的 GRANT EXECUTE 决定谁能调用,配置层的白名单(superuser_allowlist 等)可随时热调整收口。此外还提供带口令锁的 set_user(user, token) 供连接池代持连接时防逃逸,以及不可逆的 set_session_auth() 用于连接移交前的永久降权。
set_user 并不能阻止超级用户作恶——它的定位是确保任何特权操作都无法逃避审计,配合日志告警系统,构成 PostgreSQL 最小权限运维体系的关键一环。
3. 使用说明
需要将 set_user 添加到 shared_preload_libraries。如果有其他插件实现 post-execution hooks 那么需要将 set_user 列在这些插件之前。
|
superuser 账号(如 postgres)需要设为 NOLOGIN,DBA 用普通账号登录,需要时调用 set_user_u('postgres') 提权。
|
-
配置介绍
| 配置项 | 说明 | 示例 | 功能 |
|---|---|---|---|
set_user.block_alter_system |
|
|
阻止 ALTER SYSTEM 命令 |
set_user.block_copy_program |
|
|
阻止 COPY PROGRAM 命令 |
set_user.block_log_statement |
|
|
阻止修改 log_statement |
set_user.nosuperuser_target_allowlist |
通配符 '*' (默认)或字符串 |
'dba1, dba2, +admin_group' |
允许 set_user() 切换到的目标用户名单 |
set_user.superuser_allowlist |
通配符 '*' (默认)或字符串 |
'dba1, dba2, +admin_group' |
允许调用 set_user_u() 提权的用户名单 |
set_user.superuser_audit_tag |
字符串 |
'AUDIT' |
日志前缀标签 |
set_user.exit_on_error |
|
|
出现错误时是否退出当前会话 |
-
函数介绍
| 函数 | 默认权限 | 参数 | 说明 |
|---|---|---|---|
|
REMOVE FROM PUBLIC |
目标非superuser用户名 |
将当前会话的用户身份切换为指定的非superuser用户 |
|
REMOVE FROM PUBLIC |
目标非superuser用户名,随机token |
同上,但需要提供一个随机token以增加安全性 |
|
REMOVE FROM PUBLIC |
目标superuser用户名 |
将当前会话的用户身份切换为指定的superuser用户 |
|
GRANT TO PUBLIC |
无 |
将当前会话的用户身份恢复为原始用户 |
|
GRANT TO PUBLIC |
随机token |
带口令恢复,但是需要提供对应的token |
|
REMOVE FROM PUBLIC |
目标非superuser用户名 |
类似 |
4. 安装
| 源码测试安装环境为 Ubuntu 26.04。需要安装IvorySQL,具体安装步骤请参考:https://github.com/IvorySQL/IvorySQL |
4.1. 源码安装
-
设置IvorySQL安装路径
export IVY_BIN_DIR=/usr/local/ivorysql/bin
export TEST_DB_DIR=/tmp/test_db
-
获取源码
git clone https://github.com/pgaudit/set_user.git
cd set_user
git checkout REL4_2_0
-
编译安装
make USE_PGXS=1 PG_CONFIG=${IVY_BIN_DIR}/pg_config clean
make USE_PGXS=1 PG_CONFIG=${IVY_BIN_DIR}/pg_config
make USE_PGXS=1 PG_CONFIG=${IVY_BIN_DIR}/pg_config install
-
配置
shared_preload_libraries = 'set_user'
4.2. 创建测试数据库
需要将 set_user 添加到 shared_preload_libraries。
|
-
初始化数据库
# 初始化数据库
${IVY_BIN_DIR}/initdb -U postgres -D ${TEST_DB_DIR}
# 将 set_user 添加到 shared_preload_libraries 中,也可以手动配置
sed -i "s/\(shared_preload_libraries = '.*\)'/\1, set_user'/g" ${TEST_DB_DIR}/ivorysql.conf
# 修改PG端口和Oracle兼容端口,避免冲突
cat << EOF >> ${TEST_DB_DIR}/ivorysql.conf
ivorysql.port = 1522
port = 5433
EOF
# 启动测试数据库
${IVY_BIN_DIR}/pg_ctl -D ${TEST_DB_DIR} -l ${TEST_DB_DIR}/logfile start
4.3. 使用测试
-
连接数据库
PGHOST=127.0.0.1 PGPORT=1522 PGUSER=postgres $IVY_BIN_DIR/psql
-
测试示例
| 这里使用默认配置,即白名单为 '*。 |
CREATE EXTENSION set_user;
LOAD 'set_user';
--- 创建两个用户
CREATE USER dba;
CREATE USER bob;
-- 让 dba 可以执行 set_user()
GRANT EXECUTE ON FUNCTION set_user(text) TO dba;
GRANT EXECUTE ON FUNCTION set_user(text,text) TO dba;
GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba;
-- 切换到用户 dba,模拟dba登录
SET SESSION AUTHORIZATION dba;
SELECT SESSION_USER, CURRENT_USER;
--- session_user | current_user
--- --------------+--------------
--- dba | dba
--- (1 row)
--- 提权, 这里使用 set_user() 提权失败,因为目标用户是 superuser
SELECT set_user('postgres');
--- ERROR: switching to superuser not allowed
--- HINT: Use 'set_user_u' to escalate.
--- 提权, 这里使用 set_user_u() 提权成功
SELECT set_user_u('postgres');
--- set_user_u
--- ------------
--- OK
--- (1 row)
--- 查看提权后的用户状态
SELECT SESSION_USER, CURRENT_USER;
--- session_user | current_user
--- --------------+--------------
--- dba | postgres
--- (1 row)
--- 测试重复 set_user,会失败
SELECT set_user('bob');
--- ERROR: must reset previous user prior to setting again
--- 提权中,测试 ALTER SYSTEM 会失败
ALTER SYSTEM SET wal_level = minimal;
--- ERROR: ALTER SYSTEM blocked by set_user config
--- 提权中,测试 COPY PROGRAM 会失败
COPY (select 42) TO PROGRAM 'cat';
--- ERROR: COPY PROGRAM blocked by set_user config
--- 提权中,测试 SET log_statement 会失败
SET log_statement = 'none';
--- ERROR: "SET log_statement" blocked by set_user config
--- 恢复初始用户
SELECT reset_user();
SELECT SESSION_USER, CURRENT_USER;
--- session_user | current_user
--- --------------+--------------
--- dba | dba
--- (1 row)
--- 切换到普通用户bob
SELECT set_user('bob');
SELECT SESSION_USER, CURRENT_USER;
--- session_user | current_user
--- --------------+--------------
--- dba | bob
--- (1 row)
--- 恢复初始用户
SELECT reset_user();
SELECT SESSION_USER, CURRENT_USER;
--- session_user | current_user
--- --------------+--------------
--- dba | dba
--- (1 row)
查看 logfile 能看到提权后的所有操作都被记录了下来,并且日志前缀带有 AUDIT 标签。每个用户切换都有一条: LOG: XXX transitioning to YYY 对应。
2026-07-17 13:55:34.905 CST [2670610] LOG: Role dba transitioning to Superuser Role postgres
2026-07-17 13:55:34.905 CST [2670610] STATEMENT: SELECT set_user_u('postgres');
2026-07-16 16:01:21.193 CST [2644289] AUDIT: LOG: statement: SELECT SESSION_USER, CURRENT_USER;
2026-07-16 16:01:43.825 CST [2644289] AUDIT: LOG: statement: SELECT set_user('bob');
2026-07-16 16:01:43.829 CST [2644289] AUDIT: ERROR: must reset previous user prior to setting again
2026-07-16 16:01:43.829 CST [2644289] AUDIT: STATEMENT: SELECT set_user('bob');
2026-07-16 16:02:20.177 CST [2644289] AUDIT: LOG: statement: ALTER SYSTEM SET wal_level = minimal;
2026-07-16 16:02:20.177 CST [2644289] AUDIT: ERROR: ALTER SYSTEM blocked by set_user config
2026-07-16 16:02:20.177 CST [2644289] AUDIT: STATEMENT: ALTER SYSTEM SET wal_level = minimal;
2026-07-17 13:56:26.924 CST [2670610] STATEMENT: SELECT set_user_u('postgres');
2026-07-17 14:01:12.017 CST [2670610] AUDIT: LOG: statement: select reset_user();
2026-07-17 14:01:12.017 CST [2670610] AUDIT: LOG: Superuser Role postgres transitioning to Role dba
2026-07-17 14:18:42.884 CST [2671784] LOG: Role dba transitioning to Role bob
2026-07-17 14:18:42.884 CST [2671784] STATEMENT: select set_user('bob');
2026-07-17 14:18:53.301 CST [2671784] LOG: Role bob transitioning to Role dba
2026-07-17 14:18:53.301 CST [2671784] STATEMENT: select reset_user();
2026-07-17 14:26:16.737 CST [2671784] LOG: Role dba transitioning to Role bob
2026-07-17 14:26:16.737 CST [2671784] STATEMENT: SELECT set_user('bob');