PostgreSQL 16 Administration Cookbook 读书笔记:第6章 Security
PostgreSQL 安全性概述
PG 文档中没有专门的一章讲安全,散落在各处。可以参考老版本的文档或Security Information。
PostgreSQL 超级用户
postgres肯定是SUPERUSER:
postgres=# \du postgresList of rolesRole name | Attributes
-----------+------------------------------------------------------------postgres | Superuser, Create role, Create DB, Replication, Bypass RLS
SUPERUSER会绕过所有权限检查,除了登录权限之外。这是一种危险的权限,不应随意使用;最好以非超级用户的角色完成大部分工作。
SUPERUSER是role的属性之一,其他属性包括Create DB, Create role。
postgres=# \h create role
Command: CREATE ROLE
Description: define a new database role
Syntax:
CREATE ROLE name [ [ WITH ] option [ ... ] ]where option can be:SUPERUSER | NOSUPERUSER| CREATEDB | NOCREATEDB| CREATEROLE | NOCREATEROLE| INHERIT | NOINHERIT| LOGIN | NOLOGIN| REPLICATION | NOREPLICATION| BYPASSRLS | NOBYPASSRLS| CONNECTION LIMIT connlimit| [ ENCRYPTED ] PASSWORD 'password' | PASSWORD NULL| VALID UNTIL 'timestamp'| IN ROLE role_name [, ...]| IN GROUP role_name [, ...]| ROLE role_name [, ...]| ADMIN role_name [, ...]| USER role_name [, ...]| SYSID uidURL: https://www.postgresql.org/docs/16/sql-createrole.html
撤销用户对表的访问权限
对表有访问权限,可能是以下三种情况:
- SUPERUSER
- 表的owner
- 被赋予了表的权限
如果不是SUPERUSER,也不是表的owner,要剥夺对表的访问权限,首先可以:
REVOKE ALL ON table1 FROM username;
但这样可能还不够,如果username被赋予了其他的role,而这个role对表有访问权限,那么就必须进行一下的操作之一:
- 剥夺username的role
- 剥夺role对表的访问权限
可能用到的psql元命令:
\z
或\dp
,现实访问权限\u
,显示role
例如:
sampledb=> \conninfo
You are connected to database "sampledb" as user "sh" via socket in "/run/postgresql" at port "5432".
sampledb=> \z hr.employeesAccess privilegesSchema | Name | Type | Access privileges | Column privileges | Policies
--------+-----------+-------+-------------------+-------------------+----------hr | employees | table | hr=arwdDxt/hr +| || | | sh=r/hr | |
(1 row)sampledb=> \du shList of rolesRole name | Attributes
-----------+------------sh |
那么,sh没有赋予其他的role,直接收回权限就好了。
sampledb=> \c sampledb postgres
You are now connected to database "sampledb" as user "postgres".sampledb=# revoke all on hr.employees from sh;
REVOKE
sampledb=# \z hr.employeesAccess privilegesSchema | Name | Type | Access privileges | Column privileges | Policies
--------+-----------+-------+-------------------+-------------------+----------hr | employees | table | hr=arwdDxt/hr | |
(1 row)
几个最佳实践:
- 对于生产系统,通常最好在数据库创建脚本中始终包含 GRANT 和 REVOKE 语句,以便确保只有正确的用户才能访问该表。
- 撤销或授予权限时,应使用完全限定名称;否则,您可能会无意中使用错误的表。
构建安全的视图
视图可以实现对表权限的屏蔽,例如hr schema中有一个表employees和一个视图low_salary_emps,赋予用户sh查看视图的权限:
sampledb=> create view low_salary_emps as select * from employees where salary <= 2200;
CREATE VIEWsampledb=> grant usage on schema hr to sh;
GRANTsampledb=> grant select on low_salary_emps hr to sh;
GRANT
则用户hr可以看到视图的内容,但不能直接访问表:
sampledb=> \z hr.employees;Access privilegesSchema | Name | Type | Access privileges | Column privileges | Policies
--------+-----------+-------+-------------------+-------------------+----------hr | employees | table | hr=arwdDxt/hr | |
(1 row)sampledb=> \z hr.low_salary_emps;Access privilegesSchema | Name | Type | Access privileges | Column privileges | Policies
--------+-----------------+------+-------------------+-------------------+----------hr | low_salary_emps | view | hr=arwdDxt/hr +| || | | sh=r/hr | |
(1 row)sampledb=> select * from hr.low_salary_emps;employee_id | first_name | last_name | email | phone_number | hire_date | job_id | salary | commission_pct | manager_id | department_id
-------------+------------+------------+----------+--------------+------------+----------+---------+----------------+------------+---------------128 | Steven | Markle | SMARKLE | 650.124.1434 | 2008-03-08 | ST_CLERK | 2200.00 | | 120 | 50132 | TJ | Olson | TJOLSON | 650.124.8234 | 2007-04-10 | ST_CLERK | 2100.00 | | 121 | 50136 | Hazel | Philtanker | HPHILTAN | 650.127.1634 | 2008-02-06 | ST_CLERK | 2200.00 | | 122 | 50
(3 rows)sampledb=> select * from hr.employees;
ERROR: permission denied for table employees
但是,视图对底层表的保护不是完全的。假设用户sh创建了以下的函数(此例子来自官网):
CREATE FUNCTION tricky(text, text) RETURNS bool AS $$
BEGINRAISE NOTICE '% => %', $1, $2;RETURN true;
END;
$$ LANGUAGE plpgsql COST 0.0000000000000000000001;
执行以下的语句,即可获取底层表的信息:
sampledb=> select * from hr.low_salary_emps where tricky(first_name, last_name);
NOTICE: Steven => King
NOTICE: Neena => Kochhar
NOTICE: Lex => De Haan
NOTICE: Alexander => Hunold
NOTICE: Bruce => Ernst
NOTICE: David => Austin
NOTICE: Valli => Pataballa
NOTICE: Diana => Lorentz
NOTICE: Nancy => Greenberg
NOTICE: Daniel => Faviet
NOTICE: John => Chen
NOTICE: Ismael => Sciarra
NOTICE: Jose Manuel => Urman
NOTICE: Luis => Popp
NOTICE: Den => Raphaely
NOTICE: Alexander => Khoo
NOTICE: Shelli => Baida
NOTICE: Sigal => Tobias
NOTICE: Guy => Himuro
NOTICE: Karen => Colmenares
NOTICE: Matthew => Weiss
NOTICE: Adam => Fripp
NOTICE: Payam => Kaufling
NOTICE: Shanta => Vollman
NOTICE: Kevin => Mourgos
NOTICE: Julia => Nayer
NOTICE: Irene => Mikkilineni
NOTICE: James => Landry
NOTICE: Steven => Markle
NOTICE: Laura => Bissot
NOTICE: Mozhe => Atkinson
NOTICE: James => Marlow
NOTICE: TJ => Olson
NOTICE: Jason => Mallin
NOTICE: Michael => Rogers
NOTICE: Ki => Gee
NOTICE: Hazel => Philtanker
NOTICE: Renske => Ladwig
NOTICE: Stephen => Stiles
NOTICE: John => Seo
NOTICE: Joshua => Patel
NOTICE: Trenna => Rajs
NOTICE: Curtis => Davies
NOTICE: Randall => Matos
NOTICE: Peter => Vargas
NOTICE: John => Russell
NOTICE: Karen => Partners
NOTICE: Alberto => Errazuriz
NOTICE: Gerald => Cambrault
NOTICE: Eleni => Zlotkey
NOTICE: Peter => Tucker
NOTICE: David => Bernstein
NOTICE: Peter => Hall
NOTICE: Christopher => Olsen
NOTICE: Nanette => Cambrault
NOTICE: Oliver => Tuvault
NOTICE: Janette => King
NOTICE: Patrick => Sully
NOTICE: Allan => McEwen
NOTICE: Lindsey => Smith
NOTICE: Louise => Doran
NOTICE: Sarath => Sewall
NOTICE: Clara => Vishney
NOTICE: Danielle => Greene
NOTICE: Mattea => Marvins
NOTICE: David => Lee
NOTICE: Sundar => Ande
NOTICE: Amit => Banda
NOTICE: Lisa => Ozer
NOTICE: Harrison => Bloom
NOTICE: Tayler => Fox
NOTICE: William => Smith
NOTICE: Elizabeth => Bates
NOTICE: Sundita => Kumar
NOTICE: Ellen => Abel
NOTICE: Alyssa => Hutton
NOTICE: Jonathon => Taylor
NOTICE: Jack => Livingston
NOTICE: Kimberely => Grant
NOTICE: Charles => Johnson
NOTICE: Winston => Taylor
NOTICE: Jean => Fleaur
NOTICE: Martha => Sullivan
NOTICE: Girard => Geoni
NOTICE: Nandita => Sarchand
NOTICE: Alexis => Bull
NOTICE: Julia => Dellinger
NOTICE: Anthony => Cabrio
NOTICE: Kelly => Chung
NOTICE: Jennifer => Dilly
NOTICE: Timothy => Gates
NOTICE: Randall => Perkins
NOTICE: Sarah => Bell
NOTICE: Britney => Everett
NOTICE: Samuel => McCain
NOTICE: Vance => Jones
NOTICE: Alana => Walsh
NOTICE: Kevin => Feeney
NOTICE: Donald => OConnell
NOTICE: Douglas => Grant
NOTICE: Jennifer => Whalen
NOTICE: Michael => Hartstein
NOTICE: Pat => Fay
NOTICE: Susan => Mavris
NOTICE: Hermann => Baer
NOTICE: Shelley => Higgins
NOTICE: William => Gietzemployee_id | first_name | last_name | email | phone_number | hire_date | job_id | salary | commission_pct | manager_id | department_id
-------------+------------+------------+----------+--------------+------------+----------+---------+----------------+------------+---------------128 | Steven | Markle | SMARKLE | 650.124.1434 | 2008-03-08 | ST_CLERK | 2200.00 | | 120 | 50132 | TJ | Olson | TJOLSON | 650.124.8234 | 2007-04-10 | ST_CLERK | 2100.00 | | 121 | 50136 | Hazel | Philtanker | HPHILTAN | 650.127.1634 | 2008-02-06 | ST_CLERK | 2200.00 | | 122 | 50
(3 rows)
这是由于tricky函数的成本非常低,因此planner选择优先执行他。即使用户被禁止定义新函数,内置函数也可用于类似的攻击。
要避免此问题,需要重新建立视图:
create or replace view low_salary_emps WITH (security_barrier) as select * from employees where salary <= 2200;
这回没有问题了:
sampledb=> select * from hr.low_salary_emps where tricky(first_name, last_name);
NOTICE: Steven => Markle
NOTICE: TJ => Olson
NOTICE: Hazel => Philtankeremployee_id | first_name | last_name | email | phone_number | hire_date | job_id | salary | commission_pct | manager_id | department_id
-------------+------------+------------+----------+--------------+------------+----------+---------+----------------+------------+---------------128 | Steven | Markle | SMARKLE | 650.124.1434 | 2008-03-08 | ST_CLERK | 2200.00 | | 120 | 50132 | TJ | Olson | TJOLSON | 650.124.8234 | 2007-04-10 | ST_CLERK | 2100.00 | | 121 | 50136 | Hazel | Philtanker | HPHILTAN | 650.127.1634 | 2008-02-06 | ST_CLERK | 2200.00 | | 122 | 50
(3 rows)
当视图需要提供行级安全性时,应为视图应用 security_barrier 属性。这可以防止恶意选择的函数和操作符在视图完成其工作之前从行传递值。
授予用户对表的访问权限
需要两个权限:
- 对表所在的schema的USAGE权限
- 对表的操作权限
也可以间接赋权,即先赋予权限给GROUP role,然后将GROUP role的权限赋予USER role。
授予用户对特定列的访问权限
语法为:
GRANT privileges (col1, col2, ... colN) ON tablename TO role;
注意,如果赋予了表的某权限,则拥有表的当前和未来所有列的某权限。
授予用户对特定行的访问权限
需要启用RLS(Row Level Security)并创建RLS policy,详见这里。
创建新用户
使用createuser命令行或CREATE USER SQL命令。
$ createuser --interactive
Enter name of role to add: amy
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
[postgres@ol9-vagrant test]$ psql
psql (16.9)
Type "help" for help.Try \? for help.
postgres=# \du amyList of rolesRole name | Attributes
-----------+------------amy |
暂时阻止用户连接
ALTER USER role_specification LOGIN | NOLOGIN;
也可以现在用户的并发连接数,默认是-1,即无限制:
ALTER USER role_specification CONNECTION LIMIT connlimit;
默认所有用户对数据库有 CONNECT 权限,除非被显式收回。
REVOKE CONNECT ON DATABASE dbname FROM username;
检查权限,详见这里:
sampledb=# SELECT has_database_privilege('hr', 'sampledb', 'CONNECT');has_database_privilege
------------------------t
(1 row)
用户改为NOLOGIN后,强制终止现有连接。其实也可用于可以登录的用户:
SELECTpg_terminate_backend(pid)
FROMpg_stat_activity aJOIN pg_roles r ON a.usename = r.rolnameAND NOT rolcanlogin;
删除用户但不删除其数据
用户如果其下有对象,是不能直接删除的。
sampledb=# drop user hr;
ERROR: role "hr" cannot be dropped because some objects depend on it
DETAIL: owner of schema hr
owner of function hr.add_job_history(numeric,date,date,character varying,numeric)
owner of function hr.secure_dml()
...
可以将其下的对象转到其他用户:
REASSIGN OWNED BY hr TO new_owner;
也可以先删除其下所有对象,再删用户(不建议):
DROP OWNED BY hr;
DROP USER hr;
检查所有用户是否拥有安全密码
口令加密应为SCRAM-SHA-256,而非MD5:
SELECTusename,passwd
FROMpg_shadow
WHEREpasswd NOT LIKE 'SCRAM%'OR passwd IS NULL;
也可参考这里,这是一个口令复杂度验证函数,不能单独调用。
授予特定用户的有限超级用户权限
如果赋予了CREATE ROLE权限,那么即使没有CREATE DATABASE 权限,他也可以创建一个具有CREATE DATABASE权限的role。
还有一个函数的权限,详见这里:
SECURITY INVOKER indicates that the function is to be executed with the privileges of the user that calls it. That is the default. SECURITY DEFINER specifies that the function is to be executed with the privileges of the user that owns it.
PG还有很多预先定义好的role,详见Predefined Roles。
审计数据库访问
对表和列的权限详见Access Privilege Inquiry Functions中的has_table_privilege和has_column_privilege 。
审计可以用log_statement配置参数和pgaudit扩展。
也可参考博客Learning PostgresSQL读书笔记: 第14章 Logging and Auditing。
还有一个audit-trigger,利用触发器实现的,可参考Audit_trigger_91plus,91plus表示9.1版本以上支持。
了解当前登录用户
postgres=# select current_user, session_user;current_user | session_user
--------------+--------------postgres | postgres
(1 row)postgres=# set role hr;
SET
postgres=> select current_user, session_user;current_user | session_user
--------------+--------------hr | postgres
(1 row)postgres=> reset role;
RESET
postgres=# select current_user, session_user;current_user | session_user
--------------+--------------postgres | postgres
(1 row)
与 LDAP 集成
这个话题较大,参考这里和这里。
使用加密连接(SSL/GSSAPI)
略,过程较复杂,涉及配置参数和pg_hba.conf,还有证书和SSL key。
postgres=# show ssl
ssl ssl_crl_dir ssl_key_file ssl_passphrase_command
ssl_ca_file ssl_crl_file ssl_library ssl_passphrase_command_supports_reload
ssl_cert_file ssl_dh_params_file ssl_max_protocol_version ssl_prefer_server_ciphers
ssl_ciphers ssl_ecdh_curve ssl_min_protocol_version
使用 SSL 证书进行身份验证
略。
将外部用户名映射到数据库角色
略,详见User Name Maps。
使用列级加密
即pgcrypto,这是个函数,和Oracle原生的TDE不同,意味着还是要改应用。
使用预定义角色设置云安全
略。