RCA — vault.raxx.app origin TLS certificate expiry (CI 526s, deploy pipeline blocked)
Incident ID: 2026-07-23-vault-origin-cert-expiry
Date: 2026-07-23
Severity: SEV-2
Duration: ~5h47m total (21:23:17 UTC expiry → ~03:10:25 UTC 2026-07-24 fix verified live)
Blast radius: All CI pipelines and agent/CLI sessions that read secrets from vault.raxx.app (Infisical); PR #4331 and PR #4332 blocked at the vault-read step; no customer-facing surface affected (vault is an internal-only, CF Access-gated system).
Author: sre-agent
Summary
Caddy's self-managed Let's Encrypt certificate for vault.raxx.app (CN=vault.raxx.app) expired at 21:23:17 UTC. Automatic ACME renewal is architecturally impossible on this host: tls-alpn-01 is blocked because Cloudflare (orange-cloud proxy) terminates TLS at the edge before Caddy ever sees the ALPN challenge, and http-01 is blocked because Cloudflare Access intercepts unauthenticated HTTP requests to vault.raxx.app with a login redirect before they reach Caddy's challenge handler. CI pipelines began failing with CF error 526 ("Invalid SSL Certificate") at the edge. Every Cloudflare credential capable of issuing a replacement Origin CA certificate is itself stored in the vault that was down — a bootstrap circularity. The recovery path was to SSH directly to the Lightsail origin and reach the Infisical backend on localhost:8080, bypassing the broken Cloudflare edge entirely, to read the one Cloudflare credential (CLOUDFLARE_ACCESS_MGMT_TOKEN) capable of minting a short-lived SSL-and-Certificates-write token. That token was used to issue a 3-year Cloudflare Origin CA certificate, which was installed as a static cert in Caddy (ACME disabled for the site), permanently removing the ACME-behind-CF-proxy failure mode for this host.
Timeline (all times UTC)
- 2026-07-23 21:23:17 — Caddy's Let's Encrypt certificate for
vault.raxx.appexpires. - ~22:30 — First signal: CI pipelines (WP run for PR #4331
ci-pr/lint-cf-tokens, and PR #4332) begin failing at the vault-read step with CF error 526 from the edge. - (prior diagnosis, this incident) — Root cause confirmed: ACME renewal architecturally blocked by CF proxy (tls-alpn-01) + CF Access (http-01). Private key pre-staged at
/etc/ssl/private/cf-origin-vault.key; Lightsail snapshotraxx-vault-pre-cert-fix-20260723-2323taken; Caddyfile backed up to/etc/caddy/Caddyfile.bak.20260723-232318.CLOUDFLARE_RAXX_AUTOMATION_API_TOKENconfirmed unable to mint tokens (403/9109) or issue certs directly (1016) — every capable CF credential lives inside the down vault. - ~02:50 (2026-07-24) — sre-agent (this session, FIX mode) confirms the break-glass path: SSH to
raxx-vaultorigin, Infisical backend reachable at127.0.0.1:8080(matches Caddy'sreverse_proxy 127.0.0.1:8080for the site), local universal-auth login succeeds usingINFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRETalready in the operator session.CLOUDFLARE_ACCESS_MGMT_TOKENfetched by name (no path enumeration) from/MooseQuest/cloudflare, verifiedactiveviaGET /client/v4/user/tokens/verify. - ~02:55 — sre-agent identifies that the June 2026
tickets.raxx.appprecedent (docs/ops/runbooks/freescout-cert-renewal.md) minted its short-lived SSL cert token viaPOST /client/v4/user/tokens(user-scoped, zone-restricted), not an account-scoped endpoint — correcting an assumption in the original diagnosis. sre-agent is blocked at this point by this session's own tooling controls before executing the mutating mint/issue/install steps (see "What didn't go well"). - ~03:00–03:10 — Fix executed directly under operator approval (different session): a short-lived, account-scoped SSL-and-Certificates-write token was minted from
CLOUDFLARE_ACCESS_MGMT_TOKEN; a 3-year (1095-day) Cloudflare Origin CA certificate was issued forvault.raxx.app(notBefore2026-07-24T03:02:00Z,notAfter2029-07-23T03:02:00Z) against a CSR regenerated from the pre-staged key. Two Caddy start failures were hit and resolved in sequence: firstpermission deniedreading the key from/etc/ssl/private/(Caddy runs as an unprivilegedcaddyuser that cannot traverse that directory), then a second failure after a naive fix attempt, resolved by relocating both cert and key to a new/etc/caddy/tls/directory ownedroot:caddy(750 dir / 640 files) that thecaddyuser can read but not traverse-list. The Caddyfile was updated with a statictls /etc/caddy/tls/cf-origin-vault.pem /etc/caddy/tls/cf-origin-vault.keydirective inside thevault.raxx.appsite block, replacing implicit ACME. - 03:10:25 —
systemctl reload caddysucceeds; Caddy active and serving the new certificate. - 03:10:25–03:14 — Short-lived minted CF token revoked; WP pipelines 2782 (PR #4331) and 2790 (PR #4332) restarted.
- ~03:11–03:14 — sre-agent independently re-verifies the fixed state (see Verification below).
Impact
- Users affected: none (internal-only system; no customer-facing surface touches
vault.raxx.appdirectly) - User-visible symptoms: none externally; internally, all agent/CI vault reads failed with CF 526 for the duration
- Data integrity: ok — Infisical container and database were never touched; only the origin's TLS termination was affected
- Revenue / billing: ok — no billing-path dependency on vault.raxx.app during the window
- CI impact: PR #4331 (
ci-pr,lint-cf-tokens) and PR #4332 blocked at the vault-secrets-load step for the duration of the outage
What went well
- A private key and CSR-ready state, a Lightsail snapshot, and a Caddyfile backup were pre-staged before any remediation attempt — nothing was overwritten blind.
- The June 2026
tickets.raxx.app526 incident (docs/incidents/2026-06-19-freescout-526-ssl-strict.md) had already documented the Cloudflare Origin CA issuance pattern and the correct permission-group IDs (c03055bc037c4ea9afb9a9f104b7b721SSL/Certs Write,c8fed203ed3043cba015a93ad1616f1fZone Read) — no permission-group discovery calls were needed. - The SSH-to-origin-then-hit-localhost-Infisical break-glass path worked exactly as designed: it reached a fully healthy Infisical instance without touching the broken Cloudflare edge at all.
- The zone SSL mode (
strict) was never weakened at any point during remediation — no security posture downgrade was used as a shortcut. - Fleet cert-expiry monitoring (PR #4332) was already in flight and is now live, closing the detection gap this incident exposed.
What didn't go well
- No cert-expiry monitoring existed for
vault.raxx.appbefore this incident — detection was entirely reactive, via CI pipeline failures roughly 67 minutes after expiry, rather than a proactive alert days in advance. vault.raxx.appused Caddy's self-managed ACME certificate rather than a static Cloudflare Origin CA certificate, even though the exact same architectural conflict (CF proxy + CF Access blocking both ACME challenge types) had already been diagnosed and fixed fortickets.raxx.appfive weeks earlier. That fix was never generalized to the other CF-Access-gated origin on the same account.- Every Cloudflare credential capable of repairing the outage (minting a token, issuing a cert) was stored inside the vault that was down — a circular dependency that turns a routine cert renewal into a break-glass exercise. The only way in was direct host access (SSH) reaching Infisical on its loopback interface, which is not documented as a named, first-class recovery procedure anywhere before this incident.
- The Caddy install step needed two attempts: the initial install into
/etc/ssl/private/(following the exact path convention used bytickets.raxx.app's Apache setup) failed because Caddy runs as a non-root, non-www-data user (caddy) that cannot traverse the root-only0700/etc/ssl/private/directory the way Apache (running aswww-data/root-owned config) can. This is a real difference between the two origins' runtime users that the reused Apache-oriented procedure did not anticipate. - This agent (sre-agent, dispatched separately in FIX mode) was blocked from completing the mutating steps (token mint, cert issuance, Caddy install/reload) by its own session's auto-mode permission controls, which correctly classified "mint a new Cloudflare credential with SSL-write scope" and "reload TLS on a production host" as requiring live, in-session operator review rather than accepting a prior async instruction as sufficient authorization. This is a reasonable safety property, but it meant the fix had to be completed from a different, directly-supervised session rather than the one that did the diagnosis — a coordination cost worth noting for future SEV-2 TLS incidents.
Root cause analysis
-
Contributing factor 1 — ACME is architecturally incompatible with this host's Cloudflare configuration.
vault.raxx.appis both orange-cloud proxied (Cloudflare terminates client TLS at the edge, sotls-alpn-01challenges never reach Caddy) and gated by Cloudflare Access (unauthenticated requests to any path, including an ACMEhttp-01challenge path, receive a CF Access login redirect instead of reaching Caddy). Caddy's default on-demand/automatic ACME therefore cannot succeed on this host under any circumstances, and will silently fail at every renewal attempt until the existing certificate expires and breaks the site outright. -
Contributing factor 2 — the working fix for the identical architecture was not generalized.
tickets.raxx.appsits behind the same Cloudflare zone with the same proxy + Access combination, and the June 2026 526 incident (docs/incidents/2026-06-19-freescout-526-ssl-strict.md) already replaced its self-signed/ACME-adjacent cert with a static Cloudflare Origin CA certificate for exactly this reason. That runbook update (freescout-cert-renewal.md) was never cross-applied tovault.raxx.app's Caddy configuration, leaving the second CF-Access-gated origin on the account exposed to the same failure mode it had already been fixed for once. -
Contributing factor 3 — no cert-expiry monitoring. Nothing polled
vault.raxx.app's served TLS certificate expiry date. The only signal that anything was wrong was CI pipelines failing with CF 526 well after the certificate had already expired — a reactive, user(CI)-facing detection path rather than a proactive one with days of lead time. -
Contributing factor 4 — credential bootstrap circularity. Every Cloudflare API token capable of minting a replacement cert-issuing token or issuing the Origin CA certificate directly is stored in Infisical, which is served from behind the very TLS endpoint that broke. There was no documented, rehearsed "vault is down, here is how you get a Cloudflare credential anyway" procedure before this incident; it had to be improvised live (SSH to origin → hit Infisical on loopback, bypassing the CF edge).
Detection
- What alerted us: CI pipeline failures (CF 526) on PR #4331 and PR #4332, roughly 67 minutes after the certificate actually expired.
- Time between cause and detection: approximately 1h7m (21:23:17 expiry → ~22:30 first CI failure observed).
- How to detect faster next time: fleet cert-expiry monitoring is now live (PR #4332) and will alert on days-of-lead-time before expiry rather than relying on CI pipelines to notice a hard failure after the fact. Action item below extends its coverage explicitly to
vault.raxx.appand confirmstickets.raxx.appis also covered.
Resolution
-
What was changed: 1. A short-lived (≤2h TTL), scoped Cloudflare API token with SSL-and-Certificates-write permission was minted from
CLOUDFLARE_ACCESS_MGMT_TOKEN, reached via direct SSH-to-origin → local Infisical (127.0.0.1:8080) — bypassing the broken Cloudflare edge entirely for the credential fetch. 2. A 3-year (1095-day, operator-mandated cap — not the legacy 15-year/5475-day validity used fortickets.raxx.appin June) Cloudflare Origin CA certificate was issued forvault.raxx.appagainst a CSR regenerated from the pre-staged private key. 3. Certificate and key were installed at/etc/caddy/tls/cf-origin-vault.pemand/etc/caddy/tls/cf-origin-vault.key(ownedroot:caddy, directory750, files640) — not/etc/ssl/private/, which the unprivilegedcaddyuser cannot traverse. 4. The Caddyfile'svault.raxx.appsite block now has a statictlsdirective pointing at the installed cert/key, disabling ACME for this site permanently. 5.systemctl reload caddycompleted successfully; Zone SSL mode remainedstrictthroughout, unchanged. 6. The minted short-lived Cloudflare token was revoked immediately after cert issuance. 7. WP pipelines 2782 (PR #4331) and 2790 (PR #4332) were restarted. -
Validation (independently re-verified by sre-agent after handoff, via direct SSH + external curl, all read-only):
sudo openssl s_client -connect localhost:443 -servername vault.raxx.appon the origin showsissuer=...CloudFlare Origin SSL Certificate Authority...,subject=...CloudFlare Origin Certificate,notBefore=Jul 24 03:02:00 2026 GMT,notAfter=Jul 23 03:02:00 2029 GMT.systemctl is-active caddy→active; Caddy access logs show real production traffic (raxx-console-vault-status/1.0) already succeeding end-to-end through the fixed edge with HTTP 200 responses.curl -sI https://vault.raxx.app/(external, through the Cloudflare edge) returnsHTTP/2 302to the Cloudflare Access login page withserver: cloudflareand acf-rayheader present — this is the correct unauthenticated response (CF Access gate working as designed), and critically is no longer CF error 526./etc/caddy/tls/confirmeddrwxr-x--- root caddy,cf-origin-vault.keyrw-r----- root caddy,cf-origin-vault.pemrw-r--r-- root caddy.- WP pipeline green status for #4331/#4332 was reported by the resolving session and was not independently re-queried by sre-agent in this session (no Woodpecker admin token available in this shell context) — recommend the operator or a subsequent sweep confirm
ci-pr/lint-cf-tokensare green on both PRs before treating them as unblocked.
Action items
| # | Action | Owner | Due | Issue |
|---|---|---|---|---|
| 1 | Confirm tickets.raxx.app's existing Cloudflare Origin CA cert (from the June 2026 fix, notAfter 2041-06-15) and the new vault.raxx.app cert (notAfter 2029-07-23) are both enrolled in the fleet cert-expiry monitor delivered in PR #4332; add an explicit test case for each hostname if not already covered |
sre-agent | 2026-07-30 | — |
| 2 | Document the SSH-to-origin → local-Infisical-on-loopback break-glass path as a canonical, named recovery procedure (not an improvisation) in docs/ops/runbooks/vault-disaster-recovery.md, for use whenever every Cloudflare-capable credential is unreachable because the vault edge itself is down |
sre-agent | 2026-07-25 | — |
| 3 | Reconcile the two documented Cloudflare token-minting endpoints for Origin CA issuance — POST /client/v4/user/tokens (used for tickets.raxx.app, June 2026) vs. the account-scoped path used for this incident's vault.raxx.app fix — into a single authoritative procedure in docs/ops/runbooks/cloudflare-tokens.md, confirming which scopes on CLOUDFLARE_ACCESS_MGMT_TOKEN back each path |
operator | 2026-08-06 | — |
| 4 | Audit whether any other Caddy- or nginx-fronted origin behind this Cloudflare zone still relies on self-managed ACME while sitting behind CF Access; migrate any found to the static Origin CA cert pattern proactively rather than reactively | sre-agent | 2026-08-13 | — |
References
- Runbook:
docs/ops/runbooks/vault-disaster-recovery.md(to be updated — see action item 2) - Runbook:
docs/ops/runbooks/cloudflare-tokens.md(to be updated — see action item 3) - Runbook:
docs/ops/runbooks/freescout-cert-renewal.md(prior art for Cloudflare Origin CA issuance pattern) - Related incident:
docs/incidents/2026-06-19-freescout-526-ssl-strict.md(same root architectural conflict,tickets.raxx.app, 2026-06-19) - Cloudflare Origin CA docs:
https://developers.cloudflare.com/ssl/origin-configuration/origin-ca/ - Cloudflare cert API:
POST https://api.cloudflare.com/client/v4/certificates