Skip to content

log_connections / log_disconnections ​

如果说语句日志回答的是「数据库里执行了什么」,那么连接日志回答的就是「谁进来了、什么时候离开的」。log_connections 与 log_disconnections 这对开关负责记录每一次客户端连接的建立与断开,是审计链路里最靠近「边界」的一环:IP 伪造不了 TCP 源地址、绕不开认证握手,因此连接日志是追溯账号滥用、定位异常连接源最可靠的证据。

这对参数默认都是关闭的。许多团队把语句日志开得很重,却对「谁连了进来」一无所知——而现实中的安全事件里,「用失窃的账号在凌晨三点从陌生网段连进来」远比「在授权会话里跑了一条诡异 SQL」常见。连接日志便宜(每条连接只产生两三行日志,与流量无关)、难以规避(不依赖语句内容),是性价比最高的一类审计日志。

本页覆盖两个参数的行为与日志形态、认证失败的记录机制、pg_stat_database 的会话统计、连接池场景下的日志稀释问题,以及把连接日志接入 ELK / Loki 集中分析的思路,最后用一个「排查异常连接来源」的实战把它们串起来。

前置知识

建议先阅读:log_statement 语句日志

基础概念 ​

连接审计涉及的核心参数与视图如下:

参数 / 视图默认值作用
log_connectionsoff记录每次连接尝试与认证授权完成
log_disconnectionsoff记录会话结束及会话时长
log_line_prefix—日志行首元信息,连接日志归因的前提
log_checkpointsPG15 起默认 on记录 checkpoint 活动(运维事件,非连接事件)
pg_stat_database.sessions—每个数据库的会话总数等统计(PG14+)
pg_stat_database.stats_reset—统计上次重置时间

连接日志的两段式记录 ​

开启 log_connections 后,一次成功的连接会在日志中留下两条记录:

  1. connection received: —— 服务器收到连接请求,记录来源 host 与端口。这一步发生在认证之前,无论后续认证是否成功(配合开关),它都能告诉你「谁在敲门」。
  2. connection authorized: —— 认证与授权完成,记录用户名、数据库名,以及客户端自报的 application_name。

两段式的设计意味着:一段日志负责「来源」,一段日志负责「身份」。把两段用进程号(%p)串起来,就得到一条完整的连接证据链。

断开日志与会话时长 ​

log_disconnections 在会话结束时记录一条 disconnection:,其中包含会话总时长:

text
LOG:  disconnection: session time: 0:41:02.871 user=app database=appdb host=10.0.3.17

对审计而言,会话时长是一个很有用的行为特征:正常的应用连接是「短连接高频、分钟级」,而「挂了 8 小时的交互式 psql 会话」「只在凌晨出现的短会话」都是值得关注的异常模式。

认证失败:不需要开关的日志 ​

一个常见误解是「不开 log_connections 就看不到登录失败」。事实相反:认证失败的错误消息始终会以 LOG / FATAL 级别写入服务器日志,与这两个开关无关:

text
2026-10-07 03:12:44.101 CST [31112] LOG:  connection received: host=203.0.113.9 port=60122
2026-10-07 03:12:44.355 CST [31112] FATAL:  password authentication failed for user "postgres"

也就是说,暴力破解尝试天然会被记录,不需要任何额外配置。log_connections 增补的是「成功连接」的可见性——两者合起来才是完整的连接审计面。

连接生命周期与日志时点 ​

把一次连接从建立到销毁的全部日志时点画在一条时间线上,排障与审计时按图索骥:

text
客户端 ──────TCP 建连──────► 服务器
   │
   ├─ (1) connection received: host=… port=…        ← log_connections
   │
   ├─ (2) 认证:匹配 pg_hba.conf → scram-sha-256 / cert …
   │        ├─ 失败:FATAL password authentication failed   ← 恒定落盘
   │        └─ 成功 ↓
   ├─ (3) connection authorized: user=… db=… app=…  ← log_connections
   │
   ├─ (4) 会话期间:语句日志 / pgaudit / 错误消息
   │
   └─ (5) disconnection: session time: …            ← log_disconnections
时点日志行触发条件回答的问题
(1)connection received:log_connections = on谁在敲门
(2) 失败FATAL: password authentication failed恒定谁在试图冒充谁
(3)connection authorized:log_connections = on谁连上了哪个库
(5)disconnection:log_disconnections = on会话活了多久

(1) 与 (3) 之间可能存在秒级间隔——认证握手、TLS 协商、外部认证服务(LDAP / Kerberos)都可能耗时。排查「登录慢」时对比两个时间戳,即可判断耗时发生在认证阶段还是连接排队阶段。

语法与用法 ​

基础配置 ​

conf
# postgresql.conf

# 记录连接建立(received + authorized 两段)
log_connections = on

# 记录会话结束与会话时长
log_disconnections = on

# 行首前缀:时间 [进程号] 用户@库 —— 连接日志归因的最低要求
log_line_prefix = '%m [%p] %q%u@%d '

# (可选)日志输出形式;PG15+ 可选 jsonlog 便于机读采集
logging_collector = on
log_destination = 'stderr'

这两个参数不能在会话内用 SET 临时修改,需要写入配置并重载:

bash
pg_ctl reload -D /var/lib/postgresql/18/main
# 或
psql -U postgres -c "SELECT pg_reload_conf();"
sql
-- 验证
SHOW log_connections;
SHOW log_disconnections;

成功连接的完整日志形态 ​

text
2026-10-07 11:02:18.553 CST [31200] LOG:  connection received: host=10.0.3.17 port=52913
2026-10-07 11:02:18.590 CST [31200] LOG:  connection authorized: user=app database=appdb application_name=order-service
2026-10-07 11:02:59.441 CST [31200] app@appdb LOG:  disconnection: session time: 0:00:40.887 user=app database=appdb host=10.0.3.17

注意第二行与第三行因为 log_line_prefix 里的 %u@%d 已经带上了用户与库名;第一行(received)发生在认证之前,用户与库还是未知的,这正是 %q 哨兵让 %u@%d 自动省略的效果——不会输出空字段。

会话内自证来源的 SQL ​

审计不止可以看日志,连接里的会话自己也能报告来源信息,用于现场核对与脚本取证:

sql
SELECT inet_client_addr()  AS client_ip,
       inet_client_port()  AS client_port,
       session_user        AS login_role,
       current_user        AS current_role,
       current_database()  AS dbname,
       current_setting('application_name') AS app_name,
       pg_backend_pid()    AS pid;
text
 client_ip | client_port | login_role | current_role | dbname |  app_name  |  pid
-----------+-------------+------------+--------------+--------+------------+-------
 10.0.3.17 |       53120 | app        | app          | appdb  | order-svc  | 31901

session_user 是认证登录用的角色(谁连进来的),current_user 是当前生效角色(SET ROLE 之后会变化)——审计口径应优先用 session_user,防止换壳后归因错位。本地 Unix socket 连接时 inet_client_addr() 返回 NULL,这是识别「本机连接」的简单方法。这组函数在业务留痕场景也常用:把来源 IP 直接写进操作的审计表,比事后翻日志便宜。

会话统计视图 ​

sql
-- 每个数据库的会话计数与时长统计(PG14+ 列)
SELECT datname,
       numbackends,
       sessions,
       sessions_abandoned,
       sessions_fatal,
       sessions_killed,
       round(sessions_time::numeric / 1000, 1) AS total_session_seconds,
       stats_reset
FROM pg_stat_database
WHERE datname NOT LIKE 'template%';
text
  datname  | numbackends | sessions | sessions_abandoned | sessions_fatal | sessions_killed | total_session_seconds |        stats_reset
-----------+-------------+----------+--------------------+----------------+-----------------+-----------------------+------------------------------
 appdb     |           6 |   419882 |                  3 |             11 |               7 |         1.0368742e+09 | 2026-09-01 00:00:00.00008+08
 postgres  |           1 |     2931 |                  0 |              2 |               1 |             41210.881 | 2026-09-01 00:00:00.00008+08

sessions_abandoned(客户端异常断开)、sessions_fatal(服务端 FATAL 终止)、sessions_killed(被 pg_terminate_backend 等主动终止)三个计数器把「异常断开的会话」从总量里拆了出来,是连接健康度的直接指标。

详细说明 ​

两段日志分别回答什么问题 ​

connection received 与 connection authorized 的分工值得再强调一遍,因为它们在审计中的用法不同:

日志段时机关键字段回答的问题
connection received:认证前host、port谁在敲门?来自哪个网段?敲门频率是否异常?
connection authorized:认证后user、database、application_name哪个账号连上了哪个库?自称是什么应用?

只关心「扫描与爆破」时,received 段就够了;追溯「已授权账号的行为」时,需要把 authorized 段与该进程后续的语句日志按 PID 关联。这也是 log_line_prefix 必须包含 %p 的原因——PID 是把同一次连接的多行日志串成证据链的钥匙。

跨行关联:会话 ID 与行号 ​

PID 之外,log_line_prefix 还有两个专门为跨行关联设计的转义符:

  • %c:会话 ID,形如 689e4c1e.5618(前半是会话开始的十六进制时间戳,后半是 PID 的十六进制)。跨日志轮转、跨主机的场景下比裸 PID 更稳——PID 会被操作系统复用。
  • %l:每个会话内的日志行号,用于确认「有没有丢行」,也是判断采集链路是否截断的依据。

启用两者的前缀写法与日志形态:

conf
log_line_prefix = '%m [%p] %c-%l %q%u@%d '
text
2026-10-07 11:02:18.553 CST [31200] 689e4c1e.7a20-1 LOG:  connection received: host=10.0.3.17 port=53120
2026-10-07 11:02:18.590 CST [31200] 689e4c1e.7a20-2 LOG:  connection authorized: user=app database=appdb application_name=order-service

实践建议:stderr 文本日志的实例加 %c 收益明显(跨轮转文件追踪一次事件的所有行);接入 ELK / Loki 的 jsonlog 实例自带 session_id 结构化字段,无需重复。

会话时长的统计口径 ​

disconnection: 里的 session time 从会话进程启动(收到连接)计起,到会话结束为止,包含认证耗时、空闲等待与事务执行的全部时间。因此:

  • 「短会话 + 高频连接」模式(每会话几秒)通常意味着应用没有使用持久连接或连接池,审计上要结合业务确认是否为预期行为。
  • 「超长会话 + 近零活动」要结合 pg_stat_activity 的 state(idle / idle in transaction)判断是悬挂连接还是滞留的事务。
  • idle_in_transaction_session_timeout(以及 PG14+ 的 idle_session_timeout)触发的终止会以 FATAL 级别写入日志(例如空闲事务超时导致连接被强制结束),配合断开日志可以完整还原「会话为什么死掉了」。

认证失败日志的边界 ​

失败认证永远落盘,但有两点需要知道:

  • FATAL: password authentication failed for user "xxx" 是固定形态,配合 connection received 行可以还原「哪个 IP 在对哪个账号尝试口令」。据此可以统计爆破源的 Top N 并反馈到网络层封禁。
  • 如果还想看到「这次失败匹配了 pg_hba.conf 的哪一行」,需要更高的调试级别(详情信息在调试级别日志中输出),生产环境一般不为这个目的调低日志阈值,用 log_connections + 失败消息已经足够。

pg_hba.conf 与连接审计的分工 ​

连接日志记录「发生了什么」,pg_hba.conf 决定「允许发生什么」,两者配合才是完整的边界管控:

conf
# pg_hba.conf 片段:最小网段 + 强认证 + 显式拒绝
# TYPE    DATABASE    USER        ADDRESS          METHOD
hostssl   appdb       app         10.0.3.0/24      scram-sha-256
hostssl   appdb       ops_admin   10.0.8.0/28      cert
host      all         all         0.0.0.0/0        reject

工程要点:

  • 规则自上而下首次匹配:被前面的规则拦下的连接不会落到后面的规则;「为什么这个 IP 连不上」的排查要按顺序逐行核对。
  • reject 收尾:显式拒绝兜底让边界意图在配置里可读,也避免依赖隐式的默认拒绝行为。
  • hostssl 强制 TLS:传输加密与连接审计共同构成边界,与安全章节 SSL 相关页面呼应。
  • 认证方法演进:口令认证推荐 scram-sha-256(由 password_encryption 控制存储格式);证书认证(cert)把身份绑定到客户端证书,来源可信度更高。

审计侧的用法:把 pg_hba 的规则清单维护成「合法来源 × 账号 × 方法」基线,连接日志里出现基线外的组合即告警——规则文件定义边界,日志验证边界真的在起作用。

连接审计能回答的四类问题 ​

把日志形态与审计需求对齐,连接日志主要覆盖:

  1. 异常连接源分析:新 IP、陌生网段、非业务时段的连接。做法是把 connection authorized 行按 host 聚合,与业务网段清单比对。
  2. 账号滥用发现:交互式运维账号(如 postgres、dba)被应用服务器使用、单一账号从几十个来源同时连接、application_name 与账号的业务角色不匹配(例如 app_rw 账号自报 psql)。
  3. 权限回收的验证:离职人员的账号、下线的旧服务,从连接日志里可以直接看到「最后一次连接是什么时候」,为账号清理提供依据。
  4. 会话行为画像:结合会话时长与语句日志,区分「人类交互会话」与「程序会话」,为不同会话类型匹配不同的语句审计挡位。

pg_stat_database 的会话统计与重置 ​

sessions 系列列(PG14+)是累计计数器,与日志互补的用法是:

  • 日志回答「每一次」的问题,计数器回答「趋势」的问题。例如 sessions_killed 突增说明有人(或监控脚本)在大量终止会话,先看计数器发现异常,再去日志里找 PID 与 SQLSTATE。
  • stats_reset 标记统计上次清零的时间,做周期性快照(如每小时一次存入监控库)后求差,即可得到各数据库每小时的会话增速,这比解析日志便宜得多。
  • 重置用 pg_stat_reset_shared('database')(重置集簇级统计)——注意它会影响所有基于累计值的监控基线,安排在维护窗口执行。

与 pg_stat_activity 的配合 ​

pg_stat_activity 是「现在进行时」,连接日志是「过去时」。排查进行中的可疑连接时,两者相互印证:

sql
SELECT pid,
       usename,
       datname,
       application_name,
       client_addr,
       backend_start,
       state,
       state_change,
       query
FROM pg_stat_activity
WHERE backend_type = 'client backend'
ORDER BY backend_start;

backend_start 是该后端会话的建立时间,与日志里的 connection received/authorized 时间一一对应;client_addr 与日志的 host 对应。审计回溯时,先在日志中锁定可疑 PID,再用 PID 查 pg_stat_activity(若会话还活着)拿到它正在执行的语句。

IPv6 与 NAT 环境的来源记录 ​

两个网络环境细节会影响来源分析的准确性:

  • IPv6 地址:%h 与结构化日志的 remote_host 可能出现 IPv6 长地址,聚合脚本的正则要同时兼容 IPv4 / IPv6。log_hostname = on 时 %h 会输出反向解析的主机名——它引入 DNS 查询开销与依赖,一般保持默认 off 用 IP 归因,主机名交给 CMDB / 资产表映射。
  • NAT / 出口网段:经过 NAT 的连接在日志里表现为网关 IP,大量客户端共享同一来源。此时来源分析要下沉到应用层(application_name、request id),或让各业务子网使用独立出口——这在规划网络时就值得提出来,而不是审计失效后再改。

连接池场景下的日志稀释 ​

连接池会把「谁在用」稀释掉

启用 PgBouncer 等连接池后,日志里看到的是池子与数据库之间的连接:几十个应用真实会话复用少数后端连接,connection authorized 记录的 user 是池子配置的服务端用户,来源永远是池子所在的主机。每一条后端连接背后可能是任意一个客户端。

应对思路:

  • 让客户端身份穿透池子:PgBouncer 会在建立后端连接时把客户端的 application_name 透传给服务器(配合服务器端 application_name_add_host = on 还能附上来源主机),这样日志与 pg_stat_activity 中能区分「是哪个应用接的入」。事务级/语句级池化下,会话级信息仍然有限,这是池化模式本身的代价。
  • 审计重点前移:池子场景下,数据库侧的连接日志负责「池子 ↔ 数据库」这一段,客户端到池子那一段的审计要靠池子自己的日志(PgBouncer 有独立的连接日志)。
  • 压低连接日志的噪音预期:池子会显著降低连接建立频率,这既是日志稀释也是日志减负——连接审计在池化架构中的权重下降,语句审计(前两页)与对象审计(pgaudit)的权重相应上升。连接池的选型与行为详见连接池章节的 PgBouncer 一页。

认证超时与慢连接的治理 ​

连接日志里有一类容易被误判的记录:连接建立后迟迟不完成认证。这类会话由 authentication_timeout(默认 1min)兜底——认证流程超时后,后端进程被强制终止并留下 FATAL 级日志(措辞形如 authentication timeout,随版本略有差异):

conf
# postgresql.conf:认证窗口
authentication_timeout = 1min    # 认证必须在 1 分钟内完成(默认值)

审计视角下,这类记录有明确的解读路径:

  • 负载均衡与健康检查探针:只建 TCP 连接不发认证包,会持续产生 timeout 记录——把这些来源 IP 登记为白名单,并让探活配置做更完整的探测,比在告警里挨个排除省心。
  • 端口扫描:陌生来源的 timeout 记录就是扫描行为,与 connection received 行合并统计即得「敲门但进不来」的清单。
  • 网络故障:应用服务器到数据库之间的 MTU、防火墙会话老化问题也会以半开连接的形式出现,特征是集中在特定网段并伴随重传。

用一条查询排查「疑似停滞在认证阶段」的连接——未完成认证的会话在 pg_stat_activity 中没有用户名:

sql
SELECT pid, client_addr, backend_start,
       now() - backend_start AS age
FROM pg_stat_activity
WHERE backend_type = 'client backend'
  AND usename IS NULL
ORDER BY age DESC;

与它互补的是会话建立后的两条超时线:idle_in_transaction_session_timeout(悬挂事务)与 idle_session_timeout(PG14+,纯空闲会话),加上客户端侧的 connect_timeout,共同构成「连接生命周期的时间边界」。边界触发的终止都可在日志中按 PID / 会话 ID 还原全过程——这正是前文强调统一前缀的又一处回报。

连接日志里的 TLS 信息 ​

log_connections 开启时,成功授权的连接行会附带 TLS 信息(协议版本、套件、密钥位数,字段随版本略有差异),让「谁在用什么加密方式连我」变得可盘点:

text
LOG:  connection authorized: user=app_ro database=appdb address=10.0.2.15:52100
     application_name=billing SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off)

三个直接用途:

  • TLS 治理清单:正则提取 SSL connection 出现的行,与全量授权行做差集,得到「仍在明文连接的账号 × 来源」清单——这就是 SSL 章节「传输加密基线」的落地核对手段。
  • 证书轮换验证:轮换证书后观察新连接行的协议版本与套件,确认协商结果符合预期(例如禁用旧协议版本后不再出现 TLSv1.2)。
  • 合规抽查:审计方常问「传输是否加密」,这条日志行就是实例级的直接证据。

局限:该信息只出现在成功授权的连接行,认证失败的连接没有;「明文连接清单」要基于授权行全量做差集,不能只扫带 SSL 字样的行。

注意事项 ​

log_checkpoints 与运维日志的边界 ​

log_checkpoints 记录的是 checkpoint 的开始/结束与写入统计,属于「运维事件日志」,与连接审计无关,但它默认值的变化值得一提:PG15 起该参数默认值为 on(此前默认 off),因此新版本实例的日志里会周期性出现 checkpoint 记录。理解这一点有两个意义:一是做日志量预算时要把这部分算进去;二是做日志分析(比如统计连接行数)时,要按消息模式过滤,不能简单按行数估算连接量。

日志聚合:ELK / Loki 概述 ​

连接日志的价值随「留存时长」和「可检索性」增长,单机文本文件很快不够用。通用做法是把 PostgreSQL 日志接入集中式日志平台:

  • 前置条件:统一 log_line_prefix,或直接启用 log_destination = 'jsonlog'(PG15+)让每行日志天然带结构化字段(user、dbname、remote_host、session_id 等),采集与解析成本大幅降低。PG15 之前常用 csvlog。
  • 采集链路:Filebeat / Fluent Bit / Vector / Promtail 等采集器负责文件尾随与转发,rsyslog/syslog 也是经典方案(log_destination = 'syslog')。
  • ELK(Elasticsearch + Logstash + Kibana):适合全文检索与复杂聚合,按 host、user、application_name 做连接来源的时序分析都很方便。
  • Grafana Loki:标签化 + 轻量存储,配合 LogQL 可以快速做出「每分钟连接数按来源 IP 分布」这类看板,资源占用低。

平台选型不是本页重点;关键约束是:审计日志的集中存储要有保留期策略与访问控制,并且 PostgreSQL 主机上的本地文件在采集确认成功之前不要清理。

监控与告警的落地模式 ​

连接日志的告警指标不需要复杂推导,四条规则覆盖绝大多数异常:

text
rule 1  auth_fail_rate
        同一 remote_host 认证失败 > 10 次 / 1 分钟        → P2 + 自动封禁

rule 2  new_source
        (remote_host, user, application_name) 组合首次出现 → P3 通知

rule 3  offhours_login
        运维账号清单内的 authorized 时间在 09:00-20:00 之外 → P2

rule 4  conn_storm
        单来源授权连接速率 > 基线均值 × 3                  → P3 排查

落地步骤:先跑两周「只记录不告警」的观察期,统计各指标的正常分布;据此定阈值与白名单;上线后每月复核误报率并回写规则。规则 2(新组合告警)是最省心的通用规则——基线即白名单,不需要为每类异常单独建模。

实战示例 ​

示例一:开启连接审计并验证两段日志 ​

conf
# postgresql.conf
log_connections = on
log_disconnections = on
log_line_prefix = '%m [%p] %q%u@%d '
bash
psql -U postgres -c "SELECT pg_reload_conf();"
bash
# 从另一台机器发起一次连接
psql "host=pg-prod-1 dbname=appdb user=app" -c "SELECT 1;"
text
2026-10-07 11:30:02.101 CST [31901] LOG:  connection received: host=10.0.3.17 port=53120
2026-10-07 11:30:02.130 CST [31901] LOG:  connection authorized: user=app database=appdb application_name=psql
2026-10-07 11:30:02.135 CST [31901] app@appdb LOG:  disconnection: session time: 0:00:00.034 user=app database=appdb host=10.0.3.17

三行日志构成一条完整证据:来源 IP → 账号/库/应用 → 会话时长。

示例二:统计连接来源 Top N 与失败认证 Top N ​

bash
# 今天的授权连接按来源 IP 聚合
grep 'connection authorized' postgresql-2026-10-07.log \
  | sed -E 's/.*host=([0-9a-fA-F:.]+).*/\1/' \
  | sort | uniq -c | sort -rn | head
text
   5312 host=10.0.3.17
   2240 host=10.0.3.18
     61 host=10.0.8.4
      2 host=203.0.113.9
bash
# 认证失败按 (host, user) 聚合 —— 直接暴露爆破源与被尝试的账号
grep 'password authentication failed' postgresql-2026-10-07.log \
  | sed -E 's/.*host=([0-9a-fA-F:.]+).*user "([^"]+)".*/\1 \2/' \
  | sort | uniq -c | sort -rn | head
text
     318 host=203.0.113.9 user "postgres"
      47 host=203.0.113.9 user "admin"
       2 host=10.0.3.17 user "app"

结果里 203.0.113.9 对 postgres 账号的数百次失败尝试就是典型的爆破行为,应立即反馈到防火墙与 pg_hba.conf 的访问控制(如收紧允许网段)。

示例三:排查一条可疑连接的完整轨迹 ​

假设监控发现 10.0.8.4(本不在业务网段清单内)建立了连接:

bash
# 1. 从日志锁定 PID 与身份
grep '203.0.113\|10.0.8.4' postgresql-2026-10-07.log | head
text
2026-10-07 03:41:15.922 CST [32177] LOG:  connection received: host=10.0.8.4 port=55817
2026-10-07 03:41:15.940 CST [32177] LOG:  connection authorized: user=dba_ops database=appdb application_name=psql
2026-10-07 03:41:20.114 CST [32177] dba_ops@appdb LOG:  disconnection: session time: 0:00:04.192 user=dba_ops database=appdb host=10.0.8.4
bash
# 2. 用 PID 拉出该会话的语句日志(若 log_statement 对该角色开启)
grep '\[32177\]' postgresql-2026-10-07.log
text
2026-10-07 03:41:15.940 CST [32177] LOG:  connection authorized: user=dba_ops database=appdb application_name=psql
2026-10-07 03:41:16.010 CST [32177] dba_ops@appdb LOG:  statement: SELECT email, id_card FROM customers LIMIT 100;
2026-10-07 03:41:20.114 CST [32177] dba_ops@appdb LOG:  disconnection: session time: 0:00:04.192 user=dba_ops database=appdb host=10.0.8.4

凌晨、陌生网段、运维账号、批量读取敏感列——四个信号叠加,按事件响应流程立案处理:核对值班记录、冻结该账号、在交换机/防火墙侧溯源该主机。如果会话当时还在,用 SELECT * FROM pg_stat_activity WHERE pid = 32177; 可以直接看到正在执行的语句。

示例四:用会话统计监控异常终止趋势 ​

sql
-- 每小时快照一次,落到监控库做差值
CREATE TABLE db_conn_snap (
    taken_at timestamptz NOT NULL DEFAULT now(),
    datname text NOT NULL,
    sessions bigint,
    sessions_abandoned bigint,
    sessions_fatal bigint,
    sessions_killed bigint
);

INSERT INTO db_conn_snap (datname, sessions, sessions_abandoned, sessions_fatal, sessions_killed)
SELECT datname, sessions, sessions_abandoned, sessions_fatal, sessions_killed
FROM pg_stat_database
WHERE datname = 'appdb';
sql
-- 最近一小时的异常终止增量
SELECT sum(sessions_fatal)  AS fatal_delta,
       sum(sessions_killed) AS killed_delta,
       sum(sessions_abandoned) AS abandoned_delta
FROM db_conn_snap
WHERE taken_at > now() - interval '1 hour';

计数器突增再去日志里按时间窗口捞 FATAL 行,定位成本远低于在日志里盲目搜索。

示例五:jsonlog 采集链路的端到端验证 ​

conf
log_destination = 'jsonlog'
logging_collector = on
log_connections = on
log_disconnections = on

reload 后发起一次测试连接,日志文件应出现 JSON 行:

text
{"timestamp":"2026-10-07 12:10:02.101 CST","user":"app","dbname":"appdb",
 "pid":32201,"remote_host":"10.0.3.17","session_id":"689e58aa.7db9",
 "level":"LOG","message":"connection authorized: user=app database=appdb application_name=order-service"}

采集管道示意(Promtail / Vector / Filebeat 类工具,配置语法以各工具文档为准):

text
json 日志文件 → 解析 JSON → 保留 user / dbname / remote_host / session_id 为标签
        → 按 message 前缀分流:connection 行 / 语句行 / AUDIT 行
        → 写入日志平台(Loki / Elasticsearch),按天建索引

验证清单:平台里能按 remote_host 聚合出当日连接数;能按 session_id 拉出单会话的全部日志行;人为断开采集器 5 分钟,恢复后确认缺口告警触发或有补采。三步都通过,采集链路才算验收。

示例六:连接来源基线的维护 ​

sql
CREATE TABLE conn_baseline (
    remote_host  inet        NOT NULL,
    usename      text         NOT NULL,
    app_name     text         NOT NULL,
    approved_by  text         NOT NULL,
    approved_at  timestamptz  NOT NULL DEFAULT now(),
    PRIMARY KEY (remote_host, usename, app_name)
);

INSERT INTO conn_baseline (remote_host, usename, app_name, approved_by) VALUES
  ('10.0.3.17', 'app',       'order-service', 'sec-review-2026-09'),
  ('10.0.3.18', 'app',       'order-service', 'sec-review-2026-09'),
  ('10.0.8.2',  'ops_admin', 'pgadmin',       'sec-review-2026-09');
sql
-- 每日核对:采集端按天汇总的 (host, user, app) 组合与基线做差
-- (audit_conn_daily 为日志平台侧回灌的每日汇总表)
SELECT d.remote_host, d.usename, d.app_name, d.conns
FROM audit_conn_daily d
LEFT JOIN conn_baseline b
  ON  b.remote_host = d.remote_host
  AND b.usename     = d.usename
  AND b.app_name    = d.app_name
WHERE d.day = current_date - 1
  AND b.remote_host IS NULL
ORDER BY d.conns DESC;

基线外组合按约定处置:业务负责人认领 → 补基线;无人认领 → 按事件流程核查。基线表本身纳入变更管理,每次增删都记录审批人与原因。

注意事项 ​

这两个参数不能在会话里临时打开。 log_connections / log_disconnections 只能在配置文件(或启动命令行)中设置,reload 后对新建连接生效,已存在的会话不受影响。想「临时记录某个会话的连接信息」是不成立的,规划时按常态开启考虑。

connection authorized 的 application_name 是客户端自报的。 任何客户端都可以把 application_name 设成任何值,它有辅助归因价值但没有身份证明效力。审计结论要以账号 + 来源 IP + 语句内容为主,application_name 只作旁证。

连接池会稀释来源信息。 见前文:池化架构下日志里的 host 是池子主机、user 是池子的服务端账号。多租户池化环境中,把数据库侧连接审计与池子日志、应用层审计拼起来才有完整链路。

失败认证日志永远在,成功连接日志才需要开关。 这意味着即使什么都不配置,日志里也会持续出现失败尝试(公网暴露的实例尤其多)。给日志配额做预算时不要按「全关」假设,要用实测值。

日志量与连接频率成正比、与流量无关。 短连接风暴(每秒数百次建连)会把连接日志放大成主要噪音,这种场景优先治理应用连接方式(上池子),而不是关掉连接审计。

%q 哨兵要写进 log_line_prefix。 没有 %q 时,connection received 这类发生在认证前的日志行会因为取不到用户/库名而输出空字段或丑陋的空格段;写上 %q(如 %m [%p] %q%u@%d )让无会话上下文的日志自动省略这些字段。

集中采集不能替代本地留存。 采集链路可能中断、可能被绕过。本地日志保留期应满足合规最低要求,采集平台上的留存期可以更长;两者都要有访问控制——日志中含 IP、账号与业务数据影子。

sessions 统计是 PG14+ 的列。 低版本实例上 pg_stat_database 没有这些列,异常终止趋势只能依赖日志统计。升级规划时可以把这一列列为监控便利性的加分项。

log_hostname 会引入 DNS 依赖。 开启后每个连接都要做反向解析,DNS 抖动会拖慢建连;保持默认 off、用 IP 归因,主机名由资产管理系统映射。

认证失败的 pg_hba 匹配详情属于调试信息。 「失败对应哪一行规则」在调试级别日志中才可见,日常不要为此调低阈值;需要复盘时在测试环境复现。

来源 IP 合法不等于会话可信。 网段内任何主机都可能冒用同一账号——IP 是低置信度信号,高危操作仍需权限与语句审计双重约束。

连接日志不回答「为什么断开」。 disconnection 行给出会话时长,但断开原因(客户端主动、网络中断、超时参数触发)要结合同时段 FATAL / ERROR 行与时间轴判断,不要把每条断开都当异常。

「数据库在记」与「平台在收」要分别验证。 采集管道中断不会影响本地日志,却会让平台侧的连接审计静默失明;定期做端到端探针(建一条测试连接,确认日志平台可检索到对应行),连接审计的存活监控才算闭环。

最佳实践 ​

  1. 连接日志默认全程开启。 它的日志量小(每连接三行)、与业务流量无关、又是最难伪造的审计证据,是所有挡位里最没有理由关闭的一类。
  2. log_line_prefix 固定为 %m [%p] %q%u@%d ,接入集中日志的实例再加 %h、%c、%i。PID 与会话 ID 是串联证据链的钥匙,缺失后跨行关联基本靠猜。
  3. 把「连接来源基线」变成资产。 用采集平台维护一张「合法来源 × 账号 × 应用」的基线表,新出现组合触发告警——比事后翻日志有效得多。
  4. 对失败认证设置阈值告警。 单一来源对单一账号的失败次数超过阈值(如 10 次/分钟)即告警并考虑自动封禁;pg_hba.conf 遵循最小网段原则,为爆破提供的第一道拦截。
  5. 为运维账号与业务账号设定不同的会话画像。 运维账号应为「短时长、低频、来自跳板机」;业务账号应为「长连接、来自固定网段、application_name 稳定」。偏离画像的会话进入待核实清单。
  6. 用 pg_stat_database 的会话计数器做趋势监控,用日志做个案回溯。计数器便宜且适合做每小时快照,日志昂贵但细节完整,先计数器后日志。
  7. PG15+ 优先启用 jsonlog。 结构化日志让 ELK / Loki 的采集解析几乎零成本,连接、语句、错误三类日志可以共用同一条管道与同一套索引。
  8. 池化架构把连接审计重心上移。 数据库侧看「池子 ↔ 库」,池子日志看「客户端 ↔ 池子」,应用层看「用户 ↔ 应用」;三层日志用 request id / 会话时间轴对齐,缺一层就补一层。
  9. 定期用连接日志驱动账号清理。 每季度导出「账号最后连接时间」清单,超过阈值未使用的账号进入回收流程;离职账号的回收验证也以此为准。
  10. 连接审计与语句审计联动设计挡位。 连接日志负责「人」,语句日志负责「行为」;运维角色两者都开,业务角色连接必开、语句按挡位。不要出现「有连接没行为」或「有行为没连接」的半截证据链。
  11. 把连接日志纳入事件响应预案。 预置好「按 PID 提取会话全部日志」「按来源 IP 聚合当日连接」这类脚本(见实战示例二/三),事发时的响应速度以分钟计。

下一节

继续学习:pgaudit 扩展

参考链接 ​