Appearance
一次 VPN 访问收紧导致支付回调中断的事故复盘
事故背景
一次后台业务系统发生非法访问并造成资金损失后,公司开始对后台管理系统进行安全加固。
其中一项核心措施是:
后台管理系统不再允许直接从公网访问,统一通过 VPN 进入。
这项整改时间窗口较短,因此多个后台系统很快完成了网络访问策略收紧。
教育系统也在这次范围内。
从表面看,这个系统属于“后台系统”,内部业务人员平时通过后台页面处理订单、审核、查询等业务,因此把它放进 VPN 是合理的。
但当时忽略了一个关键事实:
这个应用虽然是后台系统,但它同时还承担了第三方支付渠道的公网回调入口。
也就是说,同一个系统实际上同时存在两类完全不同的访问入口:
text
后台管理入口
→ 内部人员访问
→ 适合通过 VPN 收紧
支付回调入口
→ 微信、支付宝、连连支付等第三方服务器访问
→ 必须保持公网可达当网络策略按照“整个系统”统一收紧以后,这两个不同信任边界被一起关进了 VPN。
事故由此产生。
故障发现
VPN 改造完成后,并没有立即出现明显的应用异常。
系统能够正常启动,业务人员也可以通过 VPN 登录后台。
真正的问题发生在支付完成后的异步通知链路。
两天后,客服人员首先反馈:
用户已经支付成功,但个人中心里的订单状态长时间仍然显示“未支付”。
客服平时会持续关注订单付款状态,因此最先发现了这一异常。
同时还有另一个旁证:
原本会同步业务动态的企微机器人,已经连续两天没有推送“订单付款成功”通知。
教育系统当时由单人同时承担研发、生产问题处理和部分运维配合工作。
VPN 改造完成后,没有独立的自动化机制持续验证支付回调链路是否仍然可用,因此问题没有被系统主动发现,而是最终由业务人员反馈暴露出来。
这次事故有一个很明显的特点:
定位很快,但发现很晚。
收到反馈后,第一时间联想到最近的 VPN 变更
收到客服反馈后,第一反应就是最近刚完成的 VPN 改造。
原因很直接。
用户已经完成支付,但系统订单仍然停留在待支付状态,这意味着:
text
用户支付动作可能已经完成
↓
但支付结果没有进入系统而教育系统中的支付状态主要依赖第三方渠道异步回调更新。
两天前又刚好做过一次全局网络入口收紧。
把这两个现象放在一起,故障方向就已经非常明确:
支付本身可能没有失败,真正中断的是支付成功后的 Callback 通知链路。
随后检查回调入口和网络策略,确认第三方公网请求已经无法进入该系统。
恢复 Callback 入口的公网可达性后,新产生的支付回调重新恢复正常。
但网络恢复只能解决后续请求。事故期间已经完成支付、却因为回调没有进入系统而仍停留在“待支付”状态的订单,还需要单独处理。
因此又筛选出事故时间窗口内的相关异常订单,并联系支付渠道对这些订单进行补偿回调,让系统重新收到支付结果通知并完成订单状态修复。
到这里,这次故障才真正完成恢复:
text
恢复 Callback 公网可达性
↓
后续回调恢复正常
↓
筛选事故期间异常订单
↓
渠道补偿回调
↓
历史订单状态修复根因:把“系统边界”误当成了“信任边界”
事故根因并不是 VPN 本身。
VPN 作为后台管理入口的访问控制手段没有问题。
真正的问题是:
网络策略是按照“这个系统是后台系统”来制定的,而不是按照“这个入口是谁访问、需要什么信任模型”来制定的。
教育系统虽然在部署上是同一个应用,但从访问职责上看至少包含两类入口。
Internal:后台管理入口
例如:
text
/admin/**
/management/**这类接口只供内部人员使用。
它们适合:
text
VPN
+
登录认证
+
权限控制并且不应该直接暴露到公网。
Public Callback:第三方渠道回调入口
例如:
text
/callback/wechat/**
/callback/alipay/**
/callback/lianlian/**这类接口的调用方不是内部人员,而是第三方支付渠道服务器。
它们无法进入企业 VPN,因此必须保持公网可达。
但“公网可达”并不意味着“无保护”。
它们应该采用另一套安全模型:
text
公网可达
+
严格验签
+
幂等处理
+
限流
+
必要的来源限制所以真正需要划分的不是:
text
这个系统是内网系统还是公网系统而是:
text
这个接口属于哪个信任边界为什么支付成功了,订单状态却没有变化
支付流程里,用户付款成功和业务系统更新订单状态通常不是同一件事。
简化后的链路是:
text
用户
↓
支付渠道
↓
支付成功
↓
支付渠道服务器
↓
POST Callback
↓
业务系统
↓
更新订单状态
↓
发送业务通知VPN 改造以后,前半段仍然正常:
text
用户 → 支付渠道 → 支付成功但后半段被网络策略切断:
text
支付渠道服务器
↓
Callback
↓
X 业务系统入口不可达因此产生了一个典型的状态分裂:
text
资金状态:已经支付
业务状态:仍然待支付企微机器人没有付款成功通知,也是同一个原因。
业务系统根本没有收到支付成功事件,自然也不会触发后续通知。
暴露出的第一个问题:变更前没有完成外部调用链梳理
这次网络变更的目标非常明确:
收紧后台管理系统的公网访问。
但在执行过程中,没有先把系统所有入口按调用方重新梳理一遍。
如果当时先列出:
text
谁从公网调用这个系统?
谁从 VPN 内调用这个系统?
哪些接口只供管理员使用?
哪些接口必须被第三方访问?支付回调会很快暴露出来。
因此这次事故留下的第一个直接经验是:
涉及防火墙、VPN、安全组、Nginx 入口等底层网络策略的变更,不能只看“系统名称”,必须先看真实流量方向。
暴露出的第二个问题:紧急变更也需要最小风险检查
这次安全整改的时间窗口较短,网络策略需要快速完成收紧。
这类场景很难进行长时间设计评审,但仍然应该保留一个最小检查清单。
即使只有很短的准备时间,至少需要确认:
text
1. 谁从公网调用我?
2. 我必须继续暴露哪些公网接口?
3. 哪些第三方依赖这些入口?
4. 变更完成后如何验证关键链路?
5. 出问题以后如何快速回滚?紧急变更不一定能做完整规划,但不能完全没有风险检查。
暴露出的第三个问题:关键链路过度依赖人工发现
这次事故真正值得警惕的不是“为什么没有马上猜到原因”。
实际上,一旦客服反馈出现,问题很快就和 VPN 变更关联起来了。
真正的问题是:
为什么支付回调已经中断两天,系统没有主动告诉维护者?
事故期间:
text
应用正常运行
后台可以正常登录
接口没有整体宕机因此普通的:
text
进程存活监控
CPU
内存
HTTP 200都无法发现这个问题。
真正应该关注的是业务链路监控。
例如:
text
支付订单创建量
支付成功查询量
Callback 到达量
订单从 TO_PAY → PAID 的转换量
付款成功通知量如果平时每天都有付款成功回调,而某个时间点以后突然变成:
text
0就应该触发告警。
因此这次事故进一步说明:
关键业务链路不能依赖某个人“记得去看”,应该尽可能通过自动化监控发现异常。
整改:重新划分 Internal 与 Public Callback 边界
事故恢复以后,并没有立即把整个应用拆成两个独立服务。
当时首先做的是重新梳理接口职责和访问边界。
逻辑上明确分为两类。
Internal
后台管理接口:
text
Internal
→ 管理员访问
→ VPN
→ 登录认证
→ 权限校验Public Callback
第三方回调:
text
Public Callback
→ 第三方渠道访问
→ 公网可达
→ 不走后台登录态
→ 必须独立进行请求真实性校验这是一种逻辑边界收敛。
即使它们暂时仍然部署在同一个 Spring Boot 应用里,网络和认证策略也不能继续按照“整个系统一套规则”处理。
后续在接口路径设计上,也应该明确区分职责,例如:
text
/api/**
→ 页面和业务接口
/callback/**
→ 第三方回调让网络策略、Spring Security、网关规则和日志监控都能围绕路径职责进行配置。
公网 Callback 的安全模型不能只依赖网络封禁
第三方 Callback 必须公网可达,因此不能简单依赖“只有 VPN 才能访问”。
真正重要的是请求本身是否可信。
严格验签
支付渠道回调必须按照渠道协议进行签名校验。
只有验签成功的请求才能进入业务处理。
因此:
text
公网可达
≠
任何请求都可信而是:
text
公网可达
+
应用层验证请求真实性幂等
支付渠道通常可能因为:
text
超时
响应丢失
网络抖动重复发送 Callback。
因此同一笔支付回调必须可以安全重复处理。
例如:
text
TO_PAY → PAID只允许合法状态迁移。
重复到达时不能重复扣减、重复入账或重复触发其他不可逆业务。
限流
Callback 接口可以针对异常请求频率增加限流,降低恶意请求或突发流量带来的影响。
但限流策略必须避免误伤合法渠道重试。
IP 白名单只能作为附加措施
如果某个渠道明确提供并维护稳定的服务器出口 IP 范围,可以进一步作为网络层的辅助限制。
但 IP 白名单不应该成为判断 Callback 真伪的唯一手段。
原因是:
text
第三方 IP 可能变化
CDN / 云环境可能调整
不同渠道网络模型不同一旦白名单没有及时同步,仍然可能把合法回调挡在外面。
因此更合理的优先级是:
text
验签
→ 核心身份验证
IP 白名单
→ 条件允许时的附加防护回调不能成为唯一的支付状态确认路径
这次事故还暴露出另一个问题:
如果支付结果完全依赖 Callback,一旦 Callback 因网络或第三方异常丢失,业务状态就可能长期停留在 TO_PAY。
更稳妥的支付状态链路应该有多级兜底。
第一层:Callback
实时主链路:
text
支付渠道
↓
Callback
↓
业务系统更新订单这是最快的状态确认方式。
第二层:主动查询或补偿任务
对于长时间仍处于:
text
TO_PAY
PAYING状态的订单,可以周期性调用支付渠道查询接口主动确认支付状态。
这样即使 Callback 偶发丢失,也可以在较短时间内修复业务状态。
第三层:T+1 对账
最终再通过渠道账单进行资金核对:
text
系统订单
vs
渠道账单发现漏单、状态不一致时进行补偿处理。
T+1 对账的定位应该是:
最终资金一致性的兜底。
它不能替代实时 Callback,因为让用户等待到第二天才能看到正确支付状态,业务体验通常无法接受。
变更后的验证也应该覆盖业务链路
这次事故如果只验证:
text
管理员能不能通过 VPN 打开后台那么结果一定是“变更成功”。
但真正完整的验证应该覆盖不同入口。
例如:
text
Internal
✓ VPN 内可以访问
✓ 公网不能直接访问
Public Callback
✓ 公网仍然可达
✓ 未签名请求被拒绝
✓ 合法签名请求正常进入业务对于支付系统,还应该补一条真实或模拟链路:
text
创建测试订单
↓
支付
↓
收到 Callback
↓
订单状态变更
↓
通知正常发送这也是这次事故以后最值得补上的变更验证方式。
最终回看:这不是 VPN 的问题,而是边界划分问题
如果只看表面现象,这次事故可以被描述成:
开启 VPN 后,支付回调挂了。
但 VPN 并不是根因。
真正的问题是:
把整个应用简单定义为“后台系统”,然后用同一套网络策略处理了所有入口。
而实际上:
text
后台管理入口
第三方支付回调虽然部署在同一个服务里,却属于完全不同的信任边界。
所以这次事故最终留下的核心经验是:
不要按照“系统名称”决定网络边界,要按照真实调用关系和接口职责决定网络边界。
内部管理接口可以严格进入 VPN。
公网 Callback 也可以保持开放。
关键不是追求:
text
全部开放或者:
text
全部关闭而是明确:
text
谁需要访问
访问哪个入口
通过什么方式证明身份
失败以后如何补偿
异常以后如何及时发现安全策略只有建立在真实调用关系之上,才能既收紧攻击面,又不切断正常业务链路。