Skip to content

DMARC Enforcement Plan — ciriusgroup.com and ciriusdde.com

Story: MAESTER-010 Owner: Rory (architect) / Kobe (DNS automation) Status: p=reject already published on both apex zones (2026-04-16 verification). Remaining work: validate aggregate reports show zero legitimate failures, document rollback, formalize the staged rollout playbook for future domain additions.


TL;DR

Maester flags domains that do not publish DMARC p=reject. Public DNS lookups on 2026-04-16 show:

DomainDMARCPolicyspStrict alignment
ciriusgroup.compublishedp=rejectsp=reject(not set — implicit relaxed)
ciriusdde.compublishedp=reject(not set → inherits policy)adkim=s; aspf=s
ciriussupport.comzone record exists but not resolvable publicly — see ciriussupport-email-security.md

The Maester finding for MAESTER-010 is the authoritative view at audit time; we have since confirmed that the records are in place. This document captures:

  1. How to verify current state (for re-audit).
  2. The staged rollout playbook — used retroactively to document how we got here and prescriptively for any future domain we send mail from.
  3. Rollback procedure if ACS, Microsoft 365, Zendesk, or a new sender starts failing.
  4. Where DNS lives (it is not in azure-infra Terraform — zones are hosted at generic.host).

1. Current State Assessment

1.1 Authoritative nameservers

Both apex zones are delegated to generic.host:

$ dig +short NS ciriusgroup.com @8.8.8.8
ns1.vps-ciriusgroup.generic.host.
ns2.vps-ciriusgroup.generic.host.

$ dig +short NS ciriusdde.com @8.8.8.8
ns1.vps-ciriusgroup.generic.host.
ns2.vps-ciriusgroup.generic.host.

Zone data is edited in the generic.host VPS control panel. There are no azurerm_dns_zone resources for these apex zones in azure-infra. The only DNS resources in Terraform are azurerm_private_dns_zone entries (privatelink zones used for private endpoints — unrelated to public email).

Implication: DMARC / SPF / DKIM changes are manual DNS edits at generic.host. They do not go through the azure-infra PR → Checkov → apply pipeline. That is a known gap; migrating these zones to Azure DNS or Cloudflare would bring email DNS under change control, but is out of scope for MAESTER-010.

1.2 Published DMARC records

$ dig +short TXT _dmarc.ciriusgroup.com @8.8.8.8
"v=DMARC1;p=reject; sp=reject; rua=mailto:dmarc@ciriusgroup.com; ruf=mailto:dmarc@ciriusgroup.com"

$ dig +short TXT _dmarc.ciriusdde.com @8.8.8.8
"v=DMARC1; p=reject; rua=mailto:dmarc-reports@ciriusdde.com; ruf=mailto:dmarc-reports@ciriusdde.com; fo=1; adkim=s; aspf=s"

Both records satisfy Maester's p=reject check. Two differences worth normalizing in a follow-up pass:

  • ciriusgroup.com does not set adkim / aspf. Default is relaxed — acceptable but less strict than DDE. If we have zero alignment failures in 30 days of aggregate reports, tighten to adkim=s; aspf=s to match DDE.
  • ciriusdde.com sets fo=1 (report any failure). PROD does not. fo=1 is optional and produces more ruf traffic; keep as-is.

1.3 Published SPF records

$ dig +short TXT ciriusgroup.com @8.8.8.8 | grep spf1
"v=spf1 include:spf.protection.outlook.com include:_spf.generic.host include:mail.zendesk.com include:spf.azurecomm.net -all"

$ dig +short TXT ciriusdde.com @8.8.8.8 | grep spf1
"v=spf1 include:spf.protection.outlook.com -all"

Both end with -all (hardfail) which is the correct stance alongside p=reject.

1.4 Published DKIM selectors

$ dig +short CNAME selector1._domainkey.ciriusgroup.com @8.8.8.8
selector1-ciriusgroup-com._domainkey.CiriusGroupinc.onmicrosoft.com.

$ dig +short CNAME selector2._domainkey.ciriusgroup.com @8.8.8.8
selector2-ciriusgroup-com._domainkey.CiriusGroupinc.onmicrosoft.com.

$ dig +short CNAME selector1._domainkey.ciriusdde.com @8.8.8.8
selector1-ciriusdde-com._domainkey.CIRIUSDDE.r-v1.dkim.mail.microsoft.

$ dig +short CNAME selector2._domainkey.ciriusdde.com @8.8.8.8
selector2-ciriusdde-com._domainkey.CIRIUSDDE.r-v1.dkim.mail.microsoft.

$ dig +short CNAME selector1-azurecomm-prod-net._domainkey.ciriusgroup.com @8.8.8.8
selector1-azurecomm-prod-net._domainkey.azurecomm.net.

Microsoft 365 DKIM (selector1/selector2) is in place on both tenants. ACS DKIM is in place on ciriusgroup.com. Zendesk signs with its own headers — alignment works via SPF only (include:mail.zendesk.com in the SPF record).

1.5 Known mail flows

SenderDomainAuth pathNotes
Microsoft 365 (user mail)ciriusgroup.com, ciriusdde.comSPF include:spf.protection.outlook.com + DKIM (selector1/selector2)Primary user mail. DKIM enabled in Exchange Online Protection.
Azure Communication Services — services@, servicesinternal@, security@ciriusgroup.comSPF include:spf.azurecomm.net + ACS DKIMTransactional relay (see azure-infra/logging/acs_email.tf).
Zendesk (support tickets)ciriusgroup.comSPF include:mail.zendesk.com; Zendesk-signed DKIM (their domain)Alignment via SPF.
generic.host webmail forwarderciriusgroup.comSPF include:_spf.generic.hostLegacy fallback from the zone host. Candidate for removal once confirmed unused.
Twingate notificationsSaaS-side — from twingate.com, not our domainsDoes not need our SPF/DKIM.
Sentinel alert webhooksPosts to Teams/Telegram, not emailN/A for DMARC.

1.6 Maester authoritative check

To reproduce Maester's DMARC evaluation locally:

az rest --method GET \
  --uri "https://graph.microsoft.com/v1.0/domains" \
  --query "value[].{id:id, auth:authenticationType}"

# Or run Maester directly:
pwsh -c "Install-Module Maester -Scope CurrentUser -Force; Connect-Maester; Invoke-Maester -Include Network"

The MT.1036 test is the DMARC policy check; it passes only when p=reject is published on every accepted domain.


2. Staged Rollout Playbook (Prescriptive)

For new domains added to the tenant, or if a DMARC failure forces us to back a domain down to p=none and roll it forward again, follow this five-phase sequence.

Phase 0 — Prerequisites

Before publishing any DMARC record, confirm:

  • [ ] Every legitimate mail flow for the domain is identified (see the mail-flow table pattern in §1.5).
  • [ ] SPF record lists every legitimate sender with -all at the end.
  • [ ] DKIM is enabled and signing for every legitimate sender. For M365, enable at security.microsoft.com → Email & collaboration → Policies → DKIM.
  • [ ] An address for rua= aggregate reports exists and forwards to a mailbox that is reviewed (or to a DMARC analyzer like dmarcian, URIports, Valimail).
  • [ ] An MX record for the rua= recipient's domain accepts mail from -all senders (otherwise report forwarders drop).

Phase 1 — p=none (monitoring, 14 days minimum)

Publish:

Name:  _dmarc.<domain>
Type:  TXT
Value: v=DMARC1; p=none; rua=mailto:dmarc@ciriusgroup.com; ruf=mailto:dmarc@ciriusgroup.com; fo=1
TTL:   3600

Watch aggregate reports daily. Any dkim=fail or spf=fail from a known-good sender = SPF/DKIM fix needed before advancing.

Advance criterion: 14 consecutive days with zero legitimate-sender failures in aggregate reports.

Phase 2 — p=quarantine; pct=25 (7 days)

Quarantine 25% of failing mail. This is the canary.

v=DMARC1; p=quarantine; pct=25; rua=mailto:dmarc@ciriusgroup.com; ruf=mailto:dmarc@ciriusgroup.com; fo=1

Advance criterion: zero user-reported mail loss tickets for 7 days.

Phase 3 — p=quarantine; pct=100 (30 days)

Quarantine all failing mail. Long bake window catches monthly/quarterly senders (invoice systems, annual notices, vendor relays that fire infrequently).

v=DMARC1; p=quarantine; rua=mailto:dmarc@ciriusgroup.com; ruf=mailto:dmarc@ciriusgroup.com; fo=1

Advance criterion: 30 days with zero legitimate failures in aggregate reports. Document any new senders discovered and add them to SPF/DKIM before advancing.

Phase 4 — p=reject (target state)

v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:dmarc@ciriusgroup.com; ruf=mailto:dmarc@ciriusgroup.com; fo=1

Rationale for each tag:

  • p=reject — main policy.
  • sp=reject — subdomain policy; required because attackers frequently spoof accounts.<domain>, billing.<domain>, etc.
  • adkim=s — strict DKIM alignment (d= must exactly match From).
  • aspf=s — strict SPF alignment (MAIL FROM must exactly match From).
  • fo=1 — report any failure (SPF or DKIM), not just both failing.

Phase 5 — Steady state (monitoring)

  • Review aggregate reports weekly for two months after reject.
  • After two clean months, reduce review cadence to monthly.
  • If a new sender is added, go back to Phase 0 checklist for that sender before it sends its first email.

3. Rollback Procedure

Trigger: legitimate mail is being rejected and cannot be fixed in <2 hours.

  1. Edit _dmarc.<domain> TXT at generic.host control panel.
  2. Change p=rejectp=none (monitor only — fails open, no blocked mail). Do not change to p=quarantine during an incident; it still quarantines and users must retrieve from junk.
  3. TTL is 3600 — plan for up to 1 hour of propagation.
  4. File a CM ticket in SecOps (POST /api/changes) so SecurityAgent suppresses DMARC-related alerts during the rollback window.
  5. Root-cause: pull the failing message's headers, identify the sender IP, confirm whether SPF include: or DKIM selector is missing.
  6. Fix SPF/DKIM. Re-run Phase 1 → Phase 4 for that sender (not the whole domain — the domain is already known good except for the missing sender).

4. Terraform / azure-infra Changes

None required for MAESTER-010. The apex DNS zones for ciriusgroup.com and ciriusdde.com are not managed in Terraform — they live at generic.host and are edited manually in the VPS control panel. A comment to this effect is added in logging/acs_email.tf (existing file already contains the ACS DNS manual-step instructions).

If/when these zones migrate into Azure DNS or Cloudflare, add a _dmarc TXT resource of the form:

hcl
# Example for future migration to Azure DNS
resource "azurerm_dns_txt_record" "dmarc_ciriusgroup" {
  name                = "_dmarc"
  zone_name           = azurerm_dns_zone.ciriusgroup.name
  resource_group_name = azurerm_resource_group.logging.name
  ttl                 = 3600

  record {
    value = "v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:dmarc@ciriusgroup.com; ruf=mailto:dmarc@ciriusgroup.com; fo=1"
  }

  tags = local.required_tags
}

Do not add this resource now — creating azurerm_dns_zone.ciriusgroup would hijack the delegation and silently black-hole mail. Migration is a dedicated project (new story in the DMARC epic backlog).


5. Follow-ups

  • [ ] Normalize ciriusgroup.com DMARC to include adkim=s; aspf=s after 30 days of clean aggregate reports (currently implicit relaxed).
  • [ ] Confirm whether _spf.generic.host is still sending for ciriusgroup.com; if not, remove from SPF to shrink the include chain (10-lookup limit).
  • [ ] Stand up a DMARC aggregate report parser (dmarcian free tier, URIports, or a self-hosted parsedmarc container job) so rua@ciriusgroup.com reports are visualized rather than reviewed as raw XML.
  • [ ] Evaluate migrating the two apex zones to Azure DNS so DMARC/SPF/DKIM become Terraform-managed and CI-reviewed.

References

  • MAESTER-010 story (SecOps) — audit origin and acceptance criteria.
  • RFC 7489 — DMARC specification.
  • RFC 7208 — SPF specification (10-lookup limit, -all vs ~all).
  • Microsoft "Use DMARC to validate email" — https://learn.microsoft.com/en-us/defender-office-365/email-authentication-dmarc-configure
  • azure-infra/logging/acs_email.tf — ACS Email setup with DNS verification instructions.
  • bedrock-docs/security/ciriussupport-email-security.md — sibling story, EMAILSEC-013.

Internal use only — Cirius Group