Skip to content

GitHub Apps real-time webhook — operator runbook

Plan 112 — sub-minute AI-dep detection via GitHub Apps webhooks delivered to /webhooks/github/{tenant_id}. This runbook walks the operator through:

  1. Provisioning the platform-side webhook signing secret.
  2. Configuring the GitHub App's webhook URL + secret.
  3. Verifying end-to-end with a redelivery.
  4. Troubleshooting common failure modes.

Pre-requisites

  • The GitHub App is already registered (Plan 16a). The App's installation token mints + installation_id round-trips already work — verify by checking that /admin/setup/checklist reports any existing GitHub connection as active.
  • You are signed in as a Secruna platform admin (not just an org admin — the setup endpoints require the is_platform_admin flag).

Step 1 — Provision the platform-side webhook signing secret

Open /admin/setup in the dashboard. The 9th row is GitHub Apps webhook secret. Status starts as not_set because the cp-api default for GITHUB_WEBHOOK_SECRET is an empty string.

Click Generate & provision. The endpoint POST /admin/setup/github-webhook-secret/provision-secret will:

  1. Mint a 256-bit secret via secrets.token_hex(32).
  2. Write it to Key Vault under the canonical name github-webhook-secret.
  3. Attach the KV reference to the cp-api Container App secret bag (idempotent — skips when the bag already carries it).
  4. Restart the latest cp-api revision so the new env var lands.
  5. Write a platform.github_webhook_secret.provisioned audit row (with the KV version id but never the raw secret value).

The response carries the KV version id. Wait the ~30s the response suggests, then re-fetch /admin/setup/checklist. The row should now read provisioned.

Recovery / split-brain: if a prior attempt died after the KV write but before the bag wiring landed (network blip, RBAC race), a repeat click takes the recovered path automatically. No manual cleanup needed.

Re-provisioning: clicking Generate & provision when the secret is already fully wired returns already_provisioned and does nothing. To rotate, use rotate-secret instead (next section).

Step 2 — Read the secret value from Key Vault

The provision endpoint never returns the raw value (defence in depth). To paste it into the GitHub App settings page, you'll need to read it directly from Key Vault:

# Replace <kv-name> with the Key Vault name (default: rekognisedevswedkv).
az keyvault secret show \
  --vault-name <kv-name> \
  --name github-webhook-secret \
  --query value -o tsv

Copy the output to your clipboard. Treat it like a password — do not paste into chat, do not commit to repos.

Step 3 — Configure the GitHub App webhook

In your GitHub App's Settings → Advanced → Webhook:

  1. Active: check it on.
  2. Webhook URL: the canonical receiver URL is https://api.secruna.com/webhooks/github/{tenant_id} (one URL per tenant — GitHub Apps deliver to one URL per installation, so multi-tenant routing happens via the path). Look up the tenant_id UUID in /connections/[id] for the connection in question (it's surfaced on the Webhook health panel as a copy-to-clipboard).
  3. Secret: paste the value from Step 2.
  4. SSL verification: Enable. The receiver is HTTPS-only.
  5. Content type: application/json.

Then Permissions & events → Subscribe to events — check exactly these boxes:

  • Push — push commits to a repo.
  • Pull request — PR opened / edited / closed / etc.
  • Issues — issue opened / edited / closed.
  • Installation repositories — repo added or removed from the installation.
  • Installation — App installed or deleted at the org level.

Save the settings page. GitHub fires a one-off ping delivery to verify the URL — check Advanced → Recent Deliveries. The ping should show a green status with response body {"status":"ignored","delivery_id":"...","scope":"ping"}.

Step 4 — Verify with a redelivery

Open any past delivery on Advanced → Recent Deliveries and click Redeliver. Then in the Secruna dashboard:

  1. Open /connections/[id] for the GitHub connection.
  2. Wait ~5s.
  3. The Webhook health card should now show:
  4. Last received: a fresh relative timestamp.
  5. Deliveries last 24h: incremented by 1.

If the redelivery is a push to the default branch, you'll also see a new entry in the runs timeline (the partial discovery job enqueued by the receiver picks up within ~30s when WORKER_INVOKE_ENABLED=true, or on the next 2-min cron tick when it's off).

Rotation

To rotate the secret without downtime:

  1. Call POST /admin/setup/github-webhook-secret/rotate-secret (via the rotate affordance on /admin/setup — currently the one-click row only renders "Generate & provision"; rotate is a future UI affordance, today it's an API call). The endpoint mints a fresh secret, writes a new KV version, restarts cp-api.
  2. Within the ~30s restart window, read the new secret value from KV (Step 2 above) and paste it into the GitHub App's Webhook secret field.
  3. Once both sides are aligned, every new delivery from GitHub will be signed with the new key.

Past deliveries signed with the old key are not retried — GitHub discards the signing context after the delivery completes. If you want zero-downtime rotation with overlap, the cleanest path is to disable the webhook on the App for the rotation window (<5 min), which causes GitHub to skip deliveries; events that fired during the window are picked up by the next cron-driven discovery tick (2 min at most).

Troubleshooting

503 github_webhook_secret_not_configured

The receiver returns 503 cleanly when settings.github_webhook_secret is empty. This is not a 401 because we don't want to confuse "operator hasn't provisioned the secret yet" with "wrong signature".

  • Re-run Step 1 above.
  • Verify the cp-api Container App secret bag carries github-webhook-secret (az containerapp show -n rekognise-dev-cp-api, look at properties.configuration.secrets).
  • Check that the latest revision was restarted after the bag update. The provision response body's revision field is the one that should be running.

401 signature_mismatch

  • Re-check that the secret pasted into GitHub matches the value in Key Vault. The most common mistake is whitespace at either end (the GitHub UI silently trims, KV doesn't).
  • Inspect /admin/audit filtered by action=github_webhook.rejected — every signature mismatch is audited with the payload_digest (safe to log; SHA-256 of bytes we're about to discard) and the delivery_id GitHub assigned. Cross-reference the delivery id with Advanced → Recent Deliveries to confirm.

202 ignored

The receiver returns 202 + ignored for:

  • Events not in the dispatchable set (e.g. repository.starred, member). No state change needed.
  • Push to a non-default branch. By design — a feature-branch push doesn't change what's shipping to production, which is what the compliance officer cares about.
  • PR / Issue closed actions. Verdicts don't expire on PR close, so re-scanning would be wasted work.
  • Installation matched a tenant id but no connection exists for the installation_id pair. Usually means the App was installed before the customer was onboarded; install the App in a different org or have the customer re-run the OAuth flow.

200 duplicate

GitHub re-sent a delivery the receiver already processed within the last 10 minutes. The replay-protection cache drops the duplicate to avoid double-enqueueing a discovery run. The response is 200 (not 202) so the GitHub UI shows the redelivery as a clean re-send.

Webhook deliveries fail with a TLS error in GitHub's UI

The receiver is HTTPS-only. If you accidentally pasted an http:// URL in the GitHub App settings, GitHub blocks the delivery before it reaches us. Edit the URL and click Redeliver on a recent attempt.

Reference

  • Spec: docs/roadmap/2026-05-12-plan-112-github-realtime-webhook.md.
  • Source: apps/cp-api/src/cp_api/routers/github_webhooks.py, apps/cp-api/src/cp_api/services/github_webhooks.py, packages/rekognise-core/src/rekognise/shared/github_signature.py.
  • Worker: apps/discovery-worker/src/discovery_worker/main.py — the run_discovery_partial arq task is the consumer side.
  • Audit actions emitted: github_webhook.received, github_webhook.ignored, github_webhook.rejected, platform.github_webhook_secret.provisioned, platform.github_webhook_secret.rotated, platform.github_webhook_secret.recovered.