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 subjectAltNamebash
# ── 权限必须严格,否则 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.logsql
-- 确认服务端 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-256bash
# 改 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.crttext
# 服务端 pg_hba.conf:要求客户端证书
# 注意 clientcert=verify-full 会同时校验 CN 与用户名
hostssl demo app_user 10.10.0.0/16 cert clientcert=verify-fullbash
# 客户端连接(无需密码,凭证书认证)
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 subjectAltNameini
# 第 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 架构概览