安全與權限管理:從認證到 Row Level Security 全方位防護 | PostgreSQL

2026/07/04
安全與權限管理:從認證到 Row Level Security 全方位防護 | PostgreSQL

安全是資料庫運維的基石。PostgreSQL 採用「縱深防禦(Defense in Depth)」策略,從網路層認證層授權層資料加密層,每一層都有對應的防護機制。本文將從 SCRAM-SHA-256 認證、pg_hba.conf 設定,到 Row Level SecuritypgAudit 稽核與 pgcrypto 加密,帶你建立完整的 PostgreSQL 安全防線。

PostgreSQL 安全模型總覽

PostgreSQL 採用分層安全架構,每層負責不同範疇的存取控制:

┌──────────────────────────────────────────┐
│           網路層(Network Layer)         │
│   防火牆、SSL/TLS、IP 白名單             │
├──────────────────────────────────────────┤
│         認證層(Authentication Layer)    │
│   pg_hba.conf、SCRAM、Certificate、LDAP  │
├──────────────────────────────────────────┤
│         授權層(Authorization Layer)     │
│   GRANT/REVOKE、角色繼承、物件權限       │
├──────────────────────────────────────────┤
│        行級安全(Row Level Security)     │
│   CREATE POLICY、PERMISSIVE/RESTRICTIVE  │
├──────────────────────────────────────────┤
│       欄位級安全(Column-Level Security) │
│   欄位 GRANT、視圖遮罩                   │
├──────────────────────────────────────────┤
│         加密層(Encryption Layer)        │
│   pgcrypto、TDE、傳輸加密               │
└──────────────────────────────────────────┘

核心原則是「即使某一層被突破,其他層仍能限制損失範圍」:

原則說明
最小權限(Least Privilege)每個帳號只授予完成工作所需的最低權限
預設拒絕(Default Deny)未明確授權的操作一律拒絕
稽核可追溯(Auditability)所有敏感操作均有日誌紀錄
定期複查(Periodic Review)定期審查權限、憑證與設定

認證機制(Authentication)

認證是安全的第一道防線——確認「你是誰」。

SCRAM-SHA-256(推薦)

SCRAM-SHA-256 是 PostgreSQL 10+ 預設且最安全的密碼認證方式,取代舊有的 MD5 認證。它的優點包括:密碼不以明文傳輸、防止重放攻擊(Replay Attack)、伺服器端無法反推明文密碼。

-- postgresql.conf
password_encryption = 'scram-sha-256'

-- 建立使用者,密碼以 SCRAM 格式儲存
CREATE ROLE app_user WITH LOGIN PASSWORD 'StrongP@ssw0rd!';

-- 驗證密碼加密格式
SELECT rolname, LEFT(rolpassword, 14) AS format_prefix
FROM pg_authid
WHERE rolname = 'app_user';
-- 正確輸出:SCRAM-SHA-256$

Certificate 憑證認證

憑證認證透過 TLS 客戶端憑證識別使用者身份,無需密碼,適合**機器對機器(M2M)**場景:

# 1. 產生 CA 憑證
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/CN=PostgreSQL CA"

# 2. 產生伺服器憑證
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr \
  -subj "/CN=db.example.com"
openssl x509 -req -days 365 -in server.csr -CA ca.crt \
  -CAkey ca.key -CAcreateserial -out server.crt

# 3. 產生客戶端憑證(CN 需與 PostgreSQL 使用者名稱一致)
openssl genrsa -out client.key 4096
openssl req -new -key client.key -out client.csr \
  -subj "/CN=app_user"
openssl x509 -req -days 365 -in client.csr -CA ca.crt \
  -CAkey ca.key -CAcreateserial -out client.crt
# pg_hba.conf — 使用 cert 方法
hostssl all app_user 10.0.0.0/8 cert clientcert=verify-full

LDAP / GSSAPI 認證

方式適用場景特色
LDAP企業統一身份管理(Active Directory)密碼驗證委派給 LDAP Server
GSSAPIKerberos 環境單一登入(SSO),無需額外密碼
# pg_hba.conf — LDAP Search+Bind
host all all 10.0.0.0/8 ldap \
  ldapserver=ldap.example.com \
  ldapbasedn="ou=users,dc=example,dc=com" \
  ldapbinddn="cn=pgbind,dc=example,dc=com" \
  ldapbindpasswd="bindpassword" \
  ldapsearchattribute=uid \
  ldapscheme=ldaps ldapport=636

注意:LDAP 認證只驗證密碼,使用者仍需在 PostgreSQL 中以 CREATE ROLE 建立。


pg_hba.conf 最佳實踐

pg_hba.conf(Host-Based Authentication)是認證的核心設定檔,規則由上至下逐行匹配,第一條匹配的規則生效

# TYPE  DATABASE        USER            ADDRESS                 METHOD

# 1. 本機超級使用者使用 peer(OS 使用者即 PG 使用者)
local   all             postgres                                peer

# 2. 本機一般連線
local   all             all                                     scram-sha-256

# 3. 複製連線限制來源 IP
host    replication     replicator      10.0.1.0/24             scram-sha-256

# 4. 應用程式連線:限定使用者 + 資料庫 + IP + 加密
hostssl app_db          app_user        10.0.2.0/24             scram-sha-256

# 5. DBA 管理工具:限定 IP 段
hostssl all             dba_role        10.0.3.0/24             scram-sha-256

# 6. 預設拒絕(最後一行)
host    all             all             0.0.0.0/0               reject

常見錯誤(務必避免):

  • trust 用於非本機連線 → 無需密碼即可登入
  • 未限定來源 IP(0.0.0.0/0 + 非 reject)
  • 使用 MD5 而非 SCRAM-SHA-256
  • 寬鬆規則放在嚴格規則之前(後者永遠不生效)

角色與權限管理

角色架構設計

PostgreSQL 的角色(Role)統一了使用者與群組的概念。最佳實踐是建立「群組角色(不可登入)」+ 「個人角色(可登入)」的層級結構:

-- 建立群組角色(不可登入)
CREATE ROLE readonly_group NOLOGIN;
CREATE ROLE readwrite_group NOLOGIN;

-- 建立可登入使用者
CREATE ROLE app_user WITH
  LOGIN
  PASSWORD 'StrongP@ssw0rd!'
  CONNECTION LIMIT 50      -- 限制連線數
  VALID UNTIL '2027-01-01' -- 帳號有效期
  NOSUPERUSER
  NOCREATEDB
  NOCREATEROLE;

-- 建立唯讀分析帳號
CREATE ROLE analyst WITH LOGIN PASSWORD 'AnalystP@ss!' NOSUPERUSER;

GRANT / REVOKE

-- 授予資料庫連線權限
GRANT CONNECT ON DATABASE app_db TO readonly_group;
GRANT CONNECT ON DATABASE app_db TO readwrite_group;

-- 授予 Schema 使用權
GRANT USAGE ON SCHEMA public TO readonly_group;
GRANT USAGE ON SCHEMA public TO readwrite_group;

-- 授予資料表權限
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_group;
GRANT SELECT, INSERT, UPDATE, DELETE
  ON ALL TABLES IN SCHEMA public TO readwrite_group;

-- 授予 Sequence(INSERT 需要)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO readwrite_group;

-- 將群組角色指派給使用者
GRANT readonly_group TO analyst;
GRANT readwrite_group TO app_user;

-- 撤銷 public schema 的預設公開建立權限(PG15 前的安全隱患)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

DEFAULT PRIVILEGES

DEFAULT PRIVILEGES 確保未來新建立的物件自動套用指定權限,解決「忘記 GRANT 新資料表」的常見問題:

-- 未來由 app_user 建立的資料表,自動授予 readonly_group SELECT
ALTER DEFAULT PRIVILEGES FOR ROLE app_user IN SCHEMA public
  GRANT SELECT ON TABLES TO readonly_group;

-- 未來的 Sequence
ALTER DEFAULT PRIVILEGES FOR ROLE app_user IN SCHEMA public
  GRANT USAGE, SELECT ON SEQUENCES TO readwrite_group;

-- 未來的函式
ALTER DEFAULT PRIVILEGES FOR ROLE app_user IN SCHEMA public
  GRANT EXECUTE ON FUNCTIONS TO readwrite_group;

角色繼承與 SET ROLE

-- INHERIT(預設):自動繼承權限
CREATE ROLE senior_analyst INHERIT;
GRANT readonly_group TO senior_analyst;
GRANT senior_analyst TO john_doe;
-- john_doe 自動擁有 readonly_group 的所有權限

-- NOINHERIT:需明確 SET ROLE 才能取得權限
CREATE ROLE privileged_ops NOINHERIT;
GRANT privileged_ops TO ops_user;

-- ops_user 需執行:
SET ROLE privileged_ops;
-- 執行特權操作...
RESET ROLE;

PG14+ 預定義角色

PostgreSQL 14 引入預定義角色,簡化全庫授權:

-- 可讀取所有資料表、視圖、序列
GRANT pg_read_all_data TO analyst;

-- 可寫入所有資料表、視圖、序列
GRANT pg_write_all_data TO etl_user;

-- 其他實用預定義角色
GRANT pg_monitor TO monitoring_user;         -- 監控相關視圖
GRANT pg_read_all_settings TO config_reader; -- 讀取 GUC 設定
GRANT pg_read_all_stats TO stats_reader;     -- 讀取統計資訊
GRANT pg_signal_backend TO ops_user;         -- 可發送 pg_terminate_backend

Row Level Security(RLS)

RLS 讓資料表的每一列都有存取政策,不同使用者看到不同的資料子集。這是實現**多租戶(Multi-Tenant)**資料隔離最優雅的方式。

基本用法

-- 步驟 1:啟用 RLS
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 步驟 2:建立政策
-- 使用者只能看到自己的訂單
CREATE POLICY user_orders_policy ON orders
  FOR ALL
  TO app_user
  USING (user_id = current_user::TEXT)         -- 查詢過濾(SELECT/UPDATE/DELETE)
  WITH CHECK (user_id = current_user::TEXT);   -- 寫入驗證(INSERT/UPDATE)

-- 唯讀政策
CREATE POLICY select_own_data ON orders
  FOR SELECT
  USING (user_id = current_user::TEXT);

PERMISSIVE vs RESTRICTIVE

兩種政策模式決定多個 Policy 如何組合:

模式組合方式典型用途
PERMISSIVE(預設)多個 Policy 以 OR 合併admin 看全部、使用者看自己的
RESTRICTIVE多個 Policy 以 AND 合併強制所有人不能看已刪除資料
-- PERMISSIVE:admin 看到全部
CREATE POLICY admin_sees_all ON orders
  AS PERMISSIVE
  FOR SELECT
  TO admin_role
  USING (true);

-- RESTRICTIVE:無論其他政策,已刪除的資料都不可見
CREATE POLICY active_records_only ON orders
  AS RESTRICTIVE
  FOR ALL
  USING (is_deleted = false);

多租戶場景

-- 建立多租戶資料表
CREATE TABLE tenant_data (
  id          BIGSERIAL PRIMARY KEY,
  tenant_id   INTEGER NOT NULL,
  payload     JSONB,
  created_at  TIMESTAMPTZ DEFAULT now()
);

ALTER TABLE tenant_data ENABLE ROW LEVEL SECURITY;

-- 透過 Session Variable 隔離租戶
CREATE POLICY tenant_isolation ON tenant_data
  USING (tenant_id = current_setting('app.tenant_id')::INTEGER)
  WITH CHECK (tenant_id = current_setting('app.tenant_id')::INTEGER);

-- 應用程式連線後設定:
-- SET app.tenant_id = '42';

-- 連表擁有者也受 RLS 控制
ALTER TABLE tenant_data FORCE ROW LEVEL SECURITY;

-- 為管理員建立繞過政策
CREATE POLICY admin_bypass ON tenant_data
  TO admin_role
  USING (true);

Column-Level Privileges

欄位級權限(Column-Level Privileges)允許對特定欄位授予或拒絕存取,適合保護薪資、**個人識別資訊(PII)**等敏感欄位:

CREATE TABLE employees (
  id          SERIAL PRIMARY KEY,
  name        VARCHAR(100),
  department  VARCHAR(50),
  salary      NUMERIC(10,2),   -- 敏感
  ssn         VARCHAR(20),     -- 敏感
  email       VARCHAR(100)
);

-- 一般查詢角色:只看非敏感欄位
GRANT SELECT (id, name, department, email) ON employees TO hr_viewer;

-- 薪資管理角色:只看薪資相關
GRANT SELECT (id, name, salary) ON employees TO payroll_role;

-- 欄位級 UPDATE
GRANT UPDATE (email) ON employees TO self_service_user;

使用視圖(View)作為欄位遮罩的替代方案:

-- 建立遮罩視圖,SSN 只顯示後四碼
CREATE VIEW employees_public AS
SELECT
  id, name, department, email,
  '***-**-' || RIGHT(ssn, 4) AS ssn_masked
FROM employees;

GRANT SELECT ON employees_public TO hr_viewer;
REVOKE SELECT ON employees FROM hr_viewer;

SSL/TLS 傳輸加密

設定 SSL

# 產生自簽憑證(生產環境建議使用受信任 CA)
openssl req -new -x509 -days 365 -nodes \
  -out server.crt -keyout server.key \
  -subj "/CN=db.example.com"

# 設定正確的檔案權限
chmod 600 server.key
chown postgres:postgres server.key server.crt
# postgresql.conf
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file  = 'server.key'
ssl_min_protocol_version = 'TLSv1.2'   -- PG12+
ssl_ciphers = 'HIGH:MEDIUM:+3DES:!aNULL'
ssl_prefer_server_ciphers = on

強制 SSL 連線

# pg_hba.conf
hostssl   app_db    app_user    10.0.0.0/8    scram-sha-256
hostssl   all       dba_role    10.0.3.0/24   scram-sha-256

# 拒絕所有非 SSL 連線
hostnossl all       all         0.0.0.0/0     reject
-- 驗證當前連線是否使用 SSL
SELECT ssl, version, cipher, bits
FROM pg_stat_ssl
WHERE pid = pg_backend_pid();

pgAudit 稽核日誌

pgAudit 提供詳細的工作階段(Session)和物件(Object)層級稽核日誌,符合 SOX、PCI-DSS 等合規需求。

安裝與設定

# 安裝(Ubuntu/Debian)
apt-get install postgresql-17-pgaudit
# postgresql.conf
shared_preload_libraries = 'pgaudit'

# 記錄所有 DDL、DML、角色操作
pgaudit.log = 'ddl, write, role'
pgaudit.log_catalog = off      -- 避免系統目錄日誌爆炸
pgaudit.log_parameter = on     -- 記錄查詢參數
pgaudit.log_relation = on      -- 為每個關聯記錄單獨條目
-- 啟用擴充
CREATE EXTENSION pgaudit;

-- 物件級稽核:針對特定資料表
CREATE ROLE auditor;
GRANT SELECT ON sensitive_table TO auditor;
ALTER ROLE app_user SET pgaudit.role = 'auditor';
-- 現在 app_user 對 sensitive_table 的所有 SELECT 都會被記錄

日誌格式

AUDIT: SESSION,<audit_count>,<sub_count>,<class>,<command>,
       <object_type>,<object_name>,<statement>,<parameter>

範例:

AUDIT: SESSION,1,1,WRITE,INSERT,TABLE,public.orders,
  "INSERT INTO orders (user_id, amount) VALUES ($1, $2)",<none>

AUDIT: SESSION,2,1,DDL,ALTER TABLE,TABLE,public.employees,
  "ALTER TABLE employees ADD COLUMN phone VARCHAR(20)",<none>

pgcrypto 資料加密

pgcrypto 提供應用層級的資料加密函式,可對欄位值進行加密/解密:

CREATE EXTENSION pgcrypto;

-- 對稱加密(AES)
INSERT INTO sensitive_data (encrypted_payload)
VALUES (
  pgp_sym_encrypt(
    '{"ssn": "123-45-6789"}',
    'key_from_vault',          -- 金鑰應來自密鑰管理服務
    'cipher-algo=aes256'
  )
);

-- 解密
SELECT pgp_sym_decrypt(encrypted_payload::BYTEA, 'key_from_vault')
FROM sensitive_data WHERE id = 1;

-- 密碼雜湊(bcrypt,不可逆)
SELECT crypt('user_password', gen_salt('bf', 12));

-- 驗證密碼
SELECT crypt('input_password', stored_hash) = stored_hash AS is_valid
FROM users WHERE username = 'john_doe';

-- 產生安全隨機 Token
SELECT encode(gen_random_bytes(32), 'hex') AS api_token;

TDE 現況

**TDE(Transparent Data Encryption)**是在儲存層對整個資料庫檔案加密,無需修改應用程式:

方案狀態
PostgreSQL 原生 TDE社群開發中,預計未來版本加入
EDB Postgres Advanced Server商業支援
AWS RDS/Aurora透過 AWS KMS 管理金鑰
Azure Database for PostgreSQL透過 Azure Key Vault 整合
檔案系統加密(LUKS)作業系統層,PG 無感知

安全稽核檢查清單

定期執行以下查詢進行安全健檢:

-- 1. 找出所有 SUPERUSER
SELECT rolname, rolsuper, rolcreatedb, rolcreaterole
FROM pg_roles WHERE rolsuper = true;

-- 2. 找出無密碼的可登入帳號
SELECT rolname FROM pg_roles
WHERE rolcanlogin = true AND rolpassword IS NULL;

-- 3. 找出已過期但仍可登入的帳號
SELECT rolname, rolvaliduntil FROM pg_roles
WHERE rolcanlogin = true AND rolvaliduntil < now();

-- 4. 找出 PUBLIC 擁有權限的敏感資料表
SELECT schemaname, tablename, privilege_type
FROM information_schema.role_table_grants
WHERE grantee = 'PUBLIC';

-- 5. 找出未啟用 RLS 的多租戶資料表
SELECT schemaname, tablename, rowsecurity
FROM pg_tables
WHERE tablename IN (
  SELECT table_name FROM information_schema.columns
  WHERE column_name = 'tenant_id'
) AND rowsecurity = false;

-- 6. 查看 pg_hba.conf 目前設定
SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules ORDER BY line_number;

-- 7. 找出長時間閒置的連線
SELECT pid, usename, client_addr, state,
       now() - state_change AS idle_duration
FROM pg_stat_activity
WHERE state = 'idle'
  AND now() - state_change > INTERVAL '30 minutes';

常見陷阱

-- 陷阱 1:public schema 對所有使用者開放(PG15 前)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;

-- 陷阱 2:應用程式使用 postgres 超級使用者帳號
-- 正確:建立最小權限帳號
CREATE ROLE app_service WITH LOGIN PASSWORD '...'
  NOSUPERUSER NOCREATEDB NOCREATEROLE;

-- 陷阱 3:未設定連線限制
ALTER ROLE app_user CONNECTION LIMIT 100;

-- 陷阱 4:GRANT ... TO PUBLIC(影響所有現有和未來帳號)
REVOKE ALL ON TABLE sensitive_data FROM PUBLIC;

-- 陷阱 5:RLS 未啟用 FORCE,表擁有者繞過 RLS
ALTER TABLE tenant_data FORCE ROW LEVEL SECURITY;

-- 陷阱 6:未設定 search_path,可能導致 Schema 注入
ALTER ROLE app_user SET search_path = '$user', public;

-- 陷阱 7:密碼記錄在命令列歷史
-- 改用 \password 命令(提示輸入,不記錄)
-- \password app_user

版本演進

版本安全相關重要變更
PG 9.5Row Level Security(RLS) 正式引入
PG 10SCRAM-SHA-256 認證取代 MD5
PG 12ssl_min_protocol_version 設定
PG 14pg_read_all_datapg_write_all_data 預定義角色
PG 15public schema 預設不再允許 PUBLIC CREATE
PG 16pg_hba.conf 支援連線參數過濾(options 欄位)
PG 17改進的 SSL 憑證驗證、oauth 認證方法(實驗性)

總結

PostgreSQL 的安全模型建立在縱深防禦的理念上——從網路層的 SSL/TLS 和防火牆,到認證層的 SCRAM-SHA-256 和憑證認證,再到授權層的 GRANT/REVOKE 與角色繼承,最後深入到 Row Level Security 的行級隔離和 pgcrypto 的資料加密。每一層都是獨立的防線,層層堆疊才能構成完整的安全體系。

關鍵要點:永遠遵循最小權限原則、使用 DEFAULT PRIVILEGES 自動化權限管理、為多租戶場景啟用 RLS、用 pgAudit 實現合規稽核,以及定期執行安全檢查清單。

下一篇,我們將深入探討 備份與還原——掌握 pg_dump、pg_basebackup、PITR 時間點還原與自動化備份策略,確保你的資料永不丟失。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。