/** * Tell a STALE Pebble root apart from an unfinished ACME issuance. * * Pebble mints a random root CA at every startup and Caddy caches its * certificate until expiry, so a Pebble that restarted without the website * sim restarting serves a cert whose issuing root no longer exists. The bake * probe waits for HTTPS to VERIFY, so that trust failure spends the whole * 240s budget and then reports a TIMEOUT — and waiting longer cannot fix it * (celilo#1427). * * The two conditions need opposite responses and curl's exit code cannot * separate them: a cold caddy that has obtained nothing yet also fails the * handshake. The evidence that CAN is the chain itself — compare the root CN * Pebble is serving right now against the issuers in the chain the website * hands out. * * Both inputs are openssl output captured inside the management container: * live: `curl -sk https://100.64.0.100:15000/roots/0 | openssl x509 -noout -subject` * served: `openssl s_client -showcerts ... | grep '^ i:'` */ export type StaleRootVerdict = /** The served chain is signed by the root Pebble is serving now. */ | { kind: 'current' } /** Not enough evidence — no chain served yet, or no live root readable. */ | { kind: 'unknown'; reason: string } /** The served chain names a root Pebble no longer has. Fatal, instantly. */ | { kind: 'stale'; liveRoot: string; servedIssuers: string[] }; /** `subject=CN = Pebble Root CA 2a43b9` / `subject= CN=Pebble Root CA 2a43b9` -> the CN. */ function commonName(line: string): string { const m = /\bCN\s*=\s*([^,/\n]+)/.exec(line); return m ? m[1].trim() : ''; } export function detectStaleAcmeRoot( liveRootSubject: string, servedChain: string, ): StaleRootVerdict { const liveRoot = commonName(liveRootSubject); if (!liveRoot) { return { kind: 'unknown', reason: 'no live Pebble root CN readable' }; } const servedIssuers = servedChain .split('\n') .filter((l) => /(^|\s)i[:=]/.test(l)) .map(commonName) .filter(Boolean); if (servedIssuers.length === 0) { return { kind: 'unknown', reason: 'no certificate chain served yet' }; } if (servedIssuers.includes(liveRoot)) { return { kind: 'current' }; } return { kind: 'stale', liveRoot, servedIssuers }; } export function explainStaleRoot(v: Extract): string { return [ `celilo.computer is serving a certificate from a DEAD Pebble root. Chain issuers: ${v.servedIssuers.join(' <- ')}. Pebble's live root is "${v.liveRoot}".`, 'Pebble mints a random root at every startup and Caddy caches its cert until expiry, so Pebble restarted without the website sim restarting (celilo#1427).', 'This is NOT a timeout and waiting cannot fix it.', 'Remedy: `cele2e down` then `cele2e build-infra`, so Pebble and Caddy start together.', 'Restarting the website sim ALONE makes it worse — it comes back on Docker embedded DNS and cannot reach the simulated ACME endpoint at all.', ].join(' '); }