2026-03-17 21:34:11 +00:00
|
|
|
# Contents for cloudflare account
|
|
|
|
|
variable "cloudflare_api_key" {}
|
|
|
|
|
variable "cloudflare_email" {}
|
|
|
|
|
variable "cloudflare_proxied_names" { type = list(string) }
|
|
|
|
|
variable "cloudflare_non_proxied_names" { type = list(string) }
|
|
|
|
|
variable "cloudflare_zone_id" {
|
|
|
|
|
description = "Zone ID for your domain"
|
|
|
|
|
type = string
|
|
|
|
|
}
|
|
|
|
|
variable "cloudflare_account_id" {
|
|
|
|
|
type = string
|
|
|
|
|
sensitive = true
|
|
|
|
|
}
|
|
|
|
|
variable "cloudflare_tunnel_id" {
|
|
|
|
|
type = string
|
|
|
|
|
sensitive = true
|
|
|
|
|
}
|
|
|
|
|
variable "public_ip" {
|
|
|
|
|
type = string
|
|
|
|
|
}
|
2026-03-23 02:22:00 +02:00
|
|
|
variable "public_ipv6" {
|
|
|
|
|
type = string
|
|
|
|
|
description = "Public IPv6 address for AAAA records (from HE tunnel broker)"
|
|
|
|
|
}
|
2026-03-17 21:34:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
terraform {
|
|
|
|
|
required_providers {
|
|
|
|
|
cloudflare = {
|
|
|
|
|
source = "cloudflare/cloudflare"
|
|
|
|
|
version = "~> 4"
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
provider "cloudflare" {
|
|
|
|
|
api_key = var.cloudflare_api_key # I gave up on getting the permissions on the token...
|
|
|
|
|
email = var.cloudflare_email
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
locals {
|
|
|
|
|
cloudflare_proxied_names_map = {
|
|
|
|
|
for h in var.cloudflare_proxied_names :
|
|
|
|
|
h => h
|
|
|
|
|
}
|
|
|
|
|
cloudflare_non_proxied_names_map = {
|
|
|
|
|
for h in var.cloudflare_non_proxied_names :
|
|
|
|
|
h => h
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_zero_trust_tunnel_cloudflared_config" "sof" {
|
|
|
|
|
account_id = var.cloudflare_account_id
|
|
|
|
|
tunnel_id = var.cloudflare_tunnel_id
|
|
|
|
|
|
|
|
|
|
config {
|
|
|
|
|
warp_routing {
|
|
|
|
|
enabled = true
|
|
|
|
|
}
|
2026-04-16 13:45:04 +00:00
|
|
|
# Wildcard rule routes all subdomains through tunnel to Traefik.
|
|
|
|
|
# Traefik handles host-based routing via K8s Ingress resources.
|
|
|
|
|
ingress_rule {
|
|
|
|
|
hostname = "*.viktorbarzin.me"
|
|
|
|
|
service = "https://10.0.20.200:443"
|
|
|
|
|
origin_request {
|
|
|
|
|
no_tls_verify = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ingress_rule {
|
|
|
|
|
hostname = "viktorbarzin.me"
|
|
|
|
|
service = "https://10.0.20.200:443"
|
|
|
|
|
origin_request {
|
|
|
|
|
no_tls_verify = true
|
2026-03-17 21:34:11 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ingress_rule {
|
|
|
|
|
service = "http_status:404"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "dns_record" {
|
|
|
|
|
# count = length(var.cloudflare_proxied_names)
|
|
|
|
|
# name = var.cloudflare_proxied_names[count.index]
|
|
|
|
|
for_each = local.cloudflare_proxied_names_map
|
|
|
|
|
name = each.key
|
|
|
|
|
|
|
|
|
|
content = "${var.cloudflare_tunnel_id}.cfargotunnel.com"
|
|
|
|
|
proxied = true
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "CNAME"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "non_proxied_dns_record" {
|
|
|
|
|
# count = length(var.cloudflare_non_proxied_names)
|
|
|
|
|
# name = var.cloudflare_non_proxied_names[count.index]
|
|
|
|
|
for_each = local.cloudflare_non_proxied_names_map
|
|
|
|
|
name = each.key
|
|
|
|
|
|
|
|
|
|
# content = var.non_proxied_names[count.index].ip
|
|
|
|
|
content = var.public_ip
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "A"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-03-23 02:22:00 +02:00
|
|
|
resource "cloudflare_record" "non_proxied_dns_record_ipv6" {
|
|
|
|
|
for_each = local.cloudflare_non_proxied_names_map
|
|
|
|
|
name = each.key
|
|
|
|
|
content = var.public_ipv6
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "AAAA"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:24:38 +01:00
|
|
|
resource "cloudflare_record" "mail_mx" {
|
|
|
|
|
content = "mail.viktorbarzin.me"
|
2026-03-17 21:34:11 +00:00
|
|
|
name = "viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "MX"
|
2026-04-12 22:24:38 +01:00
|
|
|
priority = 1
|
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:24:38 +01:00
|
|
|
|
|
|
|
|
resource "cloudflare_record" "mail_domainkey" {
|
|
|
|
|
content = "\"v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDIDLB8mhAHNqs1s6GeZMQHOxWweoNKIrqo5tqRM3yFilgfPUX34aTIXNZg9xAmlK+2S/xXO1ymt127ZGMjnoFKOEP8/uZ54iHTCnioHaPZWMfJ7o6TYIXjr+9ShKfoJxZLv7lHJ2wKQK3yOw4lg4cvja5nxQ6fNoGRwo+mQ/mgJQIDAQAB\""
|
|
|
|
|
name = "s1._domainkey.viktorbarzin.me"
|
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
2026-04-12 22:24:38 +01:00
|
|
|
type = "TXT"
|
|
|
|
|
priority = 1
|
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:24:38 +01:00
|
|
|
resource "cloudflare_record" "mail_spf" {
|
[mailserver] Update SPF + docs for Brevo migration [ci skip]
## Context
Outbound mail relay migrated from Mailgun EU to Brevo EU on 2026-04-12 when
variables.tf:6 of the mailserver stack was switched to `smtp-relay.brevo.com:587`.
Postfix immediately began using Brevo for user mail — but the SPF TXT record
at viktorbarzin.me was left pointing at `include:mailgun.org -all`, so every
Brevo-relayed message failed SPF alignment and was spam-foldered or
DMARC-quarantined by Gmail/Outlook.
Observed on 2026-04-18 via `dig TXT viktorbarzin.me @1.1.1.1`:
"v=spf1 include:mailgun.org -all" <-- wrong sender network
User decision (2026-04-18): switch to `v=spf1 include:spf.brevo.com ~all`.
Soft-fail (`~all`) is intentional during cutover — keeps unauthorized Brevo
sends quarantined rather than outright rejected while we validate Brevo's
sending IPs + rate limits for real user mail. Tighten to `-all` once the
relay is proven stable.
The docs in `docs/architecture/mailserver.md` still described the old
Mailgun-based configuration (Overview paragraph, DNS table, Vault secrets
table). Per `infra/.claude/CLAUDE.md` rule "Update docs with every change",
those are updated in the same commit.
## This change
Coupled commit covering beads tasks code-q8p (SPF) + code-9pe (docs):
1. `stacks/cloudflared/modules/cloudflared/cloudflare.tf` — SPF TXT content
flipped from `include:mailgun.org -all` to `include:spf.brevo.com ~all`,
with an inline comment pointing at the mailserver docs for rationale.
2. `docs/architecture/mailserver.md` —
- Last-updated stamp moved to 2026-04-18 with the cutover note.
- Overview paragraph now says "relays through Brevo EU" (was Mailgun).
- DNS table SPF row reflects the new value plus an annotated history
note ("was include:mailgun.org -all until 2026-04-18").
- DMARC row now calls out the intended `dmarc@viktorbarzin.me` rua
target and flags that the current live record still points at
e21c0ff8@dmarc.mailgun.org, tracked under follow-up code-569.
- Vault secrets table: `mailserver_sasl_passwd` relabelled as Brevo
relay credentials; `mailgun_api_key` annotated as retained for the
E2E roundtrip probe only (inbound delivery testing, not user mail).
Apply was scoped with `-target=module.cloudflared.cloudflare_record.mail_spf`
to avoid sweeping up two unrelated pre-existing drifts that the Terraform
state shows on this stack: the DMARC + mail._domainkey_rspamd records are
stored on Cloudflare as RFC-compliant split TXT strings (>255 bytes), and
a naive refresh+apply would normalize them in the state back to single
strings. Those drifts are semantically equivalent (DNS concatenates
adjacent TXT strings at resolution time) and are out of scope for this
commit — they'll be handled under their own ticket.
## What is NOT in this change
- DMARC `rua=mailto:dmarc@viktorbarzin.me` cutover — that's code-569 (M1),
still using the legacy `e21c0ff8@dmarc.mailgun.org` + ondmarc addresses
in the live record.
- DMARC/DKIM TXT multi-string state reconciliation on `mail_dmarc` and
`mail_domainkey_rspamd` — pre-existing Cloudflare representation drift,
untouched here.
- Removal of Mailgun references in history/decision sections of the docs,
or the Mailgun-backed E2E roundtrip probe — probe still uses Mailgun API
on purpose for inbound delivery testing (code-569 scope).
- Mailgun DKIM record `s1._domainkey` — left in place; still consumed by
the roundtrip probe.
- Other pending items from the 2026-04-18 mail audit plan.
## Test Plan
### Automated
Targeted plan showed exactly one change, no other drift sneaking in:
module.cloudflared.cloudflare_record.mail_spf will be updated in-place
~ content = "\"v=spf1 include:mailgun.org -all\""
-> "\"v=spf1 include:spf.brevo.com ~all\""
Plan: 0 to add, 1 to change, 0 to destroy.
Apply result:
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
DNS propagation verified on three independent resolvers immediately after
apply:
$ dig TXT viktorbarzin.me @1.1.1.1 +short | grep spf
"v=spf1 include:spf.brevo.com ~all"
$ dig TXT viktorbarzin.me @8.8.8.8 +short | grep spf
"v=spf1 include:spf.brevo.com ~all"
$ dig TXT viktorbarzin.me @10.0.20.201 +short | grep spf # Technitium primary
"v=spf1 include:spf.brevo.com ~all"
### Manual Verification
Setup: nothing extra — change is already live (TF applied before commit
per home-lab convention; `[ci skip]` in title).
1. Confirm SPF is the Brevo-only record from an external resolver:
dig TXT viktorbarzin.me @1.1.1.1 +short
Expected: `"v=spf1 include:spf.brevo.com ~all"` — no Mailgun reference.
2. Send a test email via the mailserver (through Brevo relay) to a Gmail
account and view the original headers:
Authentication-Results: ... spf=pass smtp.mailfrom=viktorbarzin.me
...
Received-SPF: Pass (google.com: domain of ... designates ... as
permitted sender)
Expected: `spf=pass` (it was `spf=fail` or `spf=softfail` before this
change because the envelope sender IP was a Brevo IP not covered by
`include:mailgun.org`).
3. Confirm no live Mailgun references in the mailserver doc:
grep -n mailgun.org infra/docs/architecture/mailserver.md
Expected: only annotated-history mentions — SPF "was ... until
2026-04-18" and DMARC "current live record still points at
e21c0ff8@dmarc.mailgun.org pending cutover". No claims of active
Mailgun relay.
## Reproduce locally
cd infra
git pull
dig TXT viktorbarzin.me @1.1.1.1 +short | grep spf
# expected: "v=spf1 include:spf.brevo.com ~all"
# inspect the TF change:
git show HEAD -- stacks/cloudflared/modules/cloudflared/cloudflare.tf
# inspect the doc change:
git show HEAD -- docs/architecture/mailserver.md
Closes: code-q8p
Closes: code-9pe
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 23:13:47 +00:00
|
|
|
# Brevo replaced Mailgun as the outbound relay on 2026-04-12 (see docs/architecture/mailserver.md).
|
|
|
|
|
# Soft-fail (~all) is intentional during cutover — revisit once relay delivery is stable.
|
|
|
|
|
content = "\"v=spf1 include:spf.brevo.com ~all\""
|
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
|
|
|
name = "viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "TXT"
|
2026-04-12 22:24:38 +01:00
|
|
|
priority = 1
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "mail_domainkey_rspamd" {
|
|
|
|
|
content = "\"v=DKIM1; h=sha256; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9XHeFBKhUAEJSikXx+P49Q3nEBbnaSpn6h/9TqIhKaZWSVa2uGUGYQieNdon7DEJZ0VFo0Tvm3/UFsy2qF7ZmF+E/+N8EmkcPrMlxgJT281dpk5DxrZ+kbzw/DosfHH71K6vCLB4rSexzxJHaAx0AUddI3bFUJGjMgCXXCMZF+p8YCx+DDGPIXz2FOTtlJlR7aeZ2xXavwE/lBfI3MLnsq7X+GhPjQEax070nndOdZI0S8HpZkVxdGWl1N2Ec6LukYm2RiUkEMMQHSYX7WF3JBc+CGqUyd706Iy/5oeC3UGwZSM2uLkrp8YBjmw/h1rAeyv/ITt6ZXraP/cIMRiVQIDAQAB\""
|
|
|
|
|
name = "mail._domainkey.viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "TXT"
|
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:24:38 +01:00
|
|
|
resource "cloudflare_record" "brevo_domainkey1" {
|
|
|
|
|
content = "b1.viktorbarzin-me.dkim.brevo.com."
|
|
|
|
|
name = "brevo1._domainkey.viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "CNAME"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "brevo_domainkey2" {
|
|
|
|
|
content = "b2.viktorbarzin-me.dkim.brevo.com."
|
|
|
|
|
name = "brevo2._domainkey.viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "CNAME"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "brevo_code" {
|
|
|
|
|
content = "\"brevo-code:a6ef1dd91b248559900246eb4e7ceebd\""
|
truenas deprecation: migrate all non-immich storage to proxmox NFS
- Migrate 7 backup CronJobs to Proxmox host NFS (192.168.1.127)
(etcd, mysql, postgresql, nextcloud, redis, vaultwarden, plotting-book)
- Migrate headscale backup, ebook2audiobook, osm_routing to Proxmox NFS
- Migrate servarr (lidarr, readarr, soulseek) NFS refs to Proxmox
- Remove 79 orphaned TrueNAS NFS module declarations from 49 stacks
- Delete stacks/platform/modules/ (27 dead module copies, 65MB)
- Update nfs-truenas StorageClass to point to Proxmox (192.168.1.127)
- Remove iscsi DNS record from config.tfvars
- Fix woodpecker persistence config and alertmanager PV
Only Immich (8 PVCs, ~1.4TB) remains on TrueNAS.
2026-04-12 14:35:39 +01:00
|
|
|
name = "viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "TXT"
|
2026-03-17 21:34:11 +00:00
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:24:38 +01:00
|
|
|
resource "cloudflare_record" "mail_mta_sts" {
|
|
|
|
|
content = "\"v=STSv1; id=20260412\""
|
|
|
|
|
name = "_mta-sts.viktorbarzin.me"
|
2026-03-17 21:34:11 +00:00
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "TXT"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-12 22:24:38 +01:00
|
|
|
resource "cloudflare_record" "mail_tlsrpt" {
|
|
|
|
|
content = "\"v=TLSRPTv1; rua=mailto:postmaster@viktorbarzin.me\""
|
|
|
|
|
name = "_smtp._tls.viktorbarzin.me"
|
2026-03-17 21:34:11 +00:00
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "TXT"
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "mail_dmarc" {
|
[mailserver] Route DMARC rua/ruf to dmarc@viktorbarzin.me [ci skip]
## Context
Mailgun was decommissioned on 2026-04-12 in favour of Brevo as the outbound
SMTP relay. The DMARC aggregate (`rua`) and forensic (`ruf`) report targets
still pointed at `e21c0ff8@dmarc.mailgun.org`, an inbox that no longer
exists — meaning every DMARC report Google/Microsoft/etc. generate has
been bouncing or silently dropped for six days. No alerts fire on this
(DMARC reports are best-effort, not RFC-mandated), but we've lost visibility
into alignment failures and spoofing attempts during the exact window where
the SPF/DKIM/DMARC posture was being reshaped for the Brevo cutover.
Decision (2026-04-18): route reports to `mailto:dmarc@viktorbarzin.me`.
The mailserver's catch-all sieve delivers anything to non-existent
local-parts into `spam@`, so `dmarc@` does not need to be provisioned as
a real mailbox — the inbox will land in `spam@`'s maildir unchanged.
Alternative considered: route to a dedicated `dmarc@` maildir with sieve
rules to file into a folder. Rejected for now — the monitoring value of
DMARC reports is low-frequency (one aggregate per reporter per day at
most), so the catch-all path is good enough until volume justifies a
proper parser. Can be revisited once we see actual report traffic.
The third-party aggregator target `adb84997@inbox.ondmarc.com` (Red Sift
OnDMARC) is preserved in both rua and ruf — it provides parsed dashboards
that we actually read. The `postmaster@viktorbarzin.me` ruf-only target
also stays as a local mirror.
As a side effect, this apply also canonicalises the TXT record: the
previous value was stored as a two-string split in Cloudflare state
(`...viktorbarzin" ".me;"`) due to the 255-byte TXT string limit
(the record length exceeded 255 chars). The new value is shorter
(dmarc@viktorbarzin.me is 21 chars vs e21c0ff8@dmarc.mailgun.org's
26 chars, doubled across rua and ruf) and fits in a single string,
so the provider serialises it as one string and the prior split-drift
noise disappears from future plans.
## This change
Single-line content edit on `cloudflare_record.mail_dmarc` in
`stacks/cloudflared/modules/cloudflared/cloudflare.tf`:
Before → After (rua and ruf, both):
```
mailto:e21c0ff8@dmarc.mailgun.org → mailto:dmarc@viktorbarzin.me
```
All other DMARC tags unchanged: `v=DMARC1`, `p=quarantine`, `pct=100`,
`fo=1`, `ri=3600`, `sp=quarantine`, `adkim=r`, `aspf=r`.
Delivery flow:
```
DMARC reporter (Gmail/Outlook/...)
│ aggregate XML.gz to rua / forensic to ruf
▼
dmarc@viktorbarzin.me
│ mailserver catch-all (no local recipient)
▼
spam@viktorbarzin.me (Viki's mailbox)
```
## What is NOT in this change
- **Mailbox sieve rules** to file DMARC reports into a dedicated folder
(separate concern; deferred until traffic justifies it).
- **DMARC parser / dashboard**. OnDMARC (adb84997@inbox.ondmarc.com)
already provides this for aggregate reports.
- **Policy tightening** (`p=reject`, `pct` ramp) — out of scope.
- **SPF / DKIM records** — not touched.
- **Removal of the split-string drift suppression**, if any existed in
prior work. The canonicalisation happens naturally on this apply;
no separate workaround was needed.
## Test Plan
### Automated
Targeted terragrunt plan + apply via `scripts/tg`:
```
$ cd stacks/cloudflared && scripts/tg plan \
-target=module.cloudflared.cloudflare_record.mail_dmarc
...
Terraform will perform the following actions:
# module.cloudflared.cloudflare_record.mail_dmarc will be updated in-place
~ resource "cloudflare_record" "mail_dmarc" {
~ content = "\"v=DMARC1; ...
rua=mailto:e21c0ff8@dmarc.mailgun.org,
mailto:adb84997@inbox.ondmarc.com; ...
ruf=mailto:e21c0ff8@dmarc.mailgun.org,
mailto:adb84997@inbox.ondmarc.com,
mailto:postmaster@viktorbarzin\" \".me;\""
-> "\"v=DMARC1; ...
rua=mailto:dmarc@viktorbarzin.me,
mailto:adb84997@inbox.ondmarc.com; ...
ruf=mailto:dmarc@viktorbarzin.me,
mailto:adb84997@inbox.ondmarc.com,
mailto:postmaster@viktorbarzin.me;\""
}
Plan: 0 to add, 1 to change, 0 to destroy.
$ scripts/tg apply /tmp/dmarc.tfplan
module.cloudflared.cloudflare_record.mail_dmarc: Modifying...
module.cloudflared.cloudflare_record.mail_dmarc: Modifications complete after 1s
Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
```
Authoritative DNS post-apply:
```
$ dig TXT _dmarc.viktorbarzin.me @evan.ns.cloudflare.com +short
"v=DMARC1; p=quarantine; pct=100; fo=1; ri=3600; sp=quarantine; adkim=r; aspf=r; rua=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com; ruf=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com,mailto:postmaster@viktorbarzin.me;"
```
Note: `dig @1.1.1.1` still served the old value immediately after apply —
Cloudflare's public resolver holds its cache until TTL expires
(TTL=1/auto ≈ 5 min). Authoritative NS is the source of truth.
### Manual Verification
**Setup**: none (DNS change only).
**Commands**:
```
# 1. Confirm authoritative DNS (run now, should pass)
dig TXT _dmarc.viktorbarzin.me @evan.ns.cloudflare.com +short
# Expected: rua=mailto:dmarc@viktorbarzin.me,... and ruf similarly.
# 2. Confirm public resolver catches up (run after ~5min)
dig TXT _dmarc.viktorbarzin.me @1.1.1.1 +short
# Expected: same as above (no more mailgun.org entries).
# 3. Within 24-48h, check Viki's spam@ inbox for an incoming DMARC
# aggregate report from Google/Microsoft/etc. Reports are
# typically .zip or .gz attachments with XML inside.
```
**Interpretation**: seeing a DMARC report land in spam@ proves the
end-to-end delivery path works: reporter DNS lookup → _dmarc.viktorbarzin.me
→ mailto:dmarc@viktorbarzin.me → catch-all → spam@ maildir.
## Reproduce locally
```
1. git pull
2. cd stacks/cloudflared
3. dig TXT _dmarc.viktorbarzin.me @evan.ns.cloudflare.com +short
4. Expected: rua=mailto:dmarc@viktorbarzin.me (and ruf the same).
```
Closes: code-569
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 23:49:14 +00:00
|
|
|
content = "\"v=DMARC1; p=quarantine; pct=100; fo=1; ri=3600; sp=quarantine; adkim=r; aspf=r; rua=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com; ruf=mailto:dmarc@viktorbarzin.me,mailto:adb84997@inbox.ondmarc.com,mailto:postmaster@viktorbarzin.me;\""
|
2026-03-17 21:34:11 +00:00
|
|
|
name = "_dmarc.viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 1
|
|
|
|
|
type = "TXT"
|
|
|
|
|
priority = 1
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resource "cloudflare_record" "keyserver" {
|
|
|
|
|
content = "130.162.165.220" # Oracle VPS
|
|
|
|
|
name = "keyserver.viktorbarzin.me"
|
|
|
|
|
proxied = false
|
|
|
|
|
ttl = 3600
|
|
|
|
|
type = "A"
|
|
|
|
|
priority = 1
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Enable HTTP/3 (QUIC) for Cloudflare-proxied domains
|
|
|
|
|
resource "cloudflare_zone_settings_override" "http3" {
|
|
|
|
|
zone_id = var.cloudflare_zone_id
|
|
|
|
|
|
|
|
|
|
settings {
|
|
|
|
|
http3 = "on"
|
|
|
|
|
}
|
|
|
|
|
}
|