carlos / platform Public

mail: the DNS record set for a sending identity

Pins the two choices that cannot be changed later: the MAIL FROM subdomain
carries its region from the first domain ever verified (for a custom domain
it lives in the customer's zone), and DMARC carries adkim=s, which is the
tenant boundary on a shared registrable domain rather than hygiene. A test
asserts no record ever sits at the app host, where it would fight the edge's
A record.
Paul Campbell 09864a29f54ac63a2851aeb536e0cf7aa9d413a9 parent 26c9ae8
2 files changed, +200 −0
  • internal/mail/dnsrecords.go +93 −0
  • internal/mail/dnsrecords_test.go +107 −0
diff --git a/internal/mail/dnsrecords.go b/internal/mail/dnsrecords.go
new file mode 100644
index 000000000..01d0fe242
--- /dev/null
+++ b/internal/mail/dnsrecords.go
@@ -0,0 +1,93 @@
+// The DNS record set a sending identity needs (spec §7). Pure: SES's DKIM
+// tokens in, resource record sets out. Who WRITES them is the one difference
+// between a platform domain (CARLOS owns the zone) and a custom domain (the
+// customer publishes the same set by hand) — the set itself is identical,
+// which is what makes the two paths one code path.
+package mail
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/carlosframework/platform/internal/dnsrec"
+)
+
+// recordTTL is short on purpose: these records change on rotation and on
+// teardown, and a long TTL turns "we removed it" into "it is still being
+// answered for a day".
+const recordTTL = 300
+
+// MailFromDomain is the custom MAIL FROM subdomain for a domain in a region.
+//
+// The region is IN THE NAME, and it is there from the first domain ever
+// verified. SES's MAIL FROM MX target is region-specific, so one subdomain
+// cannot serve two regions without ambiguity — and for a custom domain this
+// record lives in the CUSTOMER's zone, which makes it the one record we
+// cannot republish for them later. The full region name is used rather than
+// an abbreviation so there is no code table to keep in sync.
+func MailFromDomain(domain, region string) string {
+ return "mail-" + region + "." + domain
+}
+
+// RecordSet renders every record a sending identity needs. rua is the DMARC
+// aggregate-report mailto target; empty means publish no rua (spec §7).
+//
+// Note what is NOT here: any record at `domain` itself. The edge already owns
+// an A record at an app's host, so every name below is a distinct label
+// underneath it. That separation is what lets the email pass and the
+// provisioning reconciler write into one zone without knowing about each
+// other, and TestNoRecordSitsAtTheDomainApex exists to keep it true.
+func RecordSet(domain, region string, dkimTokens []string, rua string) []dnsrec.Record {
+ out := make([]dnsrec.Record, 0, len(dkimTokens)+3)
+ for _, tok := range dkimTokens {
+ out = append(out, dnsrec.Record{
+ Name: tok + "._domainkey." + domain,
+ Type: dnsrec.TypeCNAME,
+ Values: []string{tok + ".dkim.amazonses.com"},
+ TTL: recordTTL,
+ })
+ }
+ mailFrom := MailFromDomain(domain, region)
+ out = append(out,
+ dnsrec.Record{
+ Name: mailFrom,
+ Type: dnsrec.TypeMX,
+ Values: []string{fmt.Sprintf("10 feedback-smtp.%s.amazonses.com", region)},
+ TTL: recordTTL,
+ },
+ // SPF is checked against the MAIL FROM domain, so it belongs here
+ // and not at the identity domain.
+ dnsrec.Record{
+ Name: mailFrom,
+ Type: dnsrec.TypeTXT,
+ Values: []string{"v=spf1 include:amazonses.com ~all"},
+ TTL: recordTTL,
+ },
+ dnsrec.Record{
+ Name: "_dmarc." + domain,
+ Type: dnsrec.TypeTXT,
+ Values: []string{dmarcValue(rua)},
+ TTL: recordTTL,
+ },
+ )
+ return out
+}
+
+// dmarcValue builds the DMARC policy.
+//
+// adkim=s and aspf=s are NOT hygiene — they are the tenant boundary. DMARC's
+// default relaxed alignment compares organizational domains, so on a shared
+// registrable domain like oncarlos.com one tenant's DKIM signature would
+// authenticate another tenant's From address. Strict alignment is what makes
+// that impossible.
+//
+// p=none because a tenant whose first send is rejected by its own DMARC
+// record learns nothing useful; tightening it is a follow-up gated on
+// aggregate reports actually arriving.
+func dmarcValue(rua string) string {
+ v := "v=DMARC1; p=none; adkim=s; aspf=s"
+ if strings.TrimSpace(rua) != "" {
+ v += "; rua=mailto:" + strings.TrimSpace(rua)
+ }
+ return v
+}
diff --git a/internal/mail/dnsrecords_test.go b/internal/mail/dnsrecords_test.go
new file mode 100644
index 000000000..749a02770
--- /dev/null
+++ b/internal/mail/dnsrecords_test.go
@@ -0,0 +1,107 @@
+package mail
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/carlosframework/platform/internal/dnsrec"
+)
+
+// The MAIL FROM subdomain carries its region from the FIRST domain ever
+// verified. For a custom domain this record lives in the CUSTOMER's zone,
+// so it is the one thing we cannot republish for them — and the MX target
+// is region-specific, so an unqualified "mail." could never hold a second
+// region without ambiguity (spec §7, §8).
+func TestMailFromDomainCarriesTheRegion(t *testing.T) {
+ got := MailFromDomain("seapointish.bag.oncarlos.com", "eu-west-1")
+ want := "mail-eu-west-1.seapointish.bag.oncarlos.com"
+ if got != want {
+ t.Fatalf("MailFromDomain = %q, want %q", got, want)
+ }
+ if MailFromDomain("x.com", "eu-west-1") == MailFromDomain("x.com", "us-east-1") {
+ t.Fatal("two regions collided on one MAIL FROM subdomain")
+ }
+}
+
+// Nothing is EVER written at the app host itself. The edge owns an A record
+// there, and an email record sharing that name could fight it — this is a
+// permanent invariant, not an implementation detail (spec §7).
+func TestNoRecordSitsAtTheDomainApex(t *testing.T) {
+ const domain = "seapointish.bag.oncarlos.com"
+ for _, r := range RecordSet(domain, "eu-west-1", []string{"t1", "t2", "t3"}, "") {
+ if r.Name == domain {
+ t.Fatalf("record %s %s sits at the apex — it would fight the edge's A record", r.Type, r.Name)
+ }
+ }
+}
+
+func TestRecordSetGolden(t *testing.T) {
+ got := RecordSet("seapointish.bag.oncarlos.com", "eu-west-1",
+ []string{"tok1", "tok2", "tok3"}, "")
+ want := []dnsrec.Record{
+ {Name: "tok1._domainkey.seapointish.bag.oncarlos.com", Type: dnsrec.TypeCNAME,
+ Values: []string{"tok1.dkim.amazonses.com"}, TTL: 300},
+ {Name: "tok2._domainkey.seapointish.bag.oncarlos.com", Type: dnsrec.TypeCNAME,
+ Values: []string{"tok2.dkim.amazonses.com"}, TTL: 300},
+ {Name: "tok3._domainkey.seapointish.bag.oncarlos.com", Type: dnsrec.TypeCNAME,
+ Values: []string{"tok3.dkim.amazonses.com"}, TTL: 300},
+ {Name: "mail-eu-west-1.seapointish.bag.oncarlos.com", Type: dnsrec.TypeMX,
+ Values: []string{"10 feedback-smtp.eu-west-1.amazonses.com"}, TTL: 300},
+ {Name: "mail-eu-west-1.seapointish.bag.oncarlos.com", Type: dnsrec.TypeTXT,
+ Values: []string{"v=spf1 include:amazonses.com ~all"}, TTL: 300},
+ {Name: "_dmarc.seapointish.bag.oncarlos.com", Type: dnsrec.TypeTXT,
+ Values: []string{"v=DMARC1; p=none; adkim=s; aspf=s"}, TTL: 300},
+ }
+ if len(got) != len(want) {
+ t.Fatalf("got %d records, want %d:\n%+v", len(got), len(want), got)
+ }
+ for i := range want {
+ if got[i].Name != want[i].Name || got[i].Type != want[i].Type ||
+ got[i].TTL != want[i].TTL || len(got[i].Values) != len(want[i].Values) ||
+ got[i].Values[0] != want[i].Values[0] {
+ t.Fatalf("record %d:\n got %+v\nwant %+v", i, got[i], want[i])
+ }
+ }
+}
+
+// adkim=s is the TENANT BOUNDARY on a shared registrable domain, not
+// hygiene. DMARC's default relaxed alignment compares ORGANIZATIONAL
+// domains, so without it tenant bag's DKIM signature authenticates a From
+// address at bae.oncarlos.com (spec §7).
+func TestDMARCIsStrictlyAligned(t *testing.T) {
+ var dmarc string
+ for _, r := range RecordSet("x.bag.oncarlos.com", "eu-west-1", []string{"t"}, "") {
+ if r.Name == "_dmarc.x.bag.oncarlos.com" {
+ dmarc = r.Values[0]
+ }
+ }
+ if dmarc == "" {
+ t.Fatal("no _dmarc record emitted")
+ }
+ for _, want := range []string{"adkim=s", "aspf=s"} {
+ if !contains(dmarc, want) {
+ t.Fatalf("DMARC %q is missing %q — tenants are not isolated", dmarc, want)
+ }
+ }
+}
+
+// rua is CONFIGURATION. Unset means publish no rua at all: a reporting
+// address is only honoured by receivers when the destination authorises it
+// (RFC 7489 §7.1), so emitting one by default would publish a record that
+// looks right and yields nothing.
+func TestRUAIsOmittedWhenUnset(t *testing.T) {
+ for _, r := range RecordSet("x.com", "eu-west-1", []string{"t"}, "") {
+ if r.Name == "_dmarc.x.com" && contains(r.Values[0], "rua=") {
+ t.Fatalf("unset rua still emitted: %q", r.Values[0])
+ }
+ }
+ for _, r := range RecordSet("x.com", "eu-west-1", []string{"t"}, "reports@cribado.report") {
+ if r.Name == "_dmarc.x.com" {
+ if !contains(r.Values[0], "rua=mailto:reports@cribado.report") {
+ t.Fatalf("rua not rendered: %q", r.Values[0])
+ }
+ }
+ }
+}
+
+func contains(h, n string) bool { return len(n) > 0 && len(h) >= len(n) && strings.Contains(h, n) }