すべての記事
Git リポジトリのプッシュおよびマージリクエストイベントを署名付き HTTPS トンネル経由でローカル開発サービスに送る。
GitLabDevOpswebhookslocalhost

ローカルホストでGitLab Webhookを安全にテストする

ローカルホストでGitLabのWebhookをテストするには、ローカルハンドラーをHTTPSトンネル経由で公開し、そのURLを設定 → Webhooksに追加し、署名トークンを生成し、ペイロードを解析する前にGitLabの標準Webhook署名を確認してください。 プッシュやマージリクエストをトリガーし、デリバリーを確認し、変更ごとに統合をデプロイせずにローカルで繰り返し作業します。

新しいプレーンテキストのシークレットトークンではなく、GitLabの署名トークンを使用してください

GitLabは、混同しやすい2つの仕組みをサポートしています。古いシークレットトークンは、次にコピーされます X-Gitlab-Token リクエストヘッダー。これは共有値の知識を証明しますが、本文の完全性は保護しません。GitLabは現在、次のことを推奨しています 署名トークン 新しいウェブフック用です。HMAC-SHA256署名を生成し、標準のウェブフックメッセージ形式に従います。

その 公式GitLabウェブフックドキュメント 署名付きリクエストには含まれていると言います 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));
}

ここで示されている5分のタイムスタンプのウィンドウは、単にコピーすべき値ではなく、アプリケーションのポリシーです。クロックスキューを許容しつつ、有用なリプレイを防ぐ許容範囲を選んでください。受信マシンの時計を同期させてください。受け入れたものをすべて保存してください 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 コールバックを使用します。エンドポイントがlocalhostに転送するだけだからといって署名チェックを無効にしてはいけません。トンネルURLは依然としてパブリックインターネットから到達可能です。

公開HTTPSエンドポイントを作成します。

  1. 統合をローカルで開始し、意図的に署名されていないリクエストでルートをテストします。401が返るはずで、認証が有効であることを証明します。
  2. 別の端末で npx portpreview 3000 を実行します。
  3. HTTPSのオリジンをコピーし、" /webhooks/gitlab.
  4. 構成およびイベントテストの間、トンネルを開いたままにしてください。

GitLabのSSL検証は有効のままにしてください。公開トラストされたTLSを使用するトンネルは、自己署名証明書のエラーを回避します。GitLabがプライベートのセルフマネージドネットワークで動作する場合、パブリックトンネルURLへのアウトバウンドアクセスも必要です。

プロジェクトのWebhookを構成してください

  1. GitLabプロジェクトを開き、 設定 → Webhookを選択します。
  2. 新しいWebhookを追加 を選択して、トンネル配信URL全体を貼り付けます。
  3. 署名トークンを生成を選択し、すぐにトークンをコピーして、 GITLAB_WEBHOOK_SIGNING_TOKENに保存します。
  4. 必要なトリガーのみを選択します—例えば、Pushイベント、Merge requestイベント、Tag pushイベント、またはPipelineイベントなどです。
  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は、リファレンス遷移の片方が欠落していることを表す場合があります。大規模なプッシュは、1コミットのフィクスチャとは異なる可能性があるため、変更されたすべてのコミットが無制限の配列に現れるとは限らないと考えないでください。表示文字列を解析するのではなく、プロジェクトとリファレンスの識別子を使用してください。

マージリクエストイベント

オープン、更新、承認、マージ、クローズなどのアクションは、同じ大まかなイベントタイプを共有する場合があります。文書化されたオブジェクト属性によってルート処理し、繰り返しの更新を冪等性のあるものにしてください。可変のタイトルやユーザー名が一致するからといって、コードをマージしたりデプロイを承認したりしてはいけません。

パイプラインおよびジョブイベント

これらは頻繁に発生する場合があります。GitLab側でフィルタリングし、ハンドラ側でもプロジェクト、ブランチ、ステータス、環境によって再度フィルタリングしてください。遅いアーティファクトやデプロイ作業はキューに入れ、Webhookを最初に確認してください。

リトライと再帰トリガーの設計

GitLabには webhook-idが含まれており、これはリトライ間で一貫しており、従来の Idempotency-Keyと同等です。配信の冪等性キーとして使用してください。 X-Gitlab-Webhook-UUID はウェブフックの実行を識別し、 X-Gitlab-Event-UUID はイベントを追跡するのに役立ちます。再帰ウェブフックはイベントUUIDを共有する場合があります。

ハンドラーがAPIを通じてGitLabを変更する場合、別のウェブフックを作成する可能性があります。明示的なループ防止を追加してください:アクションに統合の識別情報をタグ付けし、目的の状態を変更しない変更は無視し、ワークフローの遷移に上限を設定します。 リトライと冪等性ガイド では、トランザクション型インボックスパターンについて説明しています。

GitLabウェブフックテストの失敗をトラブルシューティングする

GitLabはURLに接続できません

トンネルプロセスが稼働していること、フルパスが正しいこと、ローカルサーバーが転送されたポートでリスニングしていることを確認してください。セルフマネージドGitLabの場合は、アウトバウンドネットワークポリシーとDNSを確認してください。無関係なルーティング失敗を隠すためにSSL検証をクリアしないでください。

署名が一致しません

古いシークレットトークンではなく、署名トークンを使用してください。 whsec_を削除し、残りのトークンをBase64デコードして {webhook-id}.{webhook-timestamp}.{raw body}を署名します。バイナリHMACダイジェストをBase64でエンコードし、 v1,をプレフィックスとして付けます。スペースで区切られたすべての署名と比較します。

タイムスタンプが拒否されました

システム時間とタイムゾーンの処理を確認してください。ヘッダーは秒単位のUnixタイムスタンプです。JavaScriptのミリ秒と比較する場合は1000で割る必要があります。古いリクエストをデバッグする場合、タイムスタンプの拒否は正しいリプレイ保護です。

GitLabはWebhookを無効化するか後退させます

最近の配信状況とルートの応答を確認してください。永続的に受け入れた後はすぐに2xxを返してください。401が繰り返される場合はトークンの構成が間違っています。5xxが繰り返される場合はハンドラーの失敗です。タイムアウトは同期作業が多すぎることを示します。

一部のイベントのみが届く

選択したトリガーとブランチフィルターを確認してください。グループおよびプロジェクトのウェブフックはスコープが異なります。このWebhookが設定されている正確なプロジェクトでイベントが発生したことを確認してください。

ローカルGitLabウェブフックデータを安全に保管

  • 署名トークンは無視される環境ファイルにのみ保存し、漏洩したトークンは回転させてください。
  • 副作用を発生させる前に署名、タイムスタンプ、プロジェクトID、許可されたイベントタイプを検証してください。
  • キャプチャからコミットメッセージ、プライベートリポジトリURL、ユーザーのメール、CI変数をマスキングしてください。
  • 統合APIトークンには、その下流のアクションに必要なスコープのみを付与してください。
  • テストが終了したら、ローカルのペイロード履歴を削除してください。

プロバイダーに依存しない診断を行う場合は、 ローカルWebhookデバッグガイドを使用してください。GitHubは異なる署名形式を使用するため、検証ツールを再利用するのではなく、別の GitHub Webhookガイド を参照してください。

よくある質問

ローカルホストでGitLab webhookをどうやってテストしますか?
HTTPSトンネルでローカルルートを公開し、その公開URLをGitLabプロジェクトのWebhooksに追加し、署名トークンとトリガーを設定してから、テストイベントまたは実際のイベントを生成します。
新しいGitLab webhooksではX-Gitlab-Tokenを使うべきですか?
GitLabは新しいwebhookには署名トークンを推奨しています。X-Gitlab-Tokenはプレーンテキストの秘密鍵を送信するのに対し、署名トークンはリクエストのHMAC-SHA256ダイジェストを認証します。
GitLabのwebhook署名はどのように計算されますか?
whsec_ を取り除いた後に署名トークンをデコードし、文字列 webhook-id.webhook-timestamp.raw-body に対して HMAC-SHA256 を実行し、ダイジェストを Base64 エンコードし、v1, を前置します。
GitLab の webhook アクションの重複を防ぐにはどうすればよいですか?
webhook-id を一意制約の下で保存し、副作用をトランザクション的に適用します。GitLab はリトライ間でもその ID を安定して保持するため、冪等性に適しています。