所有文章
将 Git 仓库的 push 和 merge request 事件通过签名的 HTTPS 隧道传入本地开发服务。
GitLabDevOpswebhookslocalhost

在本地主机上安全地测试 GitLab Webhooks

要在本地主机上测试 GitLab webhook,请通过 HTTPS 隧道暴露您的本地处理程序,将该 URL 添加到 设置 → Webhooks 下,生成签名令牌,并在解析有效负载之前验证 GitLab 的标准 Webhooks 签名。 触发一次 push 或 merge request,检查传递内容,并在每次更改后无需部署您的集成即可在本地迭代。

使用 GitLab 签名令牌,而不是新的明文秘密令牌。

GitLab 支持两种容易混淆的机制。较旧的秘密令牌会被复制到 X-Gitlab-Token 请求头中。它证明了共享值的知识,但不能保护主体的完整性。GitLab 现在建议使用 签名令牌 进行新的 webhook。它会生成 HMAC-SHA256 签名,并遵循标准 Webhooks 消息格式。

官方 GitLab Webhook 文档 表示已签名的请求包含 webhook-idwebhook-timestamp,和 webhook-signature签名涵盖消息 ID、时间戳和完整的原始 JSON 内容。这可以保护来源和负载的完整性。

在 Node.js 中实现标准 Webhooks 验证

GitLab 签名令牌只显示一次并使用 whsec_ 前缀。移除该前缀并对剩余部分进行 Base64 解码以获得 HMAC 密钥。每个收到的签名的形式为 v1,<base64 signature>; 头部可能包含几个以空格分隔的签名。

import crypto from 'node:crypto';

function safeEqual(a, b) {
  const left = Buffer.from(a);
  const right = Buffer.from(b);
  return left.length === right.length &&
    crypto.timingSafeEqual(left, right);
}

function verifyGitLabWebhook({ token, id, timestamp, body, signatures }) {
  if (!token?.startsWith('whsec_') || !id || !timestamp || !signatures) {
    return false;
  }

  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > 300) return false;

  const key = Buffer.from(token.slice(6), 'base64');
  const message = `${id}.${timestamp}.${body}`;
  const digest = crypto.createHmac('sha256', key).update(message).digest('base64');
  const expected = `v1,${digest}`;
  return signatures.split(' ').some((value) => safeEqual(value, expected));
}

这里显示的五分钟时间戳窗口是一种应用策略,而不是盲目复制的数值。选择一个能够容纳时钟偏差但阻止有效重放的容差。同步接收机器的时钟。存储每个被接受的 webhook-id 由于仅检查一个新的时间戳无法防止同一消息的两次立即传递,因此需要在唯一约束下进行。

构建 Express webhook 路由

在此路由上捕获原始主体。在验证之前的全局 express.json() 调用会破坏 GitLab 签名的逐字节表示。

import express from 'express';

const app = express();
app.post(
  '/webhooks/gitlab',
  express.raw({ type: 'application/json', limit: '2mb' }),
  async (req, res) => {
    const body = req.body.toString('utf8');
    const valid = verifyGitLabWebhook({
      token: process.env.GITLAB_WEBHOOK_SIGNING_TOKEN,
      id: req.get('webhook-id'),
      timestamp: req.get('webhook-timestamp'),
      signatures: req.get('webhook-signature'),
      body,
    });
    if (!valid) return res.sendStatus(401);

    await inbox.insertOnce(req.get('webhook-id'), JSON.parse(body));
    return res.sendStatus(202);
  },
);
app.use(express.json());
app.listen(3000);

在 webhook 路由之后挂载普通 JSON 解析器,或使用其 verify 回调来保留原始缓冲区。绝不要仅因为端点转发到本地主机就禁用签名检查;隧道 URL 从公共互联网仍然可访问。

创建公共 HTTPS 端点

  1. 在本地启动集成,并使用故意未签名的请求测试其路由。它应返回 401,证明身份验证已启用。
  2. 在另一个终端运行 npx portpreview 3000
  3. 复制 HTTPS 源并附加 /webhooks/gitlab.
  4. 在整个配置和事件测试期间保持隧道开放。

GitLab 的 SSL 验证应保持启用。使用公开信任的 TLS 的隧道可以避免自签名证书错误。如果 GitLab 运行在私有自管网络中,它还必须能够访问公共隧道 URL。

配置项目 Webhook

  1. 打开 GitLab 项目并选择 设置 → 网络钩子.
  2. 选择 添加新的网络钩子 并粘贴完整的隧道传输网址。
  3. 选择 生成签名令牌,立即复制令牌,并将其保存在 GITLAB_WEBHOOK_SIGNING_TOKEN.
  4. 仅选择必需的触发器——例如推送事件、合并请求事件、标签推送事件或流水线事件。
  5. 保持启用 SSL 验证并保存 webhook。
  6. 使用 GitLab 的测试操作或生成真实事件,然后检查本地请求和 GitLab 交付历史。

设置环境变量后重启本地进程。如果您正在迁移现有集成,GitLab 允许同时使用签名令牌和旧版秘密令牌。验证 webhook-signature 时,如果存在,可暂时回退到 X-Gitlab-Token,然后在所有接收端支持签名后删除较弱的秘密。

通过头部和负载分发 GitLab 事件

X-Gitlab-Event 会给出一个可读的事件名称,例如 Push Hook 或 Merge Request Hook。可用于路由,但也要验证负载的 object_kind 。这样可以使意外的组合可见。

switch (req.get('x-gitlab-event')) {
  case 'Push Hook':
    await handlePush(payload);
    break;
  case 'Merge Request Hook':
    await handleMergeRequest(payload);
    break;
  case 'Pipeline Hook':
    await handlePipeline(payload);
    break;
  default:
    await recordUnsupportedGitLabEvent(payload.object_kind);
}

推送事件

测试分支创建、普通提交、强制推送和分支删除。零 SHA 可以表示引用转换中缺失的一端。大型推送可能与一次提交的固定装置不同,所以不要假设每个已更改的提交都会出现在无限数组中。请使用项目和引用标识符,而不是解析显示字符串。

合并请求事件

打开、更新、审批、合并和关闭等操作可能共享相同的广义事件类型。请根据已记录的对象属性进行路由,并确保重复更新具有幂等性。切勿仅因为可变标题或用户名匹配而合并代码或批准部署。

流水线和作业事件

这些事件可能频繁发生。在 GitLab 过滤一次,再在处理程序中按项目、分支、状态和环境过滤。对于耗时的工件或部署工作,请先排队处理并确认 Webhook。

为重试和递归触发设计

GitLab 包含 webhook-id,它在重试过程中保持一致,并等同于传统的 Idempotency-Key。将其用作交付幂等性键。 X-Gitlab-Webhook-UUID 识别一次 webhook 执行,而 X-Gitlab-Event-UUID 可以帮助追踪事件;递归 webhook 可能会共享事件 UUID。

如果处理程序通过 API 修改 GitLab,可能会创建另一个 webhook。添加显式循环防护:用你的集成身份标记操作,忽略不改变期望状态的更改,并限制工作流转换次数。 重试和幂等性指南 涵盖事务收件箱模式。

排查 GitLab webhook 测试失败

GitLab 无法连接到 URL

确认隧道进程正在运行,完整路径正确,并且您的本地服务器正在监听转发端口。对于自管理的 GitLab,请检查出站网络策略和 DNS。不要为了隐藏无关的路由故障而清除 SSL 验证。

签名从未匹配

使用签名令牌,而不是旧的秘密令牌。去掉 whsec_,对剩余的令牌进行 Base64 解码,然后签名 {webhook-id}.{webhook-timestamp}.{raw body}。将二进制 HMAC 摘要进行 Base64 编码,并在前面加上 v1,。与每个空格分隔的签名进行比较。

时间戳被拒绝

检查系统时间和时区处理;该头是以秒为单位的 Unix 时间戳。不要在不除以 1000 的情况下将其与 JavaScript 毫秒比较。如果调试捕获的旧请求,时间戳被拒绝是正确的重放保护。

GitLab 禁用或暂停 webhook

检查最近的投递状态和你的路由响应。在持久接受后快速返回 2xx。重复的 401 表示令牌配置错误;重复的 5xx 表示处理程序失败;超时表示同步工作过多。

只有部分事件到达

检查选定的触发器和分支过滤器。组和项目 webhook 的作用范围不同。确认事件确实发生在配置此 webhook 的项目中。

保持本地 GitLab webhook 数据安全

  • 仅将签名令牌存储在忽略的环境文件中,并旋转任何泄露的令牌。
  • 在执行副作用之前验证签名、时间戳、项目 ID 和允许的事件类型。
  • 从捕获中编辑提交信息、私有仓库 URL、用户邮箱和 CI 变量。
  • 仅为集成 API 令牌提供其下游操作所需的权限范围。
  • 测试结束后删除本地负载历史记录。

对于与提供者无关的诊断,请使用 本地 webhooks 调试指南。GitHub 使用不同的签名格式,因此请参考单独的 GitHub webhooks 指南 ,而不是重复使用其验证器。

常见问题

如何在本地主机上测试 GitLab webhook?
使用 HTTPS 隧道公开你的本地路由,在 GitLab 项目 Webhooks 下添加其公共 URL,配置签名令牌和触发器,然后生成测试或实际事件。
新的 GitLab webhooks 应该使用 X-Gitlab-Token 吗?
GitLab 建议新 webhooks 使用签名令牌。X-Gitlab-Token 携带明文密钥,而签名令牌对请求的 HMAC-SHA256 摘要进行认证。
GitLab 的 webhook 签名是如何计算的?
在移除 whsec_ 后解码签名令牌,对字符串 webhook-id.webhook-timestamp.raw-body 使用 HMAC-SHA256,再对摘要进行 Base64 编码,并加上前缀 v1,.
我如何防止重复的 GitLab webhook 操作?
在唯一约束下存储 webhook-id,并以事务方式应用副作用。GitLab 在重试时会保持该 ID 稳定,因此适用于幂等性。