Configure Email Authentication DNS
Ce contenu n’est pas encore disponible dans votre langue.
Email authentication DNS records help receiving systems distinguish mail sent by your approved systems from mail that only claims to use your domain. This tutorial configures dedicated DKIM and DMARC record sets on an existing ZCP DNS zone. It also explains how to handle SPF safely.
Choose one owner for each record set: the zcp CLI or Terraform/OpenTofu. Do not use both tools to
manage the same name and type. The CLI accepts one content value per create and deletes records by
their complete name and type. Terraform models records as name-and-type record sets.
By the end, you will have:
- An inventory of the DNS zone and the systems permitted to send mail
- A published DKIM record or CNAME at a provider-issued selector
- A DMARC record in monitoring mode with reports sent to a mailbox you control
- A verification routine for DNS and authenticated mail from every sender
This guide assumes ZCP already hosts the authoritative zone. To create a zone and delegate it at your registrar, follow Host a Domain on ZCP (CLI).
Before You Start
Section titled “Before You Start”You need:
- A ZCP DNS zone for a domain you control
- The
zcpCLI authenticated with a token, or Terraform or OpenTofu withZCP_BEARER_TOKENset in your shell - Access to every system that sends mail using the domain, including application delivery, notifications, support, billing, marketing, and device or service alerts
- A mailbox dedicated to DMARC aggregate reports, such as
dmarc-reports@example.ca - The exact DKIM and SPF instructions from each sending system
The example.ca names in this guide are illustrative placeholders. Replace them with a domain you
control. Do not paste a private DKIM key into DNS. DNS publishes a public key or a provider-issued
CNAME target only.
Inventory the Zone and Senders
Section titled “Inventory the Zone and Senders”Start by recording the current state. Save the output in your change record. It gives you the zone slug, name servers, and the record sets you must preserve.
zcp dns listzcp dns show <domain-slug> --output jsonAsk both authoritative ZCP name servers for the records that matter. Replace example.ca with your
domain.
for ns in ns1.zsoftly.ca ns2.zsoftly.ca; do dig TXT example.ca "@$ns" +short dig TXT _dmarc.example.ca "@$ns" +short dig TXT selector._domainkey.example.ca "@$ns" +short dig CNAME selector._domainkey.example.ca "@$ns" +shortdoneList each legitimate sender, the domain it uses in the visible From address, the envelope sender domain, and its DKIM selector or CNAME instructions. Include systems that only send occasionally. Assign an owner who approves changes and reviews DMARC reports. Keep the inventory with the record values and the date each sender was last tested.
The current ZCP CLI creates or deletes an entire name-and-type record set. It has no in-place record
update command. The current Terraform provider also replaces a zcp_dns_record when its content,
name, type, or TTL changes. For an existing SPF record or another multi-value TXT record set, use
the established DNS change process that can preserve the full intended set. Do not create a second
root TXT resource or run a delete-and-recreate change during normal mail delivery.
Understand the Record Names
Section titled “Understand the Record Names”SPF, DKIM, and DMARC use different DNS names and answer different questions.
- SPF is a
TXTpolicy at the envelope-sender domain. When that domain is the zone apex, the record name is@. It can also be a subdomain, such asbounce.example.ca. Keep a single, complete policy and account for DNS lookups in all authorized includes. - DKIM is a
TXTrecord orCNAMEat<selector>._domainkey.example.ca. It lets receivers retrieve the public material needed to validate a message signature. - DMARC is a
TXTrecord at_dmarc.example.ca. It tells receivers what policy to apply when the visible From domain does not have aligned authenticated mail, and it requests reports.
The receiving system needs SPF or DKIM to authenticate and align with the visible From domain for a DMARC pass. DNS configuration alone cannot make that happen. Configure each sending system to use the correct domain, enable its DKIM signing, and send a test message after publication.
For a standards reference on SPF record selection and lookup limits, see RFC 7208.
Choose One Management Path
Section titled “Choose One Management Path”Use the CLI for a small, reviewed change to an unused dedicated record name. Use Terraform or OpenTofu when your team already manages this zone as code and reviews plans. The examples below create only the dedicated DKIM and DMARC record sets after you confirm that each name is absent.
Do not apply the CLI commands if Terraform or OpenTofu owns the same record set. Do not apply the Terraform configuration if someone changes the same record set through the CLI or console. Record ownership alongside your sender inventory.
If the zone apex is the envelope-sender domain and it has no TXT record set at all, use one
ownership path to create a single SPF policy with the complete value supplied by all authorized
senders. Do this only after you have checked every sender and the value’s DNS lookups. An active
sending domain with an existing root TXT set needs a separate reviewed change because this
tutorial cannot safely merge or update that shared set.
Publish DKIM and DMARC with the CLI
Section titled “Publish DKIM and DMARC with the CLI”Set variables for your zone and the instructions supplied by a sending system. The DKIM value below is read from your terminal to avoid putting it in shell history. Enter the complete DNS presentation: a quoted string for a short TXT value, or the provider’s complete quoted, chunked presentation for a long key. Enter the exact CNAME target when the sender asks for a CNAME.
export DOMAIN_SLUG="exampleca"export DOMAIN_NAME="example.ca"export DKIM_SELECTOR="mailer1"read -r DKIM_CONTENTConfirm that the selector name and _dmarc do not already have a record set. Query the actual
authoritative name servers from zcp dns show <domain-slug>. Require a successful authoritative
answer from both. A timeout, SERVFAIL, or REFUSED response is not permission to create a record.
for ns in ns1.zsoftly.ca ns2.zsoftly.ca; do dig TXT "$DKIM_SELECTOR._domainkey.$DOMAIN_NAME" "@$ns" +norecurse +noall +comments +answer dig CNAME "$DKIM_SELECTOR._domainkey.$DOMAIN_NAME" "@$ns" +norecurse +noall +comments +answer dig TXT "_dmarc.$DOMAIN_NAME" "@$ns" +norecurse +noall +comments +answerdoneReplace the illustrative ns1.zsoftly.ca and ns2.zsoftly.ca values with the authoritative servers
listed for your zone. If any answer section contains a record, stop and review its existing owner.
Create the DKIM TXT record only when the sender issued a TXT value. ZCP appends the zone to the
relative name, so use mailer1._domainkey, not the complete domain name.
zcp dns record-create \ --domain "$DOMAIN_SLUG" \ --name "$DKIM_SELECTOR._domainkey" \ --type TXT \ --content "$DKIM_CONTENT" \ --ttl 3600If the sending system issued a CNAME instead, create a CNAME with its exact target and do not also create the TXT record.
zcp dns record-create \ --domain "$DOMAIN_SLUG" \ --name "$DKIM_SELECTOR._domainkey" \ --type CNAME \ --content "<provider-issued-target>." \ --ttl 3600Create DMARC in monitoring mode first. The reporting mailbox must exist and its owner must review the reports.
zcp dns record-create \ --domain "$DOMAIN_SLUG" \ --name _dmarc \ --type TXT \ --content '"v=DMARC1; p=none; rua=mailto:dmarc-reports@example.ca"' \ --ttl 3600If, and only if, authoritative queries confirm there is no root TXT record set, you can publish
the one complete SPF policy that all authorized senders provide. Enter its complete DNS presentation
when prompted. Do not use v=spf1 -all for an active sending domain unless no system is authorized
to send with it.
dig TXT "$DOMAIN_NAME" @ns1.zsoftly.ca +norecurse +noall +comments +answerdig TXT "$DOMAIN_NAME" @ns2.zsoftly.ca +norecurse +noall +comments +answerread -r SPF_CONTENT
zcp dns record-create \ --domain "$DOMAIN_SLUG" \ --name @ \ --type TXT \ --content "$SPF_CONTENT" \ --ttl 3600TXT data must reach DNS as quoted character strings. A sender’s setup page can display an unquoted
value for copy and paste. In that case, obtain its DNS presentation before you create the record. A
single string needs surrounding quotes. A long value can need quoted chunks of no more than 255
bytes, which resolvers concatenate as one TXT value. Keep provider-issued chunks intact. Do not
split a public key into separate DNS records, and do not invent a key. Verify the published answer
against the sender’s instructions before enabling signing.
Publish DKIM and DMARC with Terraform or OpenTofu
Section titled “Publish DKIM and DMARC with Terraform or OpenTofu”Use this path only when infrastructure as code owns the dedicated records. Set the token in your
shell. Do not put it in main.tf or commit it.
export ZCP_BEARER_TOKEN="<your-token>"Create main.tf. This configuration refers to an existing zone by its slug. It deliberately does
not declare the zone itself, so an apply cannot replace it.
terraform { required_providers { zcp = { source = "zsoftly/zcp" version = "~> 0.2.0" } }}
variable "domain_slug" { type = string}
variable "dkim_selector" { type = string}
variable "dkim_txt_content" { type = string}
provider "zcp" {}
resource "zcp_dns_record" "dkim" { domain = var.domain_slug name = "${var.dkim_selector}._domainkey" type = "TXT" content = var.dkim_txt_content ttl = 3600
lifecycle { prevent_destroy = true }}
resource "zcp_dns_record" "dmarc" { domain = var.domain_slug name = "_dmarc" type = "TXT" content = "\"v=DMARC1; p=none; rua=mailto:dmarc-reports@example.ca\"" ttl = 3600
lifecycle { prevent_destroy = true }}If the root TXT record set is absent, you can add this third resource after you have validated the
complete policy from every authorized sender. Do not add it to a configuration for a root TXT set
that already exists.
variable "spf_txt_content" { type = string}
resource "zcp_dns_record" "spf" { domain = var.domain_slug name = "@" type = "TXT" content = var.spf_txt_content ttl = 3600
lifecycle { prevent_destroy = true }}If the sender issued a CNAME selector, replace both the dkim_txt_content variable and the
zcp_dns_record.dkim resource above with the following variable and resource. Do not declare both
DKIM resources at the same selector name.
variable "dkim_cname_target" { type = string}
resource "zcp_dns_record" "dkim_cname" { domain = var.domain_slug name = "${var.dkim_selector}._domainkey" type = "CNAME" content = var.dkim_cname_target ttl = 3600
lifecycle { prevent_destroy = true }}Set dkim_cname_target to the complete provider-issued hostname with a trailing dot, for example
selector.provider.example.ca.. A target without the final dot can be treated as relative to your
zone.
Store the variable values with your DNS configuration. A DKIM TXT value contains public key material. A DKIM CNAME value contains a provider-issued DNS target. Keep the bearer token in the environment and out of source control.
terraform initterraform plan -var='domain_slug=exampleca' -var='dkim_selector=mailer1'terraform apply -var='domain_slug=exampleca' -var='dkim_selector=mailer1'OpenTofu uses the same configuration and subcommands with tofu in place of terraform. Read the
plan before approval. A zcp_dns_record represents the full record set identified by domain,
name, and type. Its content is write-only, so the provider does not detect out-of-band content
changes. A change in the configuration forces replacement. The prevent_destroy guard blocks a
planned destroy while its resource block remains in the configuration. Removing the resource block
also removes the guard, so it is not a rollback method. Read every plan. Remove the guard only for
an approved maintenance change after you have reviewed the replacement and rollback plan.
To bring an existing dedicated DKIM TXT record set under Terraform, configure the
zcp_dns_record.dkim resource above to match its current values, then import it:
terraform import zcp_dns_record.dkim exampleca/TXT/mailer1._domainkeyThe record import format is <domain-slug>/<type>/<relative-name>. The provider cannot read record
content back in a comparable form, so import seeds identity only. Run terraform plan after import
and do not approve a replacement of a live record set until you have confirmed the full planned
value and rollback plan.
Verify DNS and Delivered Mail
Section titled “Verify DNS and Delivered Mail”First query both authoritative servers. Get their names from zcp dns show <domain-slug> for your
zone. The names below are illustrative. Then query a public recursive resolver after delegation and
caches have refreshed.
for ns in ns1.zsoftly.ca ns2.zsoftly.ca; do dig TXT mailer1._domainkey.example.ca "@$ns" +short dig TXT _dmarc.example.ca "@$ns" +shortdone
dig TXT mailer1._domainkey.example.ca @1.1.1.1 +shortdig TXT _dmarc.example.ca @1.1.1.1 +shortFor a CNAME-based selector, replace the first TXT query with CNAME. Compare the answer with the
exact instructions from the sending system. A successful DNS lookup does not prove that the sender
is signing messages, so this is only the first check.
Send a message from every legitimate source to a test mailbox you control. Review the received
message headers from a trusted receiver. Its Authentication-Results header should show the
authentication results for the message. Confirm the visible From domain aligns with a passing SPF or
DKIM result. Test replies, application notices, forwarded mail, and any sending path that uses a
different envelope sender or selector.
Move DMARC from Monitoring to Enforcement
Section titled “Move DMARC from Monitoring to Enforcement”Start with p=none and collect reports. Reconcile each report source against your sender inventory.
Fix missing DKIM signing, missing SPF authorization, or domain alignment before changing policy.
When the reports and delivery tests show that legitimate traffic authenticates correctly, move to
p=quarantine. Continue review during the change. Move to p=reject only when the remaining
failures are understood and accepted. Signed messages can be forwarded or changed by intermediary
systems, so include those paths in your review before tightening the policy.
Use the same management path that owns _dmarc to make each change. With the CLI, record the prior
record content before the approved maintenance change, then restore that exact content if delivery
testing fails. With Terraform or OpenTofu, keep the prior reviewed configuration and use a reviewed
plan to return to it. Do not run a broad destroy for DNS changes.
Keep DNS in an Email Security Program
Section titled “Keep DNS in an Email Security Program”Review DMARC reports, sender inventory, DNS changes, and test evidence on a regular schedule. Give new systems a sender review before they use your domain. Remove retired senders from SPF and retire unused DKIM selectors and signing configuration after mail queues and verification needs are clear.
DNS does not replace controls outside DNS:
- Require multi-factor authentication for administrator and DNS access.
- Apply least privilege and review recovery access, forwarding, delegation, and offboarding.
- Tune inbound filtering and impersonation controls, then review quarantine results and narrow exceptions.
- Review external group membership and data-sharing settings.
- Treat message encryption certificates and AI-connected mail features as separate decisions about recipients, key recovery, data access, and cost.
For the policy and ownership decisions behind this technical work, read Email Security Needs an Owner.
Next Steps
Section titled “Next Steps”- Host a Domain on ZCP (CLI) for zone creation and delegation.
- Manage DNS with the CLI for supported DNS commands.
- TXT Records for quoting and direct lookup examples.
- DNS Troubleshooting for authoritative and recursive DNS checks.
