Webhook Secret Token with HMAC Digest
### Description Currently, when creating a webhook you have the option to set a value that will be sent in the `X-Gitlab-Token` HTTP Header. This is meant for validation that the request did actually come from GitLab. But, this security feature is very poorly implemented for a few reasons: 1. This token is sent in plain text 2. No way to verify that the payload is valid and came from GitLab 3. Static tokens are vulnerable to **replay attacks** — an attacker who intercepts a legitimate request can resend it indefinitely, since the token doesn't change per request (see also https://gitlab.com/gitlab-org/gitlab/-/work_items/587536) This should function more like [GitHub's version](https://developer.github.com/webhooks/#payloads), where that token (the hook secret) is used as the key for an HMAC SHA256 hex digest, which is then sent as an HTTP Header. You can then compute your own hash of the payload using the secret that it *should be*, and compare it to the one sent in the header. However, with GitLab (as said before), you can not do this, the best that you can do is check the plain text header... Not great. Some examples of other services that sign their requests: * Stripe https://stripe.com/docs/webhooks/signatures * Slack https://api.slack.com/authentication/verifying-requests-from-slack * GitHub https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks ### Proposal This proposal also addresses the replay attack protection described in https://gitlab.com/gitlab-org/gitlab/-/work_items/587536. The HMAC signing token approach solves both payload integrity verification and replay resistance in a single feature; no separate JWT or nonce mechanism is needed. - Add a **signing token** field when creating a webhook, explicitly labeled as the key for the HMAC hash of the payload. A new field is needed to ensure backwards compatibility with existing webhooks. This field should be absolutely secret, like a password (even when editing an endpoint). - When sending a payload to an endpoint, add two additional HTTP headers: - `X-Gitlab-Signature` — an HMAC-SHA256 hex digest of `{timestamp}.{webhook-uuid}.{payload}`, prefixed with `sha256=` - `X-Gitlab-Timestamp` — the Unix timestamp used in the signature, so receivers can verify the request is recent and reject stale/replayed requests (e.g. outside a 5-minute window) - The UUID and timestamp included in the HMAC message make replay attacks impractical: a replayed request carries a stale timestamp that receivers can detect and reject. If no signing token is configured, the `X-Gitlab-Signature` and `X-Gitlab-Timestamp` headers are not sent. ### Out of scope System Hooks are not covered by this issue. Signing token support for System Hooks will be addressed separately in https://gitlab.com/gitlab-org/gitlab/-/work_items/503457. ### Technical Proposal `@van.m.anderson` worked on a merge request that was very close to implementing the feature https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163102+. Thank you, Van :heart:. We can pick up the work from where they left off. The remaining work in that MR is outlined here https://gitlab.com/gitlab-org/gitlab/-/merge_requests/163102#note_2085734123, which is to split the ~backend and ~frontend implementations and then add test coverage. ### Links / references [GitLab's Documentation on Webhooks](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md) [GitLab's "Secret Token" Documentation](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/project/integrations/webhooks.md#secret-token) [GitHub's Documentation on Webhooks](https://developer.github.com/webhooks/) [Replay Attack Protection proposal (#587536)](https://gitlab.com/gitlab-org/gitlab/-/work_items/587536) ### Designs | State | Design | | ------ | ------ | | New webhook | ![image](/uploads/6601af2a5f8ebdd944a156aba31a7a67/image.png) | | Edit legacy webhook (created before signing tokens were introduced) | ![image](/uploads/662a6476f42e5c70b78748ec231bdc9b/image.png) | | Edit webhook (with signing token) | ![image](/uploads/671a0ee6450646aef324c480f709c616/image.png) | | Edit webhook > Regenerated signing token | ![image](/uploads/963c31e87c6ce132ccb260b3b6607570/image.png) | ### Open Questions: Secret Token Deprecation 1. Should the legacy secret token (X-Gitlab-Token) be deprecated once the signing token is released? Note: The deprecation timeline will be difficult to determine from instrumentation alone. While we can measure how many webhooks have a signing token configured on the GitLab side, we have no visibility into how many receivers (external services) are actively validating it, rather than still relying on X-Gitlab-Token. This means adoption data will likely undercount real usage, and any removal decision will require tolerance for some uncertainty. ### Documentation blurb #### Overview This feature proposal would allow for more secure endpoints, since users could verify that the payload is authentic and originated from a GitLab server. It also protects against replay attacks by incorporating a timestamp and unique request UUID into the HMAC signature. Receivers can validate the timestamp is recent (e.g. within 5 minutes) to reject replayed requests, even if an attacker captured a legitimate one. This would solve multiple security flaws in GitLab's current webhook implementation that allow for easy spoofing and replay of requests that could potentially be damaging. #### Use cases 1. Any webhook consumer that needs to verify requests genuinely originated from GitLab. 2. Security-conscious teams in regulated industries (finance, healthcare) with compliance requirements around request integrity and audit trails. ### Feature checklist Make sure these are completed before closing the issue, with a link to the relevant commit. - [ ] [Feature assurance](https://about.gitlab.com/handbook/product/#feature-assurance) - [ ] Documentation - [ ] Added to [features.yml](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/features.yml)
issue