Feature Specification: Effects registrar-*
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/effects/tf/registrars.nix, core/effects/tf/common/provider-common.nix, core/effects/tf/porkbun/, core/effects/tf/inwx/, core/effects/tf/openprovider/, core/effects/common/standalone-env.nix, core/render/catalog.nix, core/setups/hosts-common.nix).
The registrar effects publish the delegation of the group apex at the
registrar. They are registrar-domain, registrar-nameservers,
registrar-dnssec and one registrar-glue-<label> per DNS node label. Each
effect applies one OpenTofu module with one provider and one resource. The
records inside the zone are the job of octodns.md. The effects deploy no
nodes. The tf-incus-hosts setup builds them as setup.registrar.<step>
(see ../009-deployment-methods/tf-incus.md). The family behavior of
standalone effects is in spec.md. The program grammar, the verb names and
the state rules are in ../004-deployment-programs/spec.md.
The dispatch table core/effects/tf/registrars.nix names, per registrar, the
steps it offers, the effect for each step, and the coordinates each step
needs:
| Registrar | domain |
nameservers |
glue |
dnssec |
|---|---|---|---|---|
porkbun |
tf-porkbun-domain (max cost) |
tf-porkbun-domain-nameservers |
tf-porkbun-glue-record |
tf-porkbun-dnssec-record (DS) |
inwx |
none | none | tf-inwx-glue-record |
tf-inwx-dnssec-key (DNSKEY) |
openprovider |
tf-openprovider-domain (max cost, owner handle) |
tf-openprovider-nsgroup |
tf-openprovider-glue-record |
none (dnssec_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 registrar-glue-<label> for each DNS node and then
registrar-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: core/setups/registrar-test.nix (suite
nix-unit-setups) asserts the leaf arguments and state keys.
effects-tf-registrar-render (suite nix-unit-effects) forces each generated
module to evaluate. No integration test applies the glue or nameserver
effects.
Acceptance Scenarios:
- Given
REGISTRAR,REGISTRAR_STATEand a group with DNS nodes, When the catalog lists the group effects, Then it listsregistrar-glue-<label>for each DNS node label of that group. - Given a nameserver inside the zone it serves, When the operator applies
registrar-nameserversatopenprovider, Then the nameserver group carries the node IPv4 and IPv6 addresses. - Given a DNS node that this deployment does not stand up, When the setup evaluates, Then it makes no glue leaf for that node.
User Story 2 - Register the apex under a spending bound (Priority: P1)
The operator states REGISTRAR_MAX_COST. The operator applies
registrar-domain. 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: core/setups/registrar-test.nix (suite
nix-unit-setups) asserts that the registration is absent until its
coordinates are stated. integrations-registrar-guards applies the
porkbun and openprovider registration 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 because of prevent_destroy. The
build of the openprovider provider runs its unit tests, which also refuse a
quote in another currency, a zero quote and a failed quote.
Acceptance Scenarios:
- Given no
REGISTRAR_MAX_COST, When the catalog lists the group effects, Then it does not listregistrar-domain. - Given a quote above the bound, in another currency, or no quote, When the operator applies
registrar-domain, Then the apply fails and names the numbers. - Given a registered domain, When the operator runs a destroy, Then OpenTofu refuses because of
prevent_destroy.
User Story 3 - Close the chain of trust (Priority: P2)
The apex node mints its signing key at run time and writes the DS to
/var/lib/named/<domain>.ds. The operator copies that value into
REGISTRAR_DS or REGISTRAR_DNSKEY and applies registrar-dnssec.
Why this priority: The zone is signed, but validating resolvers trust it
only when the parent holds the DS.
Independent Test: core/setups/registrar-test.nix (suite
nix-unit-setups) asserts that the DS reads the same as four fields or as
a whole record.
Acceptance Scenarios:
- Given no
DScoordinate, When the catalog lists the group effects, Then it does not listregistrar-dnssec. - Given
REGISTRAR_DSas a wholeDSrecord, When the setup evaluates, Then the leaf takes the key tag, algorithm, digest type and digest from it. - Given
REGISTRAR_DSthat does not parse, When the setup evaluates, Then the eval fails and prints the value.
Edge Cases
REGISTRARnames a registrar that the table does not hold: the eval fails and lists the known registrars.REGISTRAR_STATEis not set: the whole registrar surface is absent. No local state backend exists.- The group DNS is external: the whole registrar surface is absent, because that apex is not ours to delegate.
- The registrar does not offer a step: that effect is absent.
inwxoffers nodomainand nonameservers;openprovideroffers nodnssec. - A
DSdigest wrapped across lines: the setup joins the parts. openprovidertakes no dots in a nameserver group name: the setup derives the group name from the domain without dots.- A teardown at
openprovider: the nameserver group hasallow_deletion = true, so a later bootstrap can create it again.
Requirements (mandatory)
Functional Requirements
- FR-001: The catalog MUST offer a registrar effect only when
REGISTRARandREGISTRAR_STATEare set, the group DNS is not external, and the group has DNS nodes. - FR-002: The catalog MUST offer
registrar-domain,registrar-nameserversandregistrar-dnsseconly when the registrar offers that step and every coordinate that the step needs is set. - FR-003: The catalog MUST offer
registrar-glue-<label>only for groups that run a DNS node with that label. - FR-004: Each effect MUST read the env keys of the
tf-incusmethod plusREGISTRAR,REGISTRAR_MAX_COST,REGISTRAR_DS,REGISTRAR_DNSKEY,REGISTRAR_OWNER_HANDLEandREGISTRAR_STATE. - FR-005: Each effect MUST offer the OpenTofu verbs, and
runMUST be the apply. - FR-006: Each leaf MUST keep its state in the HTTP backend at
REGISTRAR_STATEplus its own suffix:-domain,-nameservers,-dnssecor-glue-<label>. - FR-007: The registration MUST carry
prevent_destroy = true. - FR-008: The registration MUST refuse to apply when the live quote is above
REGISTRAR_MAX_COST(US cents), in another currency, or missing. - FR-009: The glue for a node MUST name the node and hold all addresses of that node, IPv4 and IPv6.
- FR-010: The
nameserversstep MUST name the group apexNSset as hostnames, and MUST be absent when that set is empty. - FR-011: The setup MUST accept
REGISTRAR_DSas four fields or as a wholeDSrecord, and MUST fail the eval on any other form. - FR-012: The setup MUST accept
REGISTRAR_DNSKEYas four fields, as a wholeDNSKEYrecord, or as JSON, and MUST fail the eval on any other form. - FR-013: At
openprovider, the registration MUST setns_grouponly when the apexNSset is not empty, anddnssec_keysonly whenREGISTRAR_DNSKEYis set. - FR-014: Each provider module MUST use the committed
provider-schema.json, a single provider, and the exact provider version. - FR-015: Each provider module MUST expose only the resource types that the registrar steps use, and MUST NOT expose zone records, contacts, customers or certificate orders.
Key Entities
- Registrar: the account at which the group apex is registered. One of
porkbun,inwxoropenprovider. - Step: one of
domain,nameservers,glueordnssec, each with one effect and its own state. - Dispatch table:
core/effects/tf/registrars.nix, read by the catalog for availability and by the setup for the leaves. - Apex
DS/DNSKEY: the public key data that the apex node mints, given back to the effect as a coordinate.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001: After all applies, a query for
NSat the parent zone returns the group apexNSset. - SC-002: After
registrar-dnssec, the parent zone serves theDSthat the apex node wrote. - SC-003: No registration costs more than
REGISTRAR_MAX_COST. - SC-004: A second plan of each leaf after its apply shows no change.
Assumptions
- The operator applies the steps in order. At
openprovider: register withoutns_group, then glue for each nameserver, then the nameserver group, then the registration withns_group, then the registration withdnssec_keys. Atporkbun: glue before the delegation. netbox-pinning.nixpins the DNS node addresses, so the glue changes only on a full rebootstrap.- The cost bound and the
openproviderglue resource come from the providers thatnpins/sources.jsonpins andcore/effects/tf/common/providers.nixbuilds. Theopenproviderpin is a fork branch until an upstream release contains its fix, seenpins/README.md. - The registrar credentials reach the provider through the env of the machine that runs the apply.
Known Deviations
core/effects/default.nixdefinestf-inwx-domain, butregistrars.nixgivesinwxnodomainstep, so no registrar effect reaches it (#2131).