Skip to content

fix(ra): ANS name is permanently consumed on cancel/failure — agent_registrations holds the name before an agent is ever active #88

Description

@jhateley-godaddy

Summary

RegistrationService.finalizeServerOrder documents that a terminal
certificate-order failure leaves the lifecycle at PENDING_VALIDATION
"so the operator can cancel and re-register without the ANS name being
burned by a FAILED agent" (internal/ra/service/lifecycle.go:702-703).
That guarantee does not hold: the ANS name is consumed the moment a row
exists in agent_registrations, and it stays consumed after cancel.

Current behavior

  • Re-registration is gated by preflightRegistrationConflicts, which
    calls ExistsByAnsName (internal/ra/service/registration.go:579).
  • ExistsByAnsName is status-blind:
    SELECT COUNT(1) FROM agent_registrations WHERE ans_name = ?
    (internal/adapter/store/sqlite/agent.go:328-335).
  • Cancel/revoke retires the row to REVOKED but does not remove it, so
    the ans_name still matches → re-register fails with ANS_NAME_TAKEN.

Net: a name is burned by a registration that never reached ACTIVE,
which is exactly what the comment says won't happen.

Expected behavior

An ANS name should only be consumed once an agent becomes ACTIVE (i.e.
once an AGENT_REGISTERED leaf is sealed into the TL and the name is
resolvable). A registration that fails or is cancelled before activation
should free the name for re-registration by the same owner.

Root cause

In-flight registrations and committed registrations share one table
(agent_registrations) keyed on ans_name. There is no separation
between "an application in progress" and "a name that is live in the
registry," so name exclusivity is enforced from first insert rather than
from activation.

Proposed fix

Introduce a dedicated agent_application table for in-flight
registrations. A name is promoted into agent_registrations
(and thus becomes "taken") only at activation. Pre-active cancel/failure
simply drops the application row, freeing the name.

  • Names are never taken until an agent becomes ACTIVE.
  • Removes the status-blind exclusivity check as the burn point.

Scope / acceptance criteria

  • New agent_application table + store/port for in-flight state.
  • Name exclusivity (ANS_NAME_TAKEN) enforced against active
    registrations only; already-ACTIVE/DEPRECATED names stay burned
    (identity-confusion protection — a name with a TL leaf must never
    be reused).
  • Cancel/failure before activation frees the name; re-registration
    by the same owner succeeds.
  • finalizeServerOrder comment matches actual behavior.
  • Migration + tests; internal/domain stays at 100%, make check
    passes.

Notes

Preserve the existing invariant that names which did reach ACTIVE
(TL leaf sealed, potentially resolved/cached by clients) remain
permanently burned. The security boundary is "was this name ever
ACTIVE," not "did it pass verify-acme."

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions