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:
- Provisioning the platform-side webhook signing secret.
- Configuring the GitHub App's webhook URL + secret.
- Verifying end-to-end with a redelivery.
- Troubleshooting common failure modes.
Pre-requisites¶
- The GitHub App is already registered (Plan 16a). The App's
installation token mints +
installation_idround-trips already work — verify by checking that/admin/setup/checklistreports 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_adminflag).
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:
- Mint a 256-bit secret via
secrets.token_hex(32). - Write it to Key Vault under the canonical name
github-webhook-secret. - Attach the KV reference to the cp-api Container App secret bag (idempotent — skips when the bag already carries it).
- Restart the latest cp-api revision so the new env var lands.
- Write a
platform.github_webhook_secret.provisionedaudit 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_provisionedand does nothing. To rotate, userotate-secretinstead (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:
- Active: check it on.
- 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 thetenant_idUUID in/connections/[id]for the connection in question (it's surfaced on the Webhook health panel as a copy-to-clipboard). - Secret: paste the value from Step 2.
- SSL verification: Enable. The receiver is HTTPS-only.
- 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:
- Open
/connections/[id]for the GitHub connection. - Wait ~5s.
- The Webhook health card should now show:
- Last received: a fresh relative timestamp.
- 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:
- 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. - 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.
- 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 atproperties.configuration.secrets). - Check that the latest revision was restarted after the bag
update. The provision response body's
revisionfield 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/auditfiltered byaction=github_webhook.rejected— every signature mismatch is audited with thepayload_digest(safe to log; SHA-256 of bytes we're about to discard) and thedelivery_idGitHub 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
closedactions. 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— therun_discovery_partialarq 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.