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> |
||
|---|---|---|
| .. | ||
| 00-serviceaccount-rbac.yaml | ||
| 10-drill-script-configmap.yaml | ||
| 20-restore-drill-cronjob.yaml | ||
| 30-webhook-sealed.yaml | ||
| README.md | ||
Velero restore drill
Monthly Kubernetes CronJob that exercises the Velero backup chain by restoring
the openwebui namespace into a transient restore-test-<ts> namespace,
diffing resource counts against the live namespace, and cleaning up.
Wired by DEV-575 under the DEV-569 Velero cluster-reconstruction backup layer.
What it does
- Picks the latest
Completedbackup for scheduleopenwebui-manifests(envDRILL_BACKUP_OVERRIDEshort-circuits this — used by the failure drill). - Creates a
RestoreCR with namespace mappingopenwebui: restore-test-<ts>. - Waits for the
Restoreto reach a terminal phase (Completed,PartiallyFailed,Failed,FailedValidation) or times out at 30 min. - Counts core kinds (deployments, statefulsets, services, configmaps, PVCs, …) in source vs target and requires strict equality on the manifest-carrying kinds to pass.
- Deletes the
restore-test-<ts>namespace and theRestoreCR. - Writes the final report to ConfigMap
velero/velero-restore-drill-last-resultand logs it to stdout. - Optionally POSTs a JSON payload to the Paperclip routine webhook if
the
velero-restore-drill-webhookSecret is present. The CronJob uses ahostAliasesentry to routepaperclip.home.imicros.deat the home public IP so the webhook reaches the routine from Hetzner (DEV-579). Authentication isgithub_hmac— the script computes an HMAC-SHA256 of the raw body with the webhook secret as key and sends it asX-Hub-Signature-256: sha256=<hex>. The matching triggersigningModeisgithub_hmacon routinec9d941fa-fb97-4e6a-a305-8e46f296e0cd. Previously it wasbearer, but the home Paperclip's global actor middleware rejects anyAuthorization: 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
The Job exits non-zero on any failure. The alerting stack (DEV-574) will fire
KubeJobFailed on the velero-restore-drill-* job name and route the alert
through existing Alertmanager receivers. Ops can inspect the last result via:
kubectl -n velero get cm velero-restore-drill-last-result -o jsonpath='{.data.result}'
kubectl -n velero get cm velero-restore-drill-last-result -o jsonpath='{.data.log}'
Manual runs
Kick a drill on demand:
kubectl -n velero create job --from=cronjob/velero-restore-drill drill-adhoc-$(date -u +%Y%m%d-%H%M%S)
Force a failure to test the alert wiring (uses a bogus backup name):
kubectl -n velero create job drill-adhoc-fail-$(date -u +%s) \
--image=alpine/k8s:1.30.5 \
--dry-run=client -o yaml \
| kubectl -n velero apply -f - # only if you need a scratch, prefer:
kubectl -n velero create job --from=cronjob/velero-restore-drill drill-fail-$(date -u +%Y%m%d-%H%M%S) --dry-run=client -o yaml \
| sed 's/value: "openwebui-manifests"/value: "openwebui-manifests"\n - name: DRILL_BACKUP_OVERRIDE\n value: "no-such-backup"/' \
| kubectl apply -f -
For a simpler failure drill, apply the CronJob-derived Job then patch env before scheduling — see the parent DEV-575 comment for the exact runbook.
RBAC scope
velero.io/backups,restores: get/list/create/delete in-cluster (namespace scope is enforced by resource type — Backups/Restores live invelero).namespaces: cluster-wide get/list/create/delete. Cannot be name-scoped in RBAC; the drill script enforces therestore-test-prefix before ever deleting a namespace.configmaps: get/list/create/update/patch (only used to write the result ConfigMap invelero— pod-level namespace scoping via ServiceAccount).- Cluster-wide
viewClusterRole for the counting diff.