Feature Specification: Effects registrar-*

Created: 2026-10-07 Status: Implemented Input: Reconstructed from the code of core/effects/tf/ and core/setups/ at 14504f80d, issue #2131.

The registrar effects publish the delegation of the group apex at the registrar. There is one effect per step: the domain registration, the nameservers, the DNSSEC key, and one glue effect per DNS node label. Each effect applies one OpenTofu graph with one provider and one resource. The records in the zone are the job of octodns.md. The registrar holds no records of the zone, so that only the delegation depends on the choice of registrar, and the zones keep the standard protocols of the DNS services of the deployment. Where deSEC holds the apex, the effects delegate it to deSEC instead: no glue, the NS set of deSEC, and the key that deSEC signs with. The effects deploy no nodes. The tf-incus setup builds them (tf-incus.md). The shared behavior of a standalone effect is in spec.md. The verbs and the state rules are in 004.

One dispatch table names, per registrar, the steps that it offers, the effect of each step and the coordinates that each step needs:

Registrar domain nameservers glue dnssec
porkbun yes (max cost) yes yes yes (DS)
inwx no no yes yes (DNSKEY)
openprovider yes (max cost, owner handle) nameserver group yes no (keys on the registration)

User Scenarios & Testing (mandatory)

User Story 1 - Delegate the apex to the group nameservers (Priority: P1)

An operator deploys a group that runs its own nameservers. The operator applies the glue for each DNS node and then the nameservers. The parent zone then sends resolvers to the group nameservers.

Why this priority: Without the delegation, no resolver finds the zone that octodns-zone publishes.

Independent Test: A unit test of the setup checks the arguments and the state keys of each step, and a unit test evaluates each generated graph. No integration test applies the glue or the nameservers.

Acceptance Scenarios:

  1. Given a registrar, a registrar state address and a group with DNS nodes, When the catalog lists the group effects, Then it lists one glue effect for each DNS node label of that group.
  2. Given a nameserver in the zone that it serves, When the operator applies the nameservers at openprovider, Then the nameserver group carries the IPv4 and IPv6 addresses of the node.
  3. Given a DNS node that this deployment does not stand up, When the setup evaluates, Then it makes no glue for that node.

User Story 2 - Register the apex under a spending bound (Priority: P1)

The operator states a maximum cost and applies the domain registration. The provider asks the registrar for a live quote and registers the domain only at or below the bound.

Why this priority: A registration spends money and holds the name that all other records depend on.

Independent Test: A unit test of the setup checks that the registration is absent until its coordinates are given. An integration test applies the porkbun and openprovider registrations against a fake registrar API: a quote above the bound fails the apply and orders nothing, a quote at the bound registers, and a destroy plan fails. The build of the openprovider provider runs its unit tests, which also refuse a quote in a different currency, a zero quote and a failed quote.

Acceptance Scenarios:

  1. Given no maximum cost, When the catalog lists the group effects, Then it does not list the domain registration.
  2. Given a quote above the bound, in a different currency, or no quote, When the operator applies the registration, Then the apply fails and names the numbers.
  3. Given a registered domain, When the operator runs a destroy, Then OpenTofu refuses.

User Story 3 - Close the chain of trust (Priority: P2)

The apex node makes its signing key at run time and writes its DS to a file. The operator copies that value into the DS or DNSKEY coordinate and applies the DNSSEC step.

Why this priority: The zone is signed, but validating resolvers trust it only when the parent holds the DS.

Independent Test: A unit test of the setup checks that the DS reads the same as four fields or as a whole record.

Acceptance Scenarios:

  1. Given no DS coordinate, When the catalog lists the group effects, Then it does not list the DNSSEC step.
  2. Given a whole DS record, When the setup evaluates, Then the step takes the key tag, the algorithm, the digest type and the digest from it.
  3. Given a DS value that does not parse, When the setup evaluates, Then the evaluation fails and prints the value.

User Story 4 - Delegate the apex to deSEC (Priority: P2)

The group DNS is desec, so deSEC holds the apex and signs it. The operator applies the nameservers and the DNSSEC step (at openprovider: the registration). The parent zone then sends resolvers to deSEC and holds the DS of the key that deSEC signs with. The operator states no key: the plan reads it from the deSEC API.

Why this priority: The apex then needs no nameserver of the group, but a delegation that points elsewhere, or a DS of a different key, makes the domain fail to resolve.

Independent Test: A unit test of the setup checks the NS set, the absent glue, and the key read of each registrar. A unit test evaluates each generated graph with the key read. The check deployment-zone-keys-probe runs the key read against a fake deSEC API.

Acceptance Scenarios:

  1. Given a desec group DNS and no stated apex NS set, When the setup evaluates, Then the nameserver step names ns1.desec.io and ns2.desec.org.
  2. Given a desec group DNS and a stated apex NS set, When the setup evaluates, Then the nameserver step names the stated set.
  3. Given a desec group DNS and no DS or DNSKEY coordinate, When the operator plans the DNSSEC step, Then the plan reads the key-signing key of the apex from deSEC with the deSEC token of the deploy, and the step publishes it.
  4. Given a desec group DNS and a DS or DNSKEY coordinate, When the setup evaluates, Then the step uses the stated key and reads nothing from deSEC.

Edge Cases

  • A registrar that the table does not hold: the evaluation fails and lists the known registrars.
  • No registrar state address: no registrar effect exists. There is no local state for them.
  • External group DNS other than desec: no registrar effect exists, because that apex is not ours to delegate.
  • desec group DNS: no glue effect exists, because the nameservers of deSEC are not in the zone.
  • A step that the registrar does not offer: that effect does not exist.
  • A DS digest on more than one line: the setup joins the parts.
  • openprovider accepts no dots in the name of a nameserver group: the setup makes the name from the domain without dots.
  • A teardown at openprovider can delete the nameserver group, so a later bootstrap can create it again.

Requirements (mandatory)

Functional Requirements

  • FR-001: The catalog MUST offer a registrar effect only when a registrar and a registrar state address are given, and either the group DNS is not external and the group has DNS nodes, or the group DNS is desec.
  • FR-002: The catalog MUST offer the domain, nameserver and DNSSEC steps only when the registrar offers that step and every coordinate that the step needs is given. Under desec group DNS, the DS and the DNSKEY count as given.
  • FR-003: The catalog MUST offer the glue effect of a label only for groups that run a DNS node with that label, and only when the group DNS is not external.
  • FR-004: Each effect MUST read the key set of the tf-incus method plus the registrar keys: the registrar, the maximum cost, the DS, the DNSKEY, the owner handle and the state address.
  • FR-005: Each effect MUST offer the OpenTofu verbs, and its run MUST be the apply.
  • FR-006: Each step MUST keep its state in the HTTP backend at the registrar state address plus a suffix of its own.
  • FR-007: OpenTofu MUST refuse to destroy the registration.
  • FR-008: The registration MUST refuse to apply when the live quote is above the maximum cost (in US cents), is in a different currency, or is missing.
  • FR-009: The glue of a node MUST name the node and hold all its IPv4 and IPv6 addresses.
  • FR-010: The nameserver step MUST name the delegated NS set as hostnames, and MUST be absent when that set is empty. The delegated set is the apex NS set of the group. Under desec group DNS, it is the stated apex NS set, or ns1.desec.io and ns2.desec.org when none is stated.
  • FR-011: The setup MUST accept a DS as four fields or as a whole record, and a DNSKEY as four fields, as a whole record or as JSON. It MUST fail the evaluation on any other form.
  • FR-012: At openprovider, the registration MUST set the nameserver group only when the stated apex NS set is not empty or the group DNS is desec, and the DNSSEC keys only when a DNSKEY is given or read from deSEC.
  • FR-013: Each provider graph MUST use a committed provider schema, a single provider, and the exact provider version.
  • FR-014: Each provider graph MUST expose only the resource types that the registrar steps use, and MUST NOT expose zone records, contacts, customers or certificate orders.
  • FR-015: Under desec group DNS, the DNSSEC step MUST read the key-signing key of the apex from the deSEC API at plan time when no DS or DNSKEY is given, with the token file that DNS_API_TOKEN_FILE names. A DS read takes the SHA-256 digest. A stated DS or DNSKEY MUST win over the read.

Key Entities

  • Registrar: The account at which the group apex is registered: porkbun, inwx or openprovider.
  • Step: The domain, the nameservers, the glue or the DNSSEC key, each with one effect and its own state.
  • Dispatch table: The table of steps per registrar. The catalog reads it for availability, and the setup for the steps.
  • Apex DS / DNSKEY: The public key data that the apex node makes, which the operator gives back to the effect as a coordinate. Under desec group DNS, the plan reads it from deSEC.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: After all applies, a query for NS at the parent zone gives the apex NS set of the group.
  • SC-002: After the DNSSEC step, the parent zone serves the DS of the key that signs the apex: the key that the apex node wrote, or the key of deSEC.
  • SC-003: No registration costs more than the maximum cost.
  • SC-004: A second plan of each step after its apply shows no change.

Assumptions

  • The operator applies the steps in order. At openprovider: register without a nameserver group, then the glue for each nameserver, then the nameserver group, then the registration with the nameserver group, then the registration with the DNSSEC keys. At porkbun: the glue before the delegation. Under desec group DNS at openprovider: the nameserver group, then the registration.
  • A move of a signed apex from the group nameservers to deSEC goes through an unsigned delegation. The registrar steps give the openprovider provider no empty list of DNSSEC keys, and the provider omits an empty nameserver group, thus a step without them changes nothing. The operator removes the DS at the registrar by hand. See notes/cd-credentials.md.
  • The addresses of the DNS nodes are pinned in NetBox, so the glue changes only on a full rebootstrap.
  • The cost bound and the openprovider glue resource come from pinned providers. The openprovider pin is upstream main until a release contains the changes the registrar step needs.
  • The registrar credentials reach the provider through the environment of the machine that runs the apply.

Known Deviations

  • The effect registry defines a domain effect for inwx, but the dispatch table gives inwx no domain step, so no registrar effect reaches it (#2131).