fix(velero-drill): switch webhook from bearer to github_hmac (DEV-590)

The 2026-10-01 monthly drill passed but the post-run webhook got
HTTP 401 from the home Paperclip. Root cause: the home server's
global actorMiddleware (/app/server/src/middleware/auth.ts) rejects
every Authorization: Bearer <...> that is not a board API key or an
agent JWT before per-route handlers run, so the trigger's own
`bearer` signingMode handler is unreachable with a 48-hex random
secret. Only publicMcpGatewayProtocolPath is exempted.

Fix:
- Trigger 6983798e-018c-459d-95ea-4b5959441e2d switched to
  signingMode: github_hmac via PATCH (secret unchanged).
- post_webhook now computes HMAC-SHA256(body, secret) with openssl
  (already in alpine/k8s) and sends X-Hub-Signature-256 instead of
  Authorization: Bearer. HMAC headers bypass the Bearer branch of
  the global middleware entirely.

Verified end-to-end: synthetic HMAC-signed body posted to
.../fire returned 202 issue_created and spawned DEV-591 under
parent DEV-578. SealedSecret is unchanged (same secret value
reused as the HMAC key).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
CTO 2026-10-01 04:45:44 +00:00
parent 5188784e86
commit 0164c8cb4e
2 changed files with 16 additions and 1 deletions

View file

@ -58,9 +58,16 @@ data:
printf '{"source":"velero-restore-drill","status":"%s","timestamp":"%s","backup":"%s","restore":"%s","restore_phase":"%s","restore_duration_s":"%s","target_namespace":"%s","source_namespace":"%s","diff_summary":"%s"}\n' \ printf '{"source":"velero-restore-drill","status":"%s","timestamp":"%s","backup":"%s","restore":"%s","restore_phase":"%s","restore_duration_s":"%s","target_namespace":"%s","source_namespace":"%s","diff_summary":"%s"}\n' \
"${status}" "${TS}" "${backup}" "${restore}" "${restore_phase}" \ "${status}" "${TS}" "${backup}" "${restore}" "${restore_phase}" \
"${restore_duration_s}" "${target_ns}" "${source_ns}" "${diff_summary}" > "${body_file}" "${restore_duration_s}" "${target_ns}" "${source_ns}" "${diff_summary}" > "${body_file}"
# DEV-590: trigger is in github_hmac signingMode. Compute HMAC-SHA256 over
# the raw body and send X-Hub-Signature-256; sending Authorization: Bearer
# here is a trap because the home Paperclip's global actor middleware
# rejects any Bearer header that is not a board API key or agent JWT
# before the per-trigger handler runs.
local sig
sig=$(openssl dgst -sha256 -hmac "${PAPERCLIP_WEBHOOK_SECRET}" -r "${body_file}" | awk '{print $1}')
local hc local hc
hc=$(curl -sS -o /tmp/webhook.out -w '%{http_code}' -m 30 \ hc=$(curl -sS -o /tmp/webhook.out -w '%{http_code}' -m 30 \
-X POST -H "Authorization: Bearer ${PAPERCLIP_WEBHOOK_SECRET}" \ -X POST -H "X-Hub-Signature-256: sha256=${sig}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
--data-binary "@${body_file}" \ --data-binary "@${body_file}" \
"${PAPERCLIP_WEBHOOK_URL}" 2>&1) || hc="000" "${PAPERCLIP_WEBHOOK_URL}" 2>&1) || hc="000"

View file

@ -27,6 +27,14 @@ Velero cluster-reconstruction backup layer.
the `velero-restore-drill-webhook` Secret is present. The CronJob uses a the `velero-restore-drill-webhook` Secret is present. The CronJob uses a
`hostAliases` entry to route `paperclip.home.imicros.de` at the home `hostAliases` entry to route `paperclip.home.imicros.de` at the home
public IP so the webhook reaches the routine from Hetzner (DEV-579). public IP so the webhook reaches the routine from Hetzner (DEV-579).
Authentication is `github_hmac` — the script computes an HMAC-SHA256
of the raw body with the webhook secret as key and sends it as
`X-Hub-Signature-256: sha256=<hex>`. The matching trigger `signingMode`
is `github_hmac` on routine `c9d941fa-fb97-4e6a-a305-8e46f296e0cd`.
Previously it was `bearer`, but the home Paperclip's global actor
middleware rejects any `Authorization: Bearer …` whose value is not a
board API key or an agent JWT before the per-trigger handler runs,
making the bearer path unreachable (DEV-590).
## Failure signalling ## Failure signalling