Skip to content

SSL 连接初始化 ​

SSL/TLS 加密让客户端与 PostgreSQL 之间的数据传输不再以明文形态暴露在网络中。本节将介绍自签名证书(Self-Signed Certificate)的生成、服务端 ssl 参数配置、pg_hba.conf 中 hostssl 的强制、客户端 sslmode 的选择与验证方法。

前置知识

阅读本节前,建议先了解:监听地址与端口配置

基础概念 ​

PostgreSQL 的 SSL 是传输层加密,它解决的是「链路被窃听 / 被篡改」的问题,不解决「谁能连」的问题:

安全目标由谁负责
链路加密、防窃听SSL/TLS(本节)
防篡改、防中间人SSL/TLS + 证书校验(sslmode=verify-full)
谁能连接pg_hba.conf(认证方法)
连上后能做什么GRANT / 对象权限

一个常见误解:配了 SSL 就安全了。实际上 sslmode=require 只保证「加密了」,但不做服务端身份校验——如果 DNS 被劫持或中间人冒充服务器,客户端仍然会连上并加密地传输数据给对方。真正能防中间人的是 verify-ca 与 verify-full。

PostgreSQL 的 SSL 有两种部署形态:

形态说明适用
自签名证书自己充当 CA,用 openssl 生成内部网络、测试环境、无外部 CA 的场景
公开 CA 签发由受信任 CA(如企业 PKI、Let's Encrypt)签发需要客户端零配置校验的场景

PostgreSQL 的证书文件命名是固定的:

文件作用必需
server.crt服务端证书(或含中间证书的链)是
server.key服务端私钥是
root.crt受信任的 CA 证书,用于校验客户端证书仅在需要 cert 认证时
root.crl证书吊销列表可选,配合 root.crt
术语英文名说明
自签名证书Self-Signed Certificate由自己生成的 CA 签发、未获公共信任的证书
证书签名请求CSR, Certificate Signing Request申请证书时提交的公钥与身份信息
通用名CN, Common Name证书中的主体名,须与连接使用的主机名一致
主体备用名SAN, Subject Alternative Name现代 TLS 校验的实际依据,优先于 CN
客户端证书Client Certificate双向 TLS 中客户端出示的证书
SSL 模式sslmode客户端对证书校验的严格程度
证书吊销列表CRL, Certificate Revocation List已吊销证书名单

安装与配置 ​

生成自签名证书 ​

bash
# ── 第 1 步:准备目录与私钥权限 ──
sudo mkdir -p /etc/postgresql/18/ssl
sudo chown postgres:postgres /etc/postgresql/18/ssl
sudo chmod 700 /etc/postgresql/18/ssl
cd /etc/postgresql/18/ssl

# ── 第 2 步:生成 CA 私钥与自签名 CA 证书(有效期 10 年) ──
sudo -u postgres openssl req -new -x509 -days 3650 -nodes \
  -text -out root.crt \
  -keyout root.key \
  -subj "/C=CN/ST=Shanghai/L=Shanghai/O=Example/CN=Example-Internal-CA"

# ── 第 3 步:生成服务端私钥与 CSR ──
sudo -u postgres openssl req -new -nodes \
  -text -out server.csr \
  -keyout server.key \
  -subj "/C=CN/ST=Shanghai/L=Shanghai/O=Example/CN=db.example.com"

# ── 第 4 步:生成 SAN 扩展文件(现代客户端依此校验,务必写对) ──
cat > server.ext <<'EOF'
basicConstraints = CA:FALSE
subjectAltName = DNS:db.example.com, DNS:localhost, IP:10.10.1.5, IP:127.0.0.1
extendedKeyUsage = serverAuth
EOF

# ── 第 5 步:用 CA 签发服务端证书 ──
sudo -u postgres openssl x509 -req -in server.csr -days 3650 \
  -CA root.crt -CAkey root.key -CAcreateserial \
  -extfile server.ext \
  -out server.crt

# ── 第 6 步:校验签发结果 ──
openssl verify -CAfile root.crt server.crt
# server.crt: OK

openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName
bash
# ── 权限必须严格,否则 PostgreSQL 拒绝启动 ──
sudo chown postgres:postgres server.crt server.key root.crt
sudo chmod 600 server.key          # 私钥仅属主可读
sudo chmod 644 server.crt root.crt # 证书可读

# 确认最终权限
ls -l /etc/postgresql/18/ssl/
# -rw-r--r-- 1 postgres postgres  root.crt
# -rw-r--r-- 1 postgres postgres  server.crt
# -rw------- 1 postgres postgres  server.key

常见失败:server.key 权限过宽(如 644)会导致启动时报 private key file "server.key" has group or world access。PostgreSQL 要求私钥权限为 600 或更严。

服务端参数配置 ​

ini
# /var/lib/pgsql/18/data/postgresql.conf

# ── 启用 SSL ──
ssl = on

# ── 证书与私钥路径(相对于 PGDATA,或写绝对路径) ──
ssl_cert_file = '/etc/postgresql/18/ssl/server.crt'
ssl_key_file  = '/etc/postgresql/18/ssl/server.key'

# ── 受信任 CA(用于校验客户端证书;若只用口令认证可暂不配置) ──
ssl_ca_file = '/etc/postgresql/18/ssl/root.crt'

# ── 证书吊销列表(可选) ──
# ssl_crl_file = '/etc/postgresql/18/ssl/root.crl'

# ── 协议与加密套件(按合规要求收紧) ──
ssl_min_protocol_version = 'TLSv1.2'
ssl_max_protocol_version = 'TLSv1.3'
# 默认值即为安全基线,除非有明确合规要求,一般无需手写 cipher
# ssl_ciphers = 'HIGH:!aNULL:!MD5'

# ── 是否在未加密连接上优先劝告客户端使用 SSL ──
ssl_prefer_server_ciphers = on

# ── 是否要求客户端证书(配合 pg_hba.conf 的 cert 方法使用) ──
# ssl_cert_file 已配置即可,实际是否必须由 pg_hba.conf 决定
bash
# 修改 postgresql.conf 中的 ssl 相关参数需要重启
sudo systemctl restart postgresql

# 确认启动成功且无证书错误
sudo systemctl status postgresql
sudo tail -n 30 /var/log/postgresql/postgresql-18-main.log
sql
-- 确认服务端 SSL 已启用
SHOW ssl;
SHOW ssl_min_protocol_version;
SHOW ssl_cert_file;

-- 查看完整的 SSL 相关设置
SELECT name, setting
FROM pg_settings
WHERE name LIKE 'ssl%'
ORDER BY name;

pg_hba.conf 中强制 SSL ​

text
# ── 只允许 SSL 连接的业务规则(无 SSL 的连接会继续往下匹配,最终被拒绝) ──
# TYPE     DATABASE  USER        ADDRESS         METHOD
hostssl    demo      app_user    10.10.0.0/16    scram-sha-256
hostssl    demo      report_user 10.10.0.0/16    scram-sha-256

# ── 明确拒绝非 SSL 的同网段连接,让报错更清晰 ──
hostnossl  demo      all         10.10.0.0/16    reject

# ── 本地回环允许非 SSL(同机通信,链路无暴露风险) ──
host       all       all         127.0.0.1/32    scram-sha-256
bash
# 改 pg_hba.conf 只需 reload
sudo systemctl reload postgresql

# 确认规则加载正确
psql -d postgres -c "SELECT line_number, type, database, user_name, address, auth_method, error FROM pg_hba_file_rules ORDER BY line_number;"

详细说明 ​

sslmode 的六个等级 ​

sslmode 是客户端参数,决定「是否加密」与「校验到什么程度」:

sslmode是否加密校验服务端证书校验主机名防中间人适用
disable否——否仅本机 / 隔离环境
allow优先明文,服务端要求才加密否否否不推荐
prefer优先加密,可回退明文否否否客户端默认值,不推荐用于生产
require必须加密否否否防窃听,不防冒充
verify-ca必须加密是否部分证书 CN/SAN 不稳定时
verify-full必须加密是是是生产推荐
bash
# 弱:只加密,不校验(可被中间人冒充)
# 不推荐
psql "host=db.example.com dbname=demo user=app_user sslmode=require"

# 中:校验 CA 但不校验主机名
psql "host=db.example.com dbname=demo user=app_user \
      sslmode=verify-ca sslrootcert=/etc/pki/root.crt"

# 强:校验 CA + 主机名(推荐)
psql "host=db.example.com dbname=demo user=app_user \
      sslmode=verify-full sslrootcert=/etc/pki/root.crt"

prefer 是 libpq 的默认值,它的危险在于「可降级」:服务端若未启用 SSL,连接会静默地以明文建立。生产环境必须显式写成 verify-full(或至少 require)。

客户端证书(双向 TLS) ​

当 pg_hba.conf 使用 cert 方法时,客户端必须出示证书,且证书的 CN 需与数据库用户名匹配:

bash
# ── 生成客户端证书(在客户端机器上执行,私钥不离开本机) ──
# 1) 生成客户端私钥与 CSR
openssl req -new -nodes \
  -out app_user.csr \
  -keyout app_user.key \
  -subj "/C=CN/O=Example/CN=app_user"

# 2) 用同一 CA 签发(CN 必须等于数据库角色名 app_user)
openssl x509 -req -in app_user.csr -days 3650 \
  -CA root.crt -CAkey root.key -CAcreateserial \
  -out app_user.crt

# 3) 权限收紧
chmod 600 app_user.key
chmod 644 app_user.crt
text
# 服务端 pg_hba.conf:要求客户端证书
# 注意 clientcert=verify-full 会同时校验 CN 与用户名
hostssl  demo  app_user  10.10.0.0/16  cert  clientcert=verify-full
bash
# 客户端连接(无需密码,凭证书认证)
psql "host=db.example.com dbname=demo user=app_user \
      sslmode=verify-full \
      sslrootcert=root.crt \
      sslcert=app_user.crt \
      sslkey=app_user.key" \
     -c "\conninfo"
text
# \conninfo 输出示例
You are connected to database "demo" as user "app_user" on host "db.example.com" (address "10.10.1.5") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)

在服务端确认连接的加密状态 ​

sql
-- 查看每条连接是否加密、用的什么协议
SELECT pid,
       usename,
       coalesce(client_addr::text, 'unix socket') AS client,
       ssl,
       ssl_version,
       ssl_cipher,
       coalesce(sslcert_cn, '-') AS cert_cn
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY ssl DESC, pid;

-- 只统计加密连接占比
SELECT count(*)                                       AS total,
       count(*) FILTER (WHERE ssl)                    AS encrypted,
       round(100.0 * count(*) FILTER (WHERE ssl) / greatest(count(*), 1), 1) AS pct
FROM pg_stat_activity
WHERE backend_type = 'client backend';
text
# 输出示例
 total | encrypted |  pct
-------+-----------+-------
    18 |        16 |  88.9
(1 row)
sql
-- 从会话侧确认自己的连接状态
SELECT inet_server_addr() AS server_ip,
       ssl,
       ssl_version,
       ssl_cipher,
       version();

实战示例 ​

示例一:为一个内部库从零启用 SSL ​

bash
# 第 1 步:确认 OpenSSL 可用
openssl version
# OpenSSL 3.x

# 第 2 步:一键生成 CA + 服务端证书(脚本化,便于多地复用)
#!/usr/bin/env bash
set -euo pipefail

SSL_DIR=/etc/postgresql/18/ssl
HOSTNAME_FQDN=db.example.com
SERVER_IP=10.10.1.5
DAYS=3650

sudo mkdir -p "${SSL_DIR}"
sudo chown postgres:postgres "${SSL_DIR}"
sudo chmod 700 "${SSL_DIR}"
cd "${SSL_DIR}"

# CA
sudo -u postgres openssl req -new -x509 -days "${DAYS}" -nodes \
  -text -out root.crt -keyout root.key \
  -subj "/C=CN/O=Example/CN=Example-Internal-CA"

# 服务端 CSR
sudo -u postgres openssl req -new -nodes \
  -text -out server.csr -keyout server.key \
  -subj "/C=CN/O=Example/CN=${HOSTNAME_FQDN}"

# SAN
cat > server.ext <<EOF
basicConstraints = CA:FALSE
subjectAltName = DNS:${HOSTNAME_FQDN}, DNS:localhost, IP:${SERVER_IP}, IP:127.0.0.1
extendedKeyUsage = serverAuth
EOF

# 签发
sudo -u postgres openssl x509 -req -in server.csr -days "${DAYS}" \
  -CA root.crt -CAkey root.key -CAcreateserial \
  -extfile server.ext -out server.crt

# 权限
sudo chown postgres:postgres server.crt server.key root.crt
sudo chmod 600 server.key
sudo chmod 644 server.crt root.crt

# 校验
openssl verify -CAfile root.crt server.crt
openssl x509 -in server.crt -noout -ext subjectAltName
ini
# 第 3 步:配置 postgresql.conf
# ssl = on
# ssl_cert_file = '/etc/postgresql/18/ssl/server.crt'
# ssl_key_file  = '/etc/postgresql/18/ssl/server.key'
# ssl_ca_file   = '/etc/postgresql/18/ssl/root.crt'
# ssl_min_protocol_version = 'TLSv1.2'
bash
# 第 4 步:重启
sudo systemctl restart postgresql
psql -d postgres -c "SHOW ssl;"        # on

# 第 5 步:从远程验证(用 verify-full + 证书)
scp postgres@db.example.com:/etc/postgresql/18/ssl/root.crt /etc/pki/root.crt

psql "host=db.example.com dbname=demo user=app_user \
      sslmode=verify-full sslrootcert=/etc/pki/root.crt" \
     -c "\conninfo"

示例二:验证「加密是否可被降级」 ​

bash
# 场景:pg_hba.conf 写了 hostssl,验证非 SSL 连接确实被拒
psql "host=db.example.com dbname=demo user=app_user sslmode=disable" -c "SELECT 1;"
# psql: error: ... FATAL:  no pg_hba.conf entry for host "10.10.5.20",
#       user "app_user", database "demo", no encryption
# 说明:hostssl 规则不匹配非 SSL 连接,且后续无放行规则 → 正确拒绝
bash
# 场景:客户端误用 prefer,验证在服务端关掉 SSL 后会静默降级
# 先用 prefer 连接并观察
psql "host=db.example.com dbname=demo user=app_user sslmode=prefer" -c "\conninfo"
# 输出含 "SSL connection" → 说明加密生效

# 若服务端 ssl = off,同样的命令不会报错,而是以明文连接
# 这正是 prefer 的风险:失败被静默掩盖
sql
-- 服务端复核:是否存在未加密的远程连接
SELECT pid, usename, client_addr, ssl, state
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND client_addr IS NOT NULL
  AND ssl IS NOT TRUE;
-- 期望结果:0 行(本地回环除外,可加过滤条件排除)

示例三:证书到期监控 ​

bash
# 自签名证书默认 3650 天,但一轮换就容易被遗忘。做一次到期检查:
openssl x509 -in /etc/postgresql/18/ssl/server.crt -noout -enddate
# notAfter=Sep 19 12:00:00 2036 GMT

# 计算剩余天数(低于 30 天告警)
END=$(openssl x509 -in /etc/postgresql/18/ssl/server.crt -noout -enddate | cut -d= -f2)
END_TS=$(date -j -f "%b %d %T %Y %Z" "${END}" +%s 2>/dev/null || date -d "${END}" +%s)
NOW_TS=$(date +%s)
echo "证书剩余天数: $(( (END_TS - NOW_TS) / 86400 ))"
bash
#!/usr/bin/env bash
# check-ssl.sh:SSL 状态与证书有效期巡检
set -euo pipefail

PSQL="psql -d postgres -tAc"

echo "── 1) 服务端 SSL 是否启用 ──"
[ "$(${PSQL} 'SHOW ssl;')" = "on" ] || { echo "失败:ssl 未启用"; exit 1; }

echo "── 2) 未加密的远程连接清单(期望为空)──"
${PSQL} "SELECT pid || ' ' || usename || ' from ' || client_addr
         FROM pg_stat_activity
         WHERE backend_type = 'client backend'
           AND client_addr IS NOT NULL
           AND client_addr NOT IN ('127.0.0.1','::1')
           AND ssl IS NOT TRUE;"

echo "── 3) 是否存在明文认证规则 ──"
${PSQL} "SELECT line_number || ': ' || type || ' ' || auth_method
         FROM pg_hba_file_rules
         WHERE type = 'host' AND auth_method LIKE '%scram%'
           AND address NOT IN ('127.0.0.1/32','::1/128');"

echo "── 4) 服务端证书剩余有效期 ──"
CERT=$(${PSQL} "SHOW ssl_cert_file;" | tr -d ' ')
openssl x509 -in "${CERT}" -noout -subject -enddate

echo "检查完成"

注意事项 ​

  • server.key 权限必须为 600:这是最常见的启动失败原因。若报 private key file has group or world access,执行 chmod 600 server.key 即可。
  • SAN 必须覆盖实际连接使用的主机名 / IP:现代 TLS 客户端(含 libpq、JDBC、浏览器)已不再依赖 CN 做主机名校验。若 verify-full 报 certificate does not match host name,几乎都是 SAN 没写对。用 IP 连接就必须把 IP 加进 SAN。
  • sslmode=require 不等于安全:它只保证加密,不校验对端身份。生产必须用 verify-full,并确保 sslrootcert 指向正确的 CA。
  • prefer 是默认值,且会静默降级:不显式设置 sslmode 时,服务端不支持 SSL 也不会报错。永远显式指定。
  • ssl = on 需要重启:这是 postgresql.conf 中需重启的参数之一,reload 不生效。
  • hostssl 不拦截非 SSL:它只是「只匹配 SSL 连接」。要拒绝非 SSL 连接,需额外写 hostnossl ... reject 或依赖「无匹配即拒绝」。
  • 证书轮换后需 reload(部分参数需重启):替换 server.crt / server.key 后,PostgreSQL 在下次连接时会读取新证书(取决于版本行为),但最稳妥的做法是重启或 SELECT pg_reload_conf() 后新建连接验证。
  • ssl_ca_file 配置后行为会变:一旦设置了 ssl_ca_file(root.crt),客户端若提供证书就会被校验。调试 cert 认证时若证书链不完整,会报 certificate verify failed。
  • 不要把 CA 私钥留在服务端:root.key 签完证书后应移出服务器(或至少离线保管)。留在 PGDATA 附近意味着任何人拿到它都能签发任意客户端证书。
  • 自签名证书的信任分发:客户端需要拿到 root.crt 才能 verify-full。分发通道必须可信,否则「信任根」本身就被污染了。
  • 加密不改变权限模型:SSL 只保护链路,GRANT 与 pg_hba.conf 的配置错误仍然会造成越权,二者不可互相替代。
bash
# 常见错误排查命令
# 1) 看服务端证书内容与 SAN
openssl x509 -in server.crt -noout -text | grep -A1 "Subject Alternative Name"
# 2) 测试 TLS 握手与证书链(不涉及 PostgreSQL 协议)
openssl s_client -connect db.example.com:5432 -starttls postgres -CAfile root.crt
# 3) 客户端详细日志(libpq)
PGSSLMODE=verify-full PGROOTCERT=/etc/pki/root.crt psql "host=db.example.com dbname=demo user=app_user" -c "SELECT 1;"

最佳实践 ​

  • 生产一律 verify-full:sslmode=verify-full + 正确的 sslrootcert,让「加密」与「身份校验」同时到位。
  • pg_hba.conf 优先使用 hostssl:让加密成为默认路径,非 SSL 连接用 hostnossl ... reject 显式挡住并留下清晰日志。
  • 隔离私钥:server.key 权限 600、目录 700;CA 私钥生成完客户端证书后离线保存,不留在数据库服务器。
  • 证书有效期与轮换自动化:把到期检查纳入监控(剩余 30 天告警),轮换流程写成脚本并演练一次,避免到期时手忙脚乱。
  • 合规环境收紧协议版本:ssl_min_protocol_version = 'TLSv1.2'(甚至 TLSv1.3),淘汰 TLS 1.0/1.1。
  • 内网也加密:不要以「内网可信」为由跳过 SSL。内网横向移动是真实存在的攻击路径,且合规审计通常要求全链路加密。
  • 客户端证书优于共享口令:在服务器间通信场景,cert 认证免去了口令分发与轮换,且可与主机身份绑定。
  • 把 SSL 状态纳入巡检:定期查询 pg_stat_activity.ssl,确认没有非预期的明文连接;发现即排查。
sql
-- 巡检:明确列出「未加密的远程连接」作为告警项
SELECT pid,
       usename,
       application_name,
       client_addr,
       state,
       now() - backend_start AS conn_age
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND client_addr IS NOT NULL
  AND client_addr NOT IN ('127.0.0.1', '::1')
  AND ssl IS NOT TRUE
ORDER BY conn_age DESC;

下一节

继续学习:PostgreSQL 架构概览

参考链接 ​