A name has no resolver entry until the serial reconcile loop reaches it, so it is served the fallback #57

Closed
opened 2026-10-26 15:38:00 +00:00 by james.oates · 1 comment
Owner

From the v2.0.0 security audit.

load_into_resolver tombstones only names that already have a certificates row, so a fresh install tombstones nothing. The tombstone is written inside reconcile_once's per-name loop, after the previous name's ACME order has completed.

Issuance is serial with ORDER_SPACING plus up to ~4 minutes of ACME per name (24×5s propagation poll, two 120s poll_ready/poll_certificate timeouts). On a fresh deployment with a dozen configured names, the last names have no resolver entry for tens of minutes — so SNI for them resolves to the self-signed edge-router.local fallback instead of aborting the handshake. The same window opens for a name added over the API or by a config reload while a pass holds PASS.

src/api.rs:597 already comments on exactly this symptom ("had no certificate and was being served the fallback"), but only the readiness report was fixed, not the serving.

Distinct from #53 (a stale tombstone outliving its route) and #48 (deletion with no grace period) — this is a name that has no entry yet rather than one whose entry is wrong.

Fix

Seed mark_unavailable for every desired-but-unknown name at the top of the pass, before the first order. Cheap, and it makes the window refuse rather than substitute.

Worth checking the readiness interaction while doing it: a name tombstoned Unavailable counts against /ready, which is correct here — it genuinely is not ready — but confirm a fresh install reports not-ready rather than flapping.

From the v2.0.0 security audit. `load_into_resolver` tombstones only names that already have a `certificates` row, so a fresh install tombstones nothing. The tombstone is written inside `reconcile_once`'s per-name loop, after the previous name's ACME order has completed. Issuance is serial with `ORDER_SPACING` plus up to ~4 minutes of ACME per name (24×5s propagation poll, two 120s `poll_ready`/`poll_certificate` timeouts). On a fresh deployment with a dozen configured names, the last names have no resolver entry for tens of minutes — so SNI for them resolves to the self-signed `edge-router.local` fallback instead of aborting the handshake. The same window opens for a name added over the API or by a config reload while a pass holds `PASS`. `src/api.rs:597` already comments on exactly this symptom ("had no certificate and was being served the fallback"), but only the readiness *report* was fixed, not the serving. Distinct from #53 (a stale tombstone outliving its route) and #48 (deletion with no grace period) — this is a name that has no entry *yet* rather than one whose entry is wrong. ## Fix Seed `mark_unavailable` for every desired-but-unknown name at the top of the pass, before the first order. Cheap, and it makes the window refuse rather than substitute. Worth checking the readiness interaction while doing it: a name tombstoned `Unavailable` counts against `/ready`, which is correct here — it genuinely is not ready — but confirm a fresh install reports not-ready rather than flapping.
Author
Owner

Fixed in 99adeaa. reconcile_once now seeds a tombstone for every desired name before any ordering begins, instead of writing each one as the serial loop reaches it.

Seeding touches only names with no resolver entry, so a loaded certificate keeps serving across a pass and an existing tombstone is left alone. The tombstone kind matches what the loop would choose (NotCertifiable for a reserved name, Unavailable otherwise), so a .localhost route does not briefly count against readiness — which answers the readiness question raised in the issue.

Two tests:

  • every_desired_name_is_tombstoned_before_the_first_order asserts from inside the first order() call, via a snapshotting orderer that records the resolver's known names. Asserting after the pass would prove nothing, because by then every name has an entry either way.
  • seeding_a_tombstone_does_not_clobber_a_loaded_certificate is the negative control: a name with a usable certificate must still resolve after a pass.

354 tests green, clippy clean, fmt clean.

Fixed in `99adeaa`. `reconcile_once` now seeds a tombstone for every desired name before any ordering begins, instead of writing each one as the serial loop reaches it. Seeding touches only names with no resolver entry, so a loaded certificate keeps serving across a pass and an existing tombstone is left alone. The tombstone kind matches what the loop would choose (`NotCertifiable` for a reserved name, `Unavailable` otherwise), so a `.localhost` route does not briefly count against readiness — which answers the readiness question raised in the issue. Two tests: - `every_desired_name_is_tombstoned_before_the_first_order` asserts from *inside* the first `order()` call, via a snapshotting orderer that records the resolver's known names. Asserting after the pass would prove nothing, because by then every name has an entry either way. - `seeding_a_tombstone_does_not_clobber_a_loaded_certificate` is the negative control: a name with a usable certificate must still resolve after a pass. 354 tests green, clippy clean, fmt clean.
Sign in to join this conversation.
No labels
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
IsoHex/edge-router#57
No description provided.