feat(domains): verify domain ownership via DNS #19

Merged
ryan.mcgee merged 43 commits from feat/domains into main 2026-10-15 18:08:57 +00:00
Owner

Summary

  • Organizations can prove they own a domain with a DNS TXT record, so a name can be tied to one organization.
  • A verified domain can ask Traefik for one wildcard certificate instead of one per name.
  • Stacked on #18 (feat/tcp-sni).

Changes

  • New domains table (migration 20261016110000), Domains context, and an admin-only Domains page: add a domain, see the record to create, check DNS, set the wildcard option and certificate resolver, remove.
  • Verification: _traefik-control.<domain> TXT traefik-control-verification=<token>. The token is random and never reused; a partial unique index lets only one organization hold a name as verified; checks are throttled to one per 10 seconds; a DNS outage is reported as such rather than as a missing record.
  • TRAEFIK_CONTROL_REQUIRE_VERIFIED_DOMAINS=true (default off) requires HTTP resource names, and SNI-routed TCP names, to lie under a verified domain. Only a name being set is checked, so existing resources keep working.
  • Wildcard: routers for the domain and the names directly under it get tls.domains (example.com plus *.example.com) and the domain's resolver (or the node's). Deeper names keep their own certificate. Traefik Control only names the resolver; it holds no DNS credentials, and the resolver itself lives in the node's static Traefik configuration.
  • API: /api/v1/domains with verify; OpenAPI spec, audit category domains, README.
  • Not in blueprints (they need a DNS record to be created).

Verification

  • mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict, mix dialyzer: clean.
  • mix test: 1453 passed on SQLite and on PostgreSQL.
  • Migration up, down, up with data on both backends; the partial unique index blocks a second verified claim and a duplicate name within an organization.
  • Real resolver against a local DNS server: TXT records found (multi-chunk joined), unknown name is an empty answer, an unreachable server is an error.
  • Real Traefik (file provider): router tls.domains accepted, and Traefik asked for ["example.com", "*.example.com"] once for both routers; a deeper name got its own request.
  • Browser: added a domain on the Domains page, saw the record, "Check DNS" reported not found.

Not verified

  • A real DNS-challenge certificate was not issued (needs a real domain and DNS provider credentials).
  • Rolling back a deployment does not undo certificates Traefik already holds.
## Summary - Organizations can prove they own a domain with a DNS TXT record, so a name can be tied to one organization. - A verified domain can ask Traefik for one wildcard certificate instead of one per name. - Stacked on #18 (`feat/tcp-sni`). ## Changes - New `domains` table (migration `20261016110000`), `Domains` context, and an admin-only **Domains** page: add a domain, see the record to create, check DNS, set the wildcard option and certificate resolver, remove. - Verification: `_traefik-control.<domain>` TXT `traefik-control-verification=<token>`. The token is random and never reused; a partial unique index lets only one organization hold a name as verified; checks are throttled to one per 10 seconds; a DNS outage is reported as such rather than as a missing record. - `TRAEFIK_CONTROL_REQUIRE_VERIFIED_DOMAINS=true` (default off) requires HTTP resource names, and SNI-routed TCP names, to lie under a verified domain. Only a name being set is checked, so existing resources keep working. - Wildcard: routers for the domain and the names directly under it get `tls.domains` (`example.com` plus `*.example.com`) and the domain's resolver (or the node's). Deeper names keep their own certificate. Traefik Control only names the resolver; it holds no DNS credentials, and the resolver itself lives in the node's static Traefik configuration. - API: `/api/v1/domains` with `verify`; OpenAPI spec, audit category `domains`, README. - Not in blueprints (they need a DNS record to be created). ## Verification - `mix format --check-formatted`, `mix compile --warnings-as-errors`, `mix credo --strict`, `mix dialyzer`: clean. - `mix test`: 1453 passed on SQLite and on PostgreSQL. - Migration up, down, up with data on both backends; the partial unique index blocks a second verified claim and a duplicate name within an organization. - Real resolver against a local DNS server: TXT records found (multi-chunk joined), unknown name is an empty answer, an unreachable server is an error. - Real Traefik (file provider): router `tls.domains` accepted, and Traefik asked for `["example.com", "*.example.com"]` once for both routers; a deeper name got its own request. - Browser: added a domain on the Domains page, saw the record, "Check DNS" reported not found. ## Not verified - A real DNS-challenge certificate was not issued (needs a real domain and DNS provider credentials). - Rolling back a deployment does not undo certificates Traefik already holds.
- add Domains: an organization lists a domain and proves it with
  a TXT record; only one organization can hold a name verified
- optionally require resource names to lie under a verified domain
  (TRAEFIK_CONTROL_REQUIRE_VERIFIED_DOMAINS), checked when set
- verified domains can request one wildcard certificate: routers
  for the domain and its direct children get tls.domains and a
  DNS-challenge resolver named in the node's Traefik config
- web page, API (/api/v1/domains), audit category and tests
ryan.mcgee changed target branch from feat/tcp-sni to main 2026-10-15 18:08:46 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
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
White_Ghost_Cyber_Systems/traefik-control!19
No description provided.