PostgreSQL 安全深潜:从架构沙盒到行级细粒度控制的 360 度防线 Peter Pang 2026-04-10

PostgreSQL:极致安全的设计理念与行业现状

PostgreSQL 被誉为世界上最安全的系统,这一评价并非夸大其词,它远超一般数据库系统的范畴,而是指其整体系统的安全性达到了顶尖水平。其安全机制深入到每一个角落,能够将安全保障落实到数据反馈的每一行、每一列,乃至每一个独立的单元格。

然而,在深入探讨 PostgreSQL 的强大安全功能之前,我们不得不先正视一个更为严峻的问题:在整个计算机行业中,对于数据库安全这一关键环节,普遍存在着严重的重视缺失。正如著名程序员 Atomic Energy 所言:“系统安全的本质,在于数据读写的安全。” 回顾历史,我们可以将所有的安全事故大致归类为三种:

  1. 机密信息泄露(READ): 这类事故层出不穷,其数据量之大,甚至可以说支撑了半个暗网的运作。
  2. 系统不可逆破坏(WRITE): 与其他程序被破坏后可重启不同,数据库作为系统的“唯一真相来源”(single point of truth),一旦数据被篡改或删除,后果往往是无法挽回的。2017 年的 GitLab 事故便是惨痛的例证:在一系列令人啼笑皆非的“究极草台班子操作”后,GitLab 程序员意外删除了其 PostgreSQL 数据库中高达 300GB 的用户数据,且未能恢复。
  3. 系统功能受阻(DOWN): 这类事故,如勒索软件攻击和 DDoS 攻击,更多地归因于网络架构问题。

由此可见,2/3 的安全事故都与数据的读取(READ)和写入(WRITE)紧密相关。本质上,安全问题归结为“谁可以读取或写入什么数据”,即“身份验证和权限管理”的核心议题。

悖论:外紧内松的安全现象与多层防御的必要性

一个诡异的行业现象是:安全防护在外层越是严密,而一旦进入到数据库层面,防守就越显松懈。纵观计算机行业发展历程,我们看到前端安全从最初的“四处漏风的筛子”演变为如今固若金汤的沙盒环境。在前后端通信领域,SSL/TLS 协议的不断升级和 HTTPS 的普及,也使得通信安全达到了新的高度。后端安全架构更是百花齐放,直接暴露 IP 和 SSH 端口的时代已成过去。

然而,当数据最终抵达数据库这一终点时,安全画风却陡然变得潦草。许多大型项目,后端直接使用 admin 账号直连数据库,几乎没有任何额外的防御措施。即便有所谓的“安全”措施,也仅是将 admin 账号的密码通过加密手段嵌入后端代码中。

这样做的好处显而易见:减少数据库账号数量,简化管理;简化应用层代码编写;并在性能上有所优势(后续章节将详述)。然而,这种做法背后更深层的原因,往往是一种普遍存在的“侥幸心理”。许多开发者认为,既然后端部署在受信任的内网环境中,已经具备了基础安全防护,那么后端连接数据库便如同“客厅连卧室”,无需再设置层层复杂的安全关卡。

这种侥幸心理的直接后果是:一旦有人(无论是恶意攻击者还是无意操作)突破了外层防线,便能直接掌控 admin 账号这个“核弹”,瞬间造成毁天灭地的后果。

因此,我希望唤起大家对数据库安全的重视,因为只有数据库本身的安全机制,才是抵御此类风险的最后一道、也是最坚实的防线。著名程序员 Atomic Energy 曾强调:“安全是有覆盖范围的,超出了范围一律要默认成为不安全。”

以“身份”这一数据为例,当它从浏览器沙盒环境,经过 HTTPS 加密传输到后端时,即便后端已接收到该身份信息,也应视其为“不可信”状态,并重新进行验证。这是因为每一次数据的传递,都意味着它脱离了前者的安全框架覆盖。同理,当数据库接收到任何外部请求时,无论其来源如何,只要是在数据库自身的安全范围之外,都应被一视同仁地进行全方位的身份和权限验证。这一操作逻辑,本质上与其他应用层的安全验证并无区别。

PostgreSQL 的多层级安全体系:Role、Schema、Column 与 RLS

1. 基于 Role 的安全模型与 Schema 隔离

PostgreSQL 的安全机制围绕着 Role(角色) 这一核心概念进行设计。无论是构建独立的沙盒环境,还是进行细粒度的身份与权限认证,都离不开对自定义角色的创建和管理。简而言之,Role 可以被理解为数据库的登录账号,其具体设计将在后续章节详述。

当前,我们通过一个简单的查询语句来初步了解 PostgreSQL 的安全机制:

SELECT id, name FROM users WHERE group_name = 'bilibili';

当此语句执行时,PostgreSQL 会进行一系列权限检查:首先是 Table 层面,然后是 Column 层面,最终是 Row 层面。

与其他数据库(如 MySQL)不同,PostgreSQL 拥有 Database(数据库)、Schema(模式)Table(表) 的三层结构。虽然一个事务(transaction)不能跨越数据库执行,但在同一个数据库内,可以跨越不同的 Schema 查询不同的表,Schema 之间本身并不存在严格的隔离。

那么,Schema 除了用于整理分类表(达成一种 Namespace 的效果)外,还有什么实际价值?其核心价值在于 PostgreSQL 提供的 search_path 功能。该功能允许为不同的 Role 指定不同的 Schema 可见度。例如:

  • 多部门系统: 可以让不同部门的系统只能看到各自部门的 Schema;而需要跨部门协作的系统,则可以配置为看到多个部门的 Schema。
  • 简化架构: 对于系统架构不那么复杂的场景,可以定义三个基础 Schema:
    • public Schema: 存放用户可直接查询的表。
    • private Schema: 存放账户、密码、 Session 等敏感信息的表。通过配置用户的 search_path 不包含 private Schema,用户将无法直接查询这些敏感表,只能通过固定的 Trigger 或 Function 间接获取,从而有效控制对机密信息的访问能力。
    • worker Schema: 存放与用户完全无关、仅供后台异步服务使用的表,确保其不被外部查询或副作用所干扰。

在此设计中,Schema 被巧妙地用作“沙盒”,将不同安全等级的表进行物理隔离,构成了 PostgreSQL 的第一层防御机制。

2. Column 级权限控制:最小权限原则

Schema 权限检查通过后,我们正式进入表内部。但 PostgreSQL 的表不像 Excel 表那样一览无余。在默认状态下,一个 Role 默认不具备读写任何 Column 的权限,必须通过手动 GRANT 命令来授予。且 CRUD(Create, Read, Update, Delete)操作需要分开授权。

例如,在上述查询用户 ID 和名字的语句中,必须使用 GRANT 命令,为发起查询的 Role 授予 idname 这两个 Column 的 SELECT 权限。

虽然见过一些开发者为省事,直接将所有表的全部 Column 的所有 CRUD 权限一次性授予所有新建 Role,但若目标是“省事”,笔者建议不如直接 “DROP TABLE”,一步到位。

在 Column 层面,我的推荐是:“只给用户最低程度的使用权限”。

  • 会员等级: 用户只能查看自己的会员等级,不应修改,故仅授予 SELECT 权限。
  • 微信 OpenID/UnionID: 用户本人通常用不上这些参数,仅后台程序调用 API 时需要。因此,即使这些信息属于用户,也不应授予用户任何 SELECT 权限

这个问题的重要性日益凸显。一份安全机构的报告指出,高达 96% 的企业权限设置处于“空置”状态,即权限被创建后却无人使用也无人关闭。报告特别强调,当 AI 开始接管操作时,这些可被任意使用的宽泛权限范围将构成巨大的安全隐患。

因此,在授权问题上,我建议遵循“只做加法、不做减法”的原则,避免不经意间泄露不必要的权限。

3. Row Level Security (RLS):动态、图灵完备的最后一道锁

Column 层面权限验证通过后,我们来到了 PostgreSQL 最为强大的功能之一:Row Level Security (RLS)。这是一个独一无二的、图灵完备的动态规则系统。

前面介绍的 search_pathGRANT 均属于静态匹配逻辑。一旦执行命令授予权限,该 Role 将一直拥有该权限,直至被显式剥夺。而 RLS 则完全动态,它会在每一个事务(transaction)发起时,实时地验证 Role 对特定数据的 CRUD 权限

举例来说,在一个订单 orders 表中,我们希望买家只能读写自己的订单,而店家只能读不能写。此时,可以在用户发起请求时,通过 PostgreSQL 的运行时参数功能 pg_settings 将用户的 ID 注入到事务中。当查询到达 RLS 验证阶段时,系统会提取用户 ID,并与当前 Row 中的买家 ID 和店家 ID 进行比对。

  • 对于 UPDATE 权限的策略,会检查用户 ID 是否等于 Row 中的买家 ID。
  • 对于 SELECT 权限的策略,则会检查用户 ID 是否等于 Row 中的买家 ID 或店家 ID。

即便如此,即便用户身份尊贵如“玉皇大帝”,若其 ID 与目标 Row 中的买家或店家 ID 不匹配,也无权读取该 Row 数据。RLS 因此构成了 PostgreSQL 安全体系的最后一道、也是最强有力的锁。

RLS 的强大之处在于,其验证逻辑可以基于任何 SQL 语句或 Function。这意味着,在 Function 中,我们可以实时调用任何数据来辅助验证。

例如,要追加一项权限:允许店铺的员工读取本店的所有订单。只需在 RLS 验证时:

  1. 先查询储存员工数据的表,获取该用户当前任职的店铺 ID。
  2. 再返回订单表,匹配订单的店铺 ID。

由于验证是实时的,一旦员工被解雇,其对应的员工信息表更新的那一刻,该用户便立刻失去了所有订单的读取权限,实现了零延迟的安全响应。

若无性能上的顾虑(如 TPS 瓶颈),甚至可以将所有系统层面和业务层面的验证逻辑,全部封装在一个 Function 中,通过 RLS 进行实时验证。这正是我在 2 年前分享的【让编程再次伟大#14】中的做法,当时曾引发不少争议,如今看来,仍是因未能深入阐释 PostgreSQL 的魅力。

性能权衡与“三权分立”的最佳实践

PostgreSQL 强大的层层叠加安全体系,不仅提供了 360 度全覆盖的防护,更在使用上展现出极大的灵活性。例如,极端情况下,可以为每一位用户创建一个单独的 Role。这使得权限分配可以从 Schema 层面开始进行精细化控制。

然而,我不建议如此操作。数据库的许多性能节点(如 Connection Pool 中的连接)与 Role 挂钩,只能在同一 Role 之间重用。大量的 Role 会导致每个 Role 都经历冷启动,并舍弃大量只能在同一 Role 间共用的缓存,这几乎会抵消掉通过参数优化(如在【让PostgreSQL再次伟大#01】中讨论的)所获得的性能提升。这也是视频开篇提到许多项目倾向于使用单一 admin 角色的原因之一:避免因角色切换导致的性能损失。

但作为一个折中的实践者,我的建议是:既不为每个用户创建独立 Role,也不仅依赖单一 admin Role。应寻找一个兼顾安全与性能的平衡点。

我设计的“三权分立”角色结构便是这样一种尝试:

  1. Connector Role: 仅拥有发起数据库连接的权限。后端程序使用此 Role 连接到 Connection Pool,从而避免了连接池的稀释或角色切换带来的冷启动问题。
  2. Visitor Role: 当用户发起具体查询请求时(如前述,后端将用户 ID 注入运行参数),通过 SET ROLE 命令切换到此 Role。这是数据库所有 search_pathGRANT 和 RLS 设置所直接面对的 Role。如此,后端程序仅使用 Connector Role,而数据库的安全验证功能则全部由 Visitor Role 处理,两者实现相对独立。
  3. Admin Role: 传统意义上的管理员角色,仅用于在初始阶段进行一切设置工作,不在日常运行中使用。

总的来说,这种设计能在很大程度上隔离不同的权限范围,避免不必要的权限滥用或泄露,同时规避了角色切换对性能的破坏。当然,欢迎大家在评论区分享各自的设计方案。

安全与性能的终极博弈

若你以为以上安全机制已是 PostgreSQL 的全部,那便远远低估了它的强大。上述内容仅为入门级分享,受时间限制,我跳过了更宏观的安全管理流程。例如,在前期防御上,可通过底层操作系统实现基于主机的身份验证(如 pg_hba);在后期审计上,强大的 pgaudit 和 Logging 机制可还原几乎所有数据库操作细节。

严格来说,这些进阶功能“可做可不做”。但许多情况下,最终的决定权往往落在了“安全 vs. 性能”的权衡上。毫无疑问,这两者天然矛盾:更高的安全性通常需要更多的操作,必然影响性能,反之亦然。

业界许多项目最终选择让安全为性能让步,我认为根本原因在于:性能更容易制定量化指标。性能是一个可量化的数值,可以持续提升,成为项目组的“铁饭碗”。而安全则令人讨厌,因为“无事发生”本应是常态,并无奖励,一旦出事,则会引发轩然大波。

然而,换个角度,性能反而应为安全让步。性能的损失可以通过其他途径弥补,如硬件升级、扩容,系统参数优化(【让PostgreSQL再次伟大#01】),以及高速缓存(如【让PostgreSQL再次伟大#03】将介绍的内存缓存)等。

但安全不行,安全是“非黑即白”的,只有 0 和 1。无论前期工作做得多么出色,最后一步若出现问题,整个结果链就变成了 0。即便拥有 110% 或 120% 的性能,乘以 0 的结果依然是 0。一切努力将化为虚无,其意义何在?你觉得是不是?

📌 文中提及的人物和组织

公司/组织: PostgreSQL, GitLab

产品/模型: PostgreSQL

关键字: postgresql database-security row-level-security access-control least-privilege