A removed reserved-name route keeps refusing the handshake until restart #53
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while confirming the resolver's refuse-vs-fallback rule is internally consistent. Pre-existing, not from #52's fixes.
The rule, as everything else implements it
Whether a name is refused or served the self-signed fallback turns on whether a configured route host covers it — exactly, or by wildcard:
SniResolver::remove's doc states the third row from the other direction: "Forget a name entirely — it is no longer configured, so the fallback applies to it again."Where it breaks
A reserved name (
.localhost,.test, …) never gets acertificatesrow.reconcile_once's skip branch tombstones it in the resolver andcontinues before any DB write:But the only cleanup path iterates DB rows:
resolver.removeis called in exactly one place in the codebase, and it is this one. A name with no row is never reached, so theNotCertifiabletombstone outlives the route that created it for the life of the process.Reproduction
host = "grafana.localhost". The reconciler logs it skipped; the resolver holdsNotCertifiable.grafana.localhoststill aborts the handshake with an access-denied alert. It should now be an unconfigured name and get the fallback.load_into_resolverrebuilds from DB rows and there is no row to tombstone.Why only this branch
The issuable-name failure path is unaffected:
db.record_certificate_erroris anINSERT … ON CONFLICT, so even a name whose first order fails has a row, and the deletion loop cleans it up normally. The skip branch is the one path that tombstones without a row.Impact
Low — it only affects names no public CA can issue for, which are development names by definition, and a restart clears it. Filing because it is a genuine inconsistency in the posture rule rather than a cosmetic one: the same name is refused or served the fallback depending on process history rather than current config.
Options
GET /api/certificates, where a skipped name is currently invisible).desiredrather than against DB rows, which fixes this class rather than this instance.The second is the better shape — the resolver is described as the single source of truth for what ER can serve, so converging it from the desired set directly removes the dependency on a DB row existing.
Fixed in
d0801d3, taking your second option — the resolver's key set now converges ondesiredrather than on the DB rows.Your reasoning for preferring it holds up: the resolver is documented as the single source of truth for what Edge Router can serve, so converging it from the desired set removes the dependency on a row existing, instead of writing a row for skipped names to paper over the one case that lacks one.
Worth separating from the loop above it, which that code warns is irreversible — it deletes a private key, so a transient absence costs a re-order against the weekly limit. The resolver convergence is not that: it forgets an in-memory entry, making the name unconfigured, which is what it now is. No key is touched, and any entry worth keeping belongs to a desired name. It inherits the same "desired is real" assumption as the row deletion without widening it, and if a spurious empty set ever did get through, the row loop's key deletion is already the worse outcome.
The test runs two passes — the name tombstoned while its route exists, then absent — and asserts the premise in between:
db.list_certificates()is empty, so there is genuinely no row to clean up. Without that assertion it could have passed for the wrong reason. It failed on the final assertion only, before the fix.384 tests pass, clippy
--all-targets -D warningsclean, fmt clean. Invariant recorded in CLAUDE.md next to the tombstone-seeding one, since the two are easy to conflate.No release-notes entry: the behaviour was reachable only with development names and cleared on restart, and the notes already describe the refuse-vs-fallback rule it restores.