From 0edebc927bda006bcf7b23872538d3802c01bbbf Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Thu, 6 Aug 2026 18:51:11 -0400 Subject: [PATCH] feat: Implement DNS verification --- envs/lance.blue/main.tf | 29 +++++++++++++++++++++++++++++ envs/lance.blue/variables.tf | 10 ++++++++++ modules/dns/main.tf | 20 ++++++++++++++++++++ modules/dns/variables.tf | 6 ++++++ 4 files changed, 65 insertions(+) diff --git a/envs/lance.blue/main.tf b/envs/lance.blue/main.tf index 37ec1c2..8ddac02 100644 --- a/envs/lance.blue/main.tf +++ b/envs/lance.blue/main.tf @@ -82,9 +82,38 @@ module "dns" { domain_name = var.domain_name + # Two ATProto claims about this domain, both TXT records at the apex. + # + # _atproto binds the lance.blue handle to its DID. It is what makes handle + # verification resolve in both directions, and it is not us hosting identity: + # the account lives on somebody else's PDS, this record only names it. + # + # _lexicon is how a stranger resolves our lexicon schemas. The authority + # domain for an NSID is the name with its last segment removed and the rest + # reversed, so every three-segment blue.lance.* name is published by the DID + # this record points at. A deeper name would need its own record; there is no + # fallback up or down the hierarchy. + txt_records = { + "_atproto" = ["did=${var.atproto_did}"] + "_lexicon" = ["did=${var.lexicon_did}"] + } + tags = { Component = "dns" } } +# _atproto already exists: it was created by hand, before this zone had any +# record in Terraform. Adopt it instead of creating it. Route53 refuses to +# create a record that already exists, and destroying it to recreate it would +# unbind the handle from its DID for as long as the change took to propagate. +# +# The plan may still show an in-place update if the record's current TTL is not +# the module's 300. That is safe; a value change would not be, so read the plan +# before applying. +import { + to = module.dns.aws_route53_record.txt["_atproto"] + id = "${module.dns.zone_id}__atproto.${var.domain_name}_TXT" +} + # The API: one Graviton box running headquarters-api behind Caddy, at # api.. Deploys are image-tag changes. It launches match tasks and # proxies browsers to them, so it carries the control-plane policies. diff --git a/envs/lance.blue/variables.tf b/envs/lance.blue/variables.tf index be381dd..f6bdd90 100644 --- a/envs/lance.blue/variables.tf +++ b/envs/lance.blue/variables.tf @@ -110,3 +110,13 @@ variable "match_task_architecture" { type = string default = "X86_64" } + +variable "atproto_did" { + description = "The DID the lance.blue handle resolves to, published as the _atproto TXT record. This record was created by hand before Terraform managed any record in the zone; the import block in main.tf adopts it rather than recreating it." + type = string +} + +variable "lexicon_did" { + description = "The DID that publishes blue.lance.* lexicon schemas, published as the _lexicon TXT record. The same account as atproto_did today, but a separate variable because it is a separate claim: the handle could move without the schemas moving." + type = string +} diff --git a/modules/dns/main.tf b/modules/dns/main.tf index c9b67f4..3645e81 100644 --- a/modules/dns/main.tf +++ b/modules/dns/main.tf @@ -34,3 +34,23 @@ resource "aws_route53_zone" "this" { tags = merge(var.tags, { Name = var.domain_name }) } + +# TXT records set in this zone by hand elsewhere are not represented here, and +# Terraform will not touch what it does not declare. `_atproto` is one of them: +# it binds the lance.blue handle to its DID and predates this module. +# +# `_lexicon` is not identity either, despite the resemblance. It is how a +# stranger resolves our lexicon schemas: the authority domain for an NSID is +# the name with its last segment removed and the rest reversed, so +# `blue.lance.camo` is published by whichever DID `_lexicon.lance.blue` names. +# There is no fallback up or down the hierarchy - a missing record means +# resolution fails outright - so a deeper NSID prefix would need its own entry. +resource "aws_route53_record" "txt" { + for_each = var.txt_records + + zone_id = aws_route53_zone.this.zone_id + name = each.key == "" ? var.domain_name : "${each.key}.${var.domain_name}" + type = "TXT" + ttl = 300 + records = each.value +} diff --git a/modules/dns/variables.tf b/modules/dns/variables.tf index 4b5353d..fb9a801 100644 --- a/modules/dns/variables.tf +++ b/modules/dns/variables.tf @@ -8,3 +8,9 @@ variable "tags" { type = map(string) default = {} } + +variable "txt_records" { + description = "TXT records to create in the zone, keyed by name relative to the apex (\"\" is the apex itself). Values are the record's strings." + type = map(list(string)) + default = {} +} -- 2.51.2