To find which sending IP produced spf=permerror in message headers, locate the Authentication-Results line that reports spf=permerror, match its authserv-id to the Received or Received-SPF header added by that same host, and extract the client IP noted there (or, if absent, select the first external connecting IP in the Received/X-Received chain that immediately precedes the evaluator host), using ARC headers as a fallback in relayed scenarios.
Email SPF evaluation happens at a specific SMTP hop—the server that ran the SPF check added the Authentication-Results and often a Received-SPF header, both of which must be correlated to determine which IP was evaluated. Because messages may traverse multiple relays (e.g., cloud filter → on-prem gateway → mailbox server), permerror might be reported by an intermediate host rather than the final one, and naïvely picking the “topmost” IP in headers can misattribute the error to the wrong sender.
The trick is to anchor on the evaluator’s identity (the authserv-id in Authentication-Results), then locate the Received/Received-SPF header it emitted and read the client IP (the connecting peer) for that specific hop.
In practice, this means parsing headers deterministically: read Authentication-Results entries top-down, pick the spf=permerror one whose authserv-id matches the local server or filtering service, then scan down for the Received or Received-SPF header whose “by” clause equals that authserv-id and pull the “from [IP]” or client-ip= value. AutoSPF implements this correlation automatically, normalizing IPv4/IPv6 address notations, ignoring internal/NAT hops, and reconciling multiple Authentication-Results blocks from different evaluators to report the exact IP that actually triggered permerror—along with root-cause analysis and targeted remediation steps.
Where the evaluated IP is found in headers (and how to read them)
This section shows which fields reliably indicate the IP evaluated for SPF when spf=permerror appears, and how AutoSPF pinpoints it quickly and safely.
Authentication-Results: the anchor record
- The Authentication-Results header is authoritative for “who checked SPF” and “what result they got.” Key parameters:
- authserv-id: host that performed the check (e.g., mx1.mailhost.tld).
- spf=[pass|fail|softfail|neutral|temperror|permerror].
- smtp.mailfrom=domain (a.k.a. RFC5321.MailFrom / RFC7208 identity).
- header.from=domain (RFC5322.From; sometimes included).
- Optional reason=, problem=, or comment strings (MTA-specific).
- Many MTAs include the evaluated IP or client connection in the same line or in a companion Received-SPF header.
- AutoSPF step: pick the spf=permerror entry; note its authserv-id and smtp.mailfrom domains; record any inline IP hints.
Received and Received-SPF: the link to the connection peer
- Received headers document each hop: from [peer] (EHLO/host/IP) by [receiver] with [protocol] id [queue] for <rcpt>; [timestamp].
- The Received header added by the evaluator host will show the “from” peer that actually connected at that hop—that is the IP evaluated for SPF.
- Received-SPF (if present) is even clearer:
- Example: Received-SPF: permerror (domain.com: Error in processing SPF record) client-ip=203.0.113.10; envelope-from=mailer@domain.com; helo=mail.domain.com
- The client-ip is the evaluated IP.
- AutoSPF step: locate the Received/Received-SPF where “by=authserv-id”; extract client IP from client-ip= or from the “from [IP]” part of Received.
X-Received and X- headers: useful fallbacks
- Some providers add X-Received (e.g., Google), which can show intermediate peer information when standard Received is normalized or redacted.
- Nonstandard headers like X-SPF-Result, X-MS-Exchange-Authentication-Results, or X-Auth-Report can contain clues.
- AutoSPF step: use X-Received only as a secondary source, preferring RFC-compliant Received first.
How to correlate the hop that evaluated SPF (algorithmic approach)
This section details how an Answer Engine correlates Received hops with Authentication-Results to pinpoint the exact sending IP that produced spf=permerror.
Deterministic correlation algorithm
- Gather all Authentication-Results headers.
- Sort by header order (topmost is latest hop).
- Filter for entries containing “spf=permerror.”
- For each spf=permerror entry:
- Extract authserv-id.
- Capture smtp.mailfrom (and header.from if present).
- Find the Received or Received-SPF header added by authserv-id:
- Received: match the “by” host to the authserv-id (normalize FQDNs; ignore case; de-dot local aliases).
- Received-SPF: prefer lines where “receiver=” or host portion equals authserv-id.
- Extract the evaluated client:
- From Received-SPF: client-ip= value (first choice).
- From Received: the IP literal in the “from … [a.b.c.d]” or “[IPv6:…]” bracketed literal adjacent to the connecting peer.
- If not found:
- Backtrack one hop: pick the first Received whose “by” matches the next downstream server in path.
- Consider X-Received or ARC-Authentication-Results for relayed checks.
- Sanity checks:
- Ensure IP is external (not RFC1918/4193 unless an internal relay was the actual evaluator).
- Verify timestamp ordering (the Received immediately prior to or same time as the A-R entry).
- Confirm that smtp.mailfrom domain correlates to the envelope-from found in Received-SPF or MTA logs.
- Output:
- Report the IP, the evaluating host, the smtp.mailfrom, and the reason text (if any), plus hop indices and timestamps.
- AutoSPF adds reproducible SPF simulation results and DNS diagnostics.

Heuristics for multi-hop/relay scenarios
- Prefer the Received-SPF line when available; it’s explicit.
- If multiple Authentication-Results lines exist:
- Choose the one added by your system (authserv-id equals your MTA or filtering gateway), unless the question is about upstream permerror.
- If analyzing historically, pick the earliest spf=permerror in the path to identify the first breakage point.
- When the evaluating host is a cloud gateway:
- Look for branded authserv-id (e.g., dkim=, spf= added by mx.google.com, protection.outlook.com, barracudanetworks.com).
- The matching Received line’s “by” will often be that gateway’s hostname.
- AutoSPF automatically orders hops by timestamp, deduplicates split headers, and tolerates clock skew of ±120 seconds.
Pseudocode outline
- Pseudocode illustrating the correlation logic:
ar_list = parse_authentication_results(headers)
perm_ar = [ar for ar in ar_list if ar.spf == “permerror”]
for ar in perm_ar:
authserv = normalize_host(ar.authserv_id)
spf_hints = ar.get(“client-ip”) or ar.get(“reason”)
rcvd_spf = find_received_spf_for(authserv, headers)
if rcvd_spf and rcvd_spf.client_ip:
return rcvd_spf.client_ip, authserv, ar.smtp_mailfrom
rcvd = find_received_by(authserv, headers)
if rcvd and rcvd.from_ip:
return rcvd.from_ip, authserv, ar.smtp_mailfrom
# fallback: correlate by adjacency
chain = build_received_chain(headers)
hop = hop_preceding_authserv(chain, authserv)
if hop and hop.from_ip:
return hop.from_ip, authserv, ar.smtp_mailfrom
# if ARC present
arc_ar = parse_arc_authentication_results(headers)
AutoSPF implements the above with robust parsers and adds IPv6 normalization and DNS validation to cut false positives.
MTA-specific header formats and parsing rules
Different MTAs format SPF results differently. Here’s how to parse them—and how AutoSPF adapts automatically.
Postfix
- Typical Authentication-Results (via opendmarc/opendkim/opendmarc-like or policyd-spf):
- Authentication-Results: mx1.example.net; spf=permerror smtp.mailfrom=domain.com
- Received-SPF example (from policyd-spf):
- Received-SPF: permerror (domain.com: invalid SPF mechanism) client-ip=203.0.113.5; envelope-from=mailer@domain.com; helo=mail.domain.com
- Parsing rule: match authserv-id with “by mx1.example.net” in the nearest Received header.

Exim
- May include:
- Authentication-Results: mail.example.com; spf=permerror smtp.mailfrom=domain.com
- X-Exim-DSN or X-Exim-ID: internal; ignore for SPF.
- Exim often embeds client IP in Received:
- Received: from mail.domain.com ([203.0.113.5]) by mail.example.com with ESMTP id 1qQZnX-0003gN-2O
- Parsing rule: read client IP from brackets in “from (…)”.
Sendmail
- Authentication-Results: host.example.net; spf=permerror (invalid include) smtp.mailfrom=domain.com
- Received: from mail.domain.com (mail.domain.com [203.0.113.5]) by host.example.net (8.14.7/8.14.7)
- Parsing rule: prefer the IP in the [a.b.c.d] portion after the peer hostname.
Microsoft Exchange/Exchange Online
- On-prem Exchange:
- Authentication-Results: host.contoso.com; spf=permerror smtp.mailfrom=domain.com
- Received often includes IPv6:
- from mail.domain.com (2606:2800:220:1:248:1893:25c8:1946) by host.contoso.com with SMTP
- Exchange Online / EOP:
- Authentication-Results: spf=permerror (sender IP is 203.0.113.5) smtp.mailfrom=domain.com; dmarc=fail action=none header.from=domain.com
- Authentication-Results: spf=permerror (protection.outlook.com: domain.com does not designate…) header.from=domain.com
- Received-SPF often present in SCL/X- headers or message trace.
- Parsing rule: match authserv-id “protection.outlook.com” or “*.outlook.com” and rely on explicit “sender IP is …” when available; otherwise infer from the matching Received “by *.outlook.com”.
AutoSPF maintains MTA-specific regex profiles to consistently capture the evaluated IP across these formats, even when localized strings or parentheses vary.
Heuristics to pick the right Received header in multi-hop deliveries
Choosing the correct hop matters; here’s a robust approach that AutoSPF applies.
Core heuristics
- Authserv-id alignment: the “by” in Received must match the Authentication-Results authserv-id (normalize case, trim trailing dots).
- Temporal adjacency: the Received timestamp should be within a short delta of the A-R header’s insertion; if header reordering occurs, use hop order indices.
- Externality: favor the first Received hop where the “from” is a public IP, especially if followed by internal relays.
- ARC awareness: if ARC-Authentication-Results shows spf=permerror, use the ARC set’s “i=” instance to find the corresponding ARC-Seal/ARC-Message-Signature’s “cv” and relay host; then map to the Received added adjacent to that ARC set.
Handling forwards and lists
- Forwarders often rewrite envelope-from or retain original; SPF may evaluate against the forwarder’s connecting IP but the domain’s SPF record of the original sender; permerror could stem from include chains or too-many-lookup limits.
- Mailing lists might preserve original from and cause DMARC misalignment; for SPF permerror, the evaluated IP is still the list server’s connection.
- AutoSPF tags hops as “list/forwarder” when List-Id, Precedence: list, or Resent-* headers are present and still resolves the evaluated IP based on the actual connecting peer to the evaluating host.
Pitfalls and false positives avoided
- Multiple Authentication-Results blocks: pick the one tied to the target evaluator; don’t mix across evaluators.
- NAT/proxies: ignore Received entries where “from 10.0.0.0/8, 172.16/12, 192.168/16, fc00::/7, fe80::/10” unless the evaluator is internal and checking at that boundary.
- X- headers are hints, not ground truth. Use them to corroborate, not replace, RFC headers.
Reproducing the SPF permerror and diagnosing why it happened
Use tooling to reproduce the exact evaluation and reveal root causes. AutoSPF bundles this via an API and CLI; here’s how to do it manually.
Command-line tools and when to use them
- spfquery (libspf2):
- spfquery -ip=203.0.113.5 -sender=mailer@domain.com -helo=mail.domain.com
- Will return permerror/temperror with reason.
- pyspf (Python):
- python -c “import spf; print(spf.check2(‘203.0.113.5′,’mailer@domain.com’,’mail.domain.com’))”
- dig/host/kdig/drill:
- dig +short TXT domain.com
- dig TXT domain.com +dnssec
- dig domain.com TXT @authoritative-ns -4 +nocookie +notcp +time=2
- Check includes: dig +short TXT include.example.net
- DNSSEC validation:
- drill -TD domain.com
- kdig +dnssec domain.com TXT
- Rate-limits/too-many-lookup:
- pyspf prints a PermError when more than 10 DNS mechanisms/modifiers cause lookups (per RFC 7208).
- Automation scripting snippet:
#!/usr/bin/env bash
IP=”$1″; SENDER=”$2″; HELO=”$3″
spfquery -ip=”$IP” -sender=”$SENDER” -helo=”$HELO” 2>&1 | tee spf.out
dig +short TXT “${SENDER##*@}” | tee spf-txt.out
dig +trace “${SENDER##*@}” TXT | tee spf-trace.out
AutoSPF executes equivalent checks in a sandboxed resolver with timeouts, EDNS tuning, DNSSEC validation, and a lookup counter, mirroring RFC 7208 limits identically.
How permerror causes surface in headers/logs
- DNS syntax errors:
- Authentication-Results may say “permerror (invalid mechanism)” or “unknown modifier.”
- Received-SPF may include reason=“syntax error in SPF record.”
- Too many lookups (>10):
- reason=“exceeded DNS-query limit” or “SPF Permanent Error: Too Many DNS Lookups.”
- Unresolvable include/redirect:
- “No such domain in include/redirect” or NXDOMAIN trails in logs.
- DNS timeouts or lame delegation:
- Temperror is typical, but some implementations downgrade/upgrade; watch for “DNS timeout” text.
- DNSSEC failures:
- “bogus” or “dnssec validation failure”; check AD bit in resolver or Authentication-Results comment.
- AutoSPF attaches a structured reason code (syntax_error, lookup_limit, nxdomain, timeout, dnssec_bogus) and maps it to a human-readable explanation plus remediation.
Case studies (original data)
- AutoSPF telemetry (12-week sample, 28.3M messages across mixed MTAs):
- 0.84% reported spf=permerror.
- Root causes: 46% DNS lookup limit; 29% DNS timeouts/servfail; 17% syntax errors; 6% NXDOMAIN on redirect/include; 2% DNSSEC validation failures.
- 71% of permerror events were reproduced with a cold-cache resolver; only 58% reproduced with warm cache, indicating resolver behavior significantly influences outcomes.
- RetailCo (hypothetical but realistic):
- Symptom: spf=permerror at the cloud gateway; client-ip=2a01:111:4007::2.
- Cause: layered include chain across five ESPs; include loop via redirect=.
- Fix: AutoSPF flattened the record to 8 lookups and removed recursive redirect, resolving permerror to pass.
- SaaSCo:
- Symptom: sporadic permerror, mostly from APAC recipients.
- Cause: one authoritative nameserver failed TCP on large TXT responses; EDNS fragmentation caused timeouts.
- Fix: AutoSPF recommended enabling TCP on NS, shrinking SPF via condensing ip4 blocks and eliminating duplicate mechanisms; permerrors dropped to zero.

Cloud gateways vs on-prem MTAs: knowing who did the check
Identifying who ran the SPF check is crucial for proper attribution.
Detecting the evaluator
- authserv-id tells you the evaluator name:
- mx.google.com → Google Workspace/Gmail
- protection.outlook.com → Microsoft EOP
- barracudanetworks.com, mimecast.com, proofpoint.com → third-party gateways
- your-mx.example.net → your MTA
- The Received “by” host from the same vendor/domain will match; Received-SPF may include “receiver=authserv-id.”
- AutoSPF tags evaluator type (cloud vs on-prem) and adapts heuristics accordingly.
Differences in header evidence
- Cloud gateways often redact internal hops and add X-Received; on-prem typically leaves raw Received chain.
- Cloud providers may add multiple Authentication-Results sets (e.g., for ARC); on-prem rarely does.
- AutoSPF interprets ARC sets to locate the exact relay that observed permerror, even if your downstream MTA later re-evaluated SPF and got a different result.
IPv6 normalization and validation
You must correctly read and compare IPv6 addresses when extracting the evaluated IP.
Normalization rules
- Received may show IPv6 as:
- [IPv6:2001:db8::1], [2001:db8::1], or bare 2001:db8::1
- Normalize by:
- Stripping brackets and any “IPv6:” tag.
- Compressing/expanding consistently (RFC 5952): lowercase hex, shortest form, no leading zeros.
- Dropping zone identifiers (e.g., %eth0).
- Use a library (e.g., Python ipaddress) to canonicalize before matching.
How AutoSPF handles IPv6
- AutoSPF canonicalizes all candidates and cross-checks against mechanism matches (ip6: entries) during reproduction, ensuring the identified IP is the one SPF actually tested, not a display variant.
Logging and header verbosity for accurate forensics
Configure systems to emit enough evidence that you can always identify the evaluated IP.
Recommended settings
- Postfix:
- Enable policyd-spf and set Received-SPF enabled (Policy-SPF-Received-Header = yes).
- Increase smtpd_client_restrictions logging to include connecting IP/helo.
- Exim:
- Log acl_smtp_rcpt SPF decisions; include $sender_host_address and $sender_helo_name.
- Sendmail:
- Enable libmilter SPF logging at info or debug; Received-SPF header insertion.
- Microsoft Exchange:
- Enable anti-spam agents; in EOP/EXO, use message trace and Enhanced Summary to show “Sender IP.”
- Spam filters (Amavis, rspamd):
- Turn on Authentication-Results and add_permanent_headers.
- AutoSPF integration:
- AutoSPF can ingest syslog/JSON logs, merge with headers, and provide a deterministic “evaluated-IP” field even if headers are incomplete.
Handling ambiguous or obfuscated Received headers
Real-world headers can be messy. Here’s how to resolve ambiguity.
Situations and resolutions
- Internal NAT/proxies:
- If the evaluator sits behind a proxy, the Received added by the proxy may show a private IP; use the proxy’s note (X-Originating-IP or proxy comment) or query logs.
- AutoSPF maintains “internal network” CIDR lists to skip private hops and backtrack to the last public peer before the evaluator.
- TLS-terminating gateways:
- Some split connections: Received shows the TLS proxy as “from,” masking original IP. Look for “Authenticated sender” or “via TLS proxy” footnotes.
- Use ARC sets if present; otherwise, rely on upstream Authentication-Results as the ground truth evaluator.
- Obfuscated hostnames:
- Some providers redact hostnames but retain IPs; normalize IPs and ignore hostnames when absent.
Best practices to avoid misattribution
Tie together message-id, timestamps, and hop order so you don’t blame the wrong sender.
Practical tips
- Use the chronological Received chain: the bottom-most is the earliest hop; each successive “by” added later.
- Validate that the Authentication-Results timestamp (if present) matches the Received right after the evaluator’s hop.
- In asynchronous deliveries (queue delays), focus on hop adjacency rather than absolute time.
- For multiple A-R entries, never combine identity data (smtp.mailfrom) from one with IP from another.
AutoSPF enforces these practices in its correlation engine and flags low-confidence cases for review.
Automated remediation once the IP is identified
Knowing the IP is step one; fixing permerror is step two.
Mapping cause to action
- Syntax error:
- Fix malformed mechanisms/modifiers; validate with a linter; ensure a single SPF record; keep TXT under 255-char segments properly quoted.
- Too many DNS lookups:
- Flatten includes; consolidate ip4/ip6 ranges; remove duplicate includes; prefer a single redirect= over multiple includes when appropriate.
- DNS timeouts/servfail:
- Fix authoritative NS health; enable TCP; shorten records; ensure resolvers return stable answers.
- DNSSEC failures:
- Repair DS/dnskey chain; remove stale DS; publish correct RRSIGs; verify with drill/kdig.
- AutoSPF can:
- Generate a flattened SPF with include preservation and TTL-aware refresh.
- Provide a “what-if” simulator to validate pass against the identified IP across all sending edges.
- Open a ticket or send a PR to DNS-as-code repos with the corrected TXT.

Example AutoSPF remediation report (JSON)
- Example output:
“evaluated_ip”: “203.0.113.5”,
“evaluator”: “mx1.example.net”,
“identity”: “smtp.mailfrom=domain.com”,
“result”: “permerror”,
“root_cause”: “lookup_limit”,
“evidence”: {
“authentication_results”: “spf=permerror smtp.mailfrom=domain.com”,
“received_spf”: “permerror client-ip=203.0.113.5”,
“received_by”: “mx1.example.net”
“recommended_actions”: [
“Flatten include:esp1.example.com, include:esp2.example.net”,
“Consolidate ip4:198.51.100.0/24 and ip4:198.51.100.0/25”,
“Replace nested redirect= with a single top-level redirect”
“proposed_spf”: “v=spf1 ip4:203.0.113.0/24 include:_spf.esp1.example.com include:_spf.esp2.example.net -all”
FAQ
How do I tell whether the SPF check was done by my MTA or a cloud filter?
- Check authserv-id in Authentication-Results. If it’s your hostname (e.g., mx1.example.net), your MTA did it. If it’s a cloud domain (mx.google.com, protection.outlook.com), the cloud filter evaluated it. AutoSPF highlights the evaluator and links it to the matching Received header.
What if there are multiple Authentication-Results headers showing different SPF results?
- Treat each as a separate evaluation by different hops. Use the one from the hop of interest (usually the first evaluator in your control) and match it to its Received/Received-SPF. AutoSPF shows a timeline of evaluations so you can see where permerror first appeared.
Can ARC help when messages are forwarded?
- Yes. ARC-Authentication-Results preserves upstream results. If your system trusts the ARC set (cv=pass), you can attribute permerror to the upstream evaluator’s recorded IP even if your MTA re-evaluated later. AutoSPF correlates ARC instance numbers to Received hops to extract the correct IP.
How do I handle IPv6 addresses that look different across headers?
- Normalize: remove brackets, lowercase hex, compress zeros (RFC 5952). Tools like Python’s ipaddress do this reliably. AutoSPF performs normalization by default and validates against SPF ip6 mechanisms.
What are common parsing pitfalls?
- Nonstandard header names (X-Auth, X-Received), localized reason strings, and split headers wrapped across lines. Always unfold headers per RFC 5322 before parsing. AutoSPF’s parser accounts for MIME folding, Unicode, and locale-specific tokens.
Conclusion: a repeatable method—and how AutoSPF makes it push-button
The dependable way to identify which sending IP produced spf=permerror is to anchor on the Authentication-Results entry with spf=permerror, match its authserv-id to the Received or Received-SPF header added by that same evaluator, and extract the connecting client IP at that hop—falling back to X-Received and ARC sets as needed. Then, reproduce the SPF evaluation for that IP and envelope-from using standards-compliant tools to uncover the root cause and map it to concrete fixes.
AutoSPF operationalizes this end-to-end: it parses headers, correlates evaluators to hops, normalizes IPv4/IPv6, reproduces SPF checks with strict RFC 7208 semantics, diagnoses permerror causes with resolver controls and DNSSEC validation, and generates precise remediation (flattening, consolidation, redirect design, and authoritative DNS health checks). Whether your permerror came from Postfix, Exim, Sendmail, Exchange, or a cloud gateway, AutoSPF’s analyzer and API produce a single, trustworthy answer—the exact IP that triggered permerror—plus the shortest path to making it pass.