你可以通过 VPN 打开 Jenkins,却仍然看到 GitHub 报告 webhook 失败,因为这是两条不同的连接,归属也不同。浏览器走的是笔记本协商出的隧道;GitHub 的投递服务从公网发起,进不了私有网络。能访问仓库并不会改变这一点,即便 Jenkins 自己能顺畅地从同一个仓库拉取。请选择入站端点、中继或轮询,然后实测事件实际走的那条路径。

浏览器会话只能证明一个方向

VPN 连接回答一个问题:这台笔记本能否到达内部主机。Webhook 投递问的是另一件事:外部服务能否向那台主机发起连接。Jenkins 拉取仓库属于出站流量,完全说明不了入站的连通性。把两个方向画在图上各自独立的行里,并标明每一路由哪一侧发起。

在动手改动之前,先读投递记录。超时、连接重置、请求被接受却没有匹配到任何任务,这三者的责任方完全不同:网络、代理和触发配置。

方案 A:只暴露一个很窄的端点

如果组织已经发布了经过批准的入站入口,就暴露 webhook 路径而不是仪表盘。这通常意味着一条反向代理规则:在钩子 URL 上接受 POST 并转发给 Jenkins,同时管理界面依旧无法从外部访问。

curl -sS -o /dev/null -w "%{http_code}\n" -X POST https://jenkins.example.com/github-webhook/

这项测试要从没有 VPN 访问权限的网络发起,因为隧道内部成功什么都证明不了。端点应当拒绝缺少共享密钥的请求,该密钥由 GitHub 以签名头发送;并且只接受你发布的那一条路径和一种方法。公开的 Jenkins 控制台不是 webhook 的修复方案,而是一起等着下一个扫描器来发现的事故。把规则收得足够紧,让应用其余部分保留原有的访问控制。

方案 B:用中继反转方向

在入站暴露无法获批的地方,网络内部的小型客户端可以 维持一条通往中继的出站连接,并接收转发来的事件。中继于是成为投递路径的一部分,它的可用性和身份验证与构建任务同样重要。

ssh -N -R 9000:localhost:8080 relay.example.net

Jenkins 关于防火墙后 webhook 的说明 描述了这种形态,值得在自建转发器之前先读一遍。在中继上终止 TLS,在事件到达任务之前校验签名,并监控这条出站链路,因为掉线的隧道看起来与从未发送过的事件一模一样。还要确认中继逐字节保留请求体,因为经过解析并重新编码的 JSON 会让签名校验失败。

方案 C:改为轮询仓库

轮询用延迟换简单,完全不需要入站路径。Jenkins 可以按计划轮询,也可以由一个小任务查询 API,在发现不认识的修订时启动构建。

curl -sS -u "$USER:$TOKEN" "https://api.github.com/repos/OWNER/REPO/commits?per_page=1"

每次轮询都消耗 API 调用,并在 push 与构建之间增加延迟,这在繁忙的仓库和共享的速率限制上会显现出来。它还需要把凭据存放在轮询器运行的任何地方,所以要把令牌限制为仓库只读访问,并像其他密钥一样定期轮换。当事件量很低,或入站入口永远批不下来时选它,并把这一点写进任务说明,免得后来有人纳闷构建为什么总是滞后。

接受一次真实投递,而不是打开一个网页

无论选哪种方案,验收测试都必须是源平台发出的事件,而不是从有权限的笔记本上打开的浏览器标签。往临时分支推一个空提交,然后依次读投递响应码、中继日志或轮询任务的输出。端点返回 200 却没有构建,说明触发过滤器不对;超时则说明路径仍然是关的。记录当前用的是三种设计里的哪一种,否则下一位排查的人会重复浏览器测试,撞上同一个死胡同。

同样的道理适用于支付回调和其它外部通知:每一个都需要自己通往私有应用的路,即使每个人手里都已经有 VPN 会话。投递路径属于应用设计的一部分,而不是网络方面的脚注。

相关文章