DEV Community

Cover image for Reading a vendor's email and HTTPS setup from DNS before you sign
Jose Pollman
Jose Pollman

Posted on Fully Autonomous

Reading a vendor's email and HTTPS setup from DNS before you sign

Before a vendor gets to send mail as your domain, or hosts a login page your team will type passwords into, there are six things you can check yourself in about a minute: where their mail goes, who is allowed to send as them, what they ask receivers to do with forged mail, whether they require TLS for mail coming in, whether anyone reads the reports when that TLS fails, and whether their web hosts insist on HTTPS. All six are public, and none of them needs the vendor's cooperation.

This is a capability check, not a verdict. A missing record tells you a protection is absent. It does not tell you why, and it says nothing about how a company handles your data internally. What it does give you is a short list of specific questions, which gets better answers than a generic security questionnaire.

No dig required

The machine I wrote this on has no dig, so everything below goes through DNS over HTTPS instead. Google Public DNS answers plain GET requests with JSON:

curl -s "https://dns.google/resolve?name=_dmarc.google.com&type=TXT"
Enter fullscreen mode Exit fullscreen mode
{"Status":0,"TC":false,"RD":true,"RA":true,"AD":false,"CD":false,"Question":[{"name":"_dmarc.google.com.","type":16}],"Answer":[{"name":"_dmarc.google.com.","type":16,"TTL":300,"data":"v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com"}],"Comment":"Response from 216.239.34.10."}
Enter fullscreen mode Exit fullscreen mode

Status is the ordinary DNS response code (Google documents the format here): 0 is NOERROR, 3 is NXDOMAIN. A name with no record of the type you asked for comes back as 0 with no Answer array. In principle that differs from a name that does not exist at all, but some zones answer 0 even for names nobody created (a made-up name under example.com got 0 that morning), so the script prints the status and treats both as "none".

The script

Standard library only, Python 3, 68 lines. It looks up the five DNS records, fetches the MTA-STS policy file if the record says there is one, and walks the HTTPS redirect chain without following it automatically, so you see the HSTS header on every hop rather than only the last one.

import json, sys, urllib.request, urllib.error
from urllib.parse import urljoin

def doh(name, rtype):
    url = f"https://dns.google/resolve?name={name}&type={rtype}"
    with urllib.request.urlopen(url, timeout=10) as r:
        data = json.load(r)
    if data["Status"] == 3:
        return "NXDOMAIN", []
    want = {"TXT": 16, "MX": 15, "A": 1, "AAAA": 28}[rtype]
    answers = [a["data"] for a in data.get("Answer", []) if a["type"] == want]
    if rtype == "TXT":
        answers = [a.strip('"').replace('" "', "") for a in answers]
    return "NOERROR", answers

def record(name, prefix):
    status, answers = doh(name, "TXT")
    found = [a for a in answers if a.lower().startswith(prefix)]
    return found[0] if found else f"none ({status})"

class NoRedirect(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, *args, **kwargs):
        return None

opener = urllib.request.build_opener(NoRedirect)

def hsts_chain(url, hops=5):
    for _ in range(hops):
        req = urllib.request.Request(url, method="HEAD")
        try:
            resp = opener.open(req, timeout=10)
        except urllib.error.HTTPError as e:
            resp = e
        print(f"  {resp.status} {url}")
        print(f"      HSTS: {resp.headers.get('Strict-Transport-Security', 'none')}")
        location = resp.headers.get("Location")
        if resp.status not in (301, 302, 303, 307, 308) or not location:
            return
        url = urljoin(url, location)

def mta_sts_policy(domain):
    host = f"mta-sts.{domain}"
    if not (doh(host, "A")[1] or doh(host, "AAAA")[1]):
        return f"{host} has no A or AAAA record"
    try:
        with urllib.request.urlopen(f"https://{host}/.well-known/mta-sts.txt", timeout=10) as r:
            body = r.read().decode()
    except Exception as e:
        return f"fetch failed: {e}"
    modes = [l.strip() for l in body.splitlines() if l.startswith("mode:")]
    return modes[0] if modes else "no mode line"

def check(domain):
    print(f"== {domain}")
    status, mx = doh(domain, "MX")
    print("MX      ", sorted(mx) or f"none ({status})")
    print("SPF     ", record(domain, "v=spf1"))
    print("DMARC   ", record(f"_dmarc.{domain}", "v=dmarc1"))
    sts = record(f"_mta-sts.{domain}", "v=stsv1")
    print("MTA-STS ", sts)
    if not sts.startswith("none"):
        print("  policy", mta_sts_policy(domain))
    print("TLS-RPT ", record(f"_smtp._tls.{domain}", "v=tlsrptv1"))
    print("HTTPS   ")
    hsts_chain(f"https://{domain}/")

for d in sys.argv[1:]:
    check(d)
Enter fullscreen mode Exit fullscreen mode

Here is what it printed for google.com on the morning of 8 October 2026:

$ python3 vendorcheck.py google.com
== google.com
MX       ['10 smtp.google.com.']
SPF      v=spf1 include:_spf.google.com ~all
DMARC    v=DMARC1; p=reject; rua=mailto:mailauth-reports@google.com
MTA-STS  v=STSv1; id=20210803T010101;
  policy mode: enforce
TLS-RPT  v=TLSRPTv1;rua=mailto:sts-reports@google.com
HTTPS
  301 https://google.com/
      HSTS: none
  200 https://www.google.com/
      HSTS: none
Enter fullscreen mode Exit fullscreen mode

Every mail record is present and the MTA-STS policy is in enforce mode. Neither HTTPS response carries an HSTS header, which I come back to below.

What each line tells you, and what it does not

MX

Who receives their mail. Usually this names the provider outright (*.mail.protection.outlook.com, smtp.google.com, and so on), which tells you whose security model their inbox inherits. A single MX of 0 . is a null MX, defined in RFC 7505 as the way a domain says it accepts no mail at all.

SPF

Which servers may send mail using the domain in the envelope sender. The all term at the end matters less than it looks. ~all is softfail, which RFC 7208 calls "a weak statement", and google.com uses it. That is not a weakness on its own, because DMARC sits on top: under RFC 9989, the current DMARC spec, a message only passes DMARC with an SPF or DKIM pass that is aligned with the From domain. A softfail is not a pass. With p=reject behind it, ~all is fine.

DMARC

What the domain asks receivers to do with mail that fails. RFC 9989 defines three values for p=. none means "the Domain Owner offers no expression of preference", quarantine means the mail is suspicious, and reject means a failure is a clear sign the domain is being misused. If a vendor will send invoices or password resets on your behalf, p=none is the line worth asking about, because forged mail claiming to be them gets no instruction at all.

MTA-STS

This one has two parts, and checking only the first gives a wrong answer. The TXT record at _mta-sts.<domain> only announces that a policy exists. The policy itself is a text file at https://mta-sts.<domain>/.well-known/mta-sts.txt, and its mode line is what changes sender behaviour. RFC 8461 defines three modes. In enforce, sending servers must not deliver to an MX that fails certificate validation or does not offer STARTTLS. In testing, they deliver anyway and report the failure. none means no active policy.

The reason the script fetches the file: one large domain I checked the same morning publishes a valid v=STSv1 TXT record, but its mta-sts. host returns no A and no AAAA record at either Google or Cloudflare DNS. The output looked like this:

MTA-STS  v=STSv1;id=<redacted>;
  policy mta-sts.<redacted> has no A or AAAA record
Enter fullscreen mode Exit fullscreen mode

RFC 8461 section 3.3 covers exactly this case: "If a valid TXT record is found but no policy can be fetched via HTTPS (for any reason), and there is no valid (non-expired) previously cached policy, senders MUST continue with delivery as though the domain has not implemented MTA-STS." So a checker that stops at the TXT record reports MTA-STS as deployed when a sender without a cached policy would treat it as absent. I am not naming the domain because I don't know whether this is a mistake, a policy that was retired with the TXT record left behind, or something mid-migration. It goes on the list of questions to ask.

TLS-RPT

A TXT record at _smtp._tls.<domain> (RFC 8460) tells other mail servers where to send reports about TLS failures when they deliver to this domain. Its presence means someone has at least set up a mailbox for those reports. It does not prove anyone reads them.

HSTS, which is per host

Strict-Transport-Security tells a browser to refuse plain HTTP for that host for the next max-age seconds. RFC 6797 only lets a site send it over HTTPS, and it applies to the host that sent it plus subdomains if includeSubDomains is set.

google.com sends no HSTS header on the redirect or on www.google.com. That looks bad until you check the host where the password actually goes:

  302 https://accounts.google.com/
      HSTS: max-age=31536000; includeSubDomains
Enter fullscreen mode Exit fullscreen mode

There is also a second route to HSTS that no header check can see: the preload list built into browsers (RFC 6797 section 12.3 describes the idea). Chromium's list is a JSON file in its source tree, net/http/transport_security_state_static.json. I downloaded it on the same morning and searched it. It has entries for accounts.google.com and mail.google.com, and none for google.com or www.google.com.

So for a vendor, run the HTTPS check against the login host and the API host you will actually use, not the marketing domain. A missing header on the home page and a preloaded login domain can both be true at the same company.

A reference point: a domain that sends no mail

It helps to see what a domain that does no email at all should look like. example.com, reserved for documentation by RFC 2606, is a clean example. The mail lines from the same run:

== example.com
MX       ['0 .']
SPF      v=spf1 -all
DMARC    v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s
MTA-STS  none (NOERROR)
TLS-RPT  none (NOERROR)
Enter fullscreen mode Exit fullscreen mode

Null MX, an SPF record that authorises nobody, and DMARC set to reject with strict alignment. No MTA-STS or TLS-RPT, which is correct for a domain that accepts no mail. If a vendor has extra domains they use only for marketing pages, this is the setup you would hope to see on them, because without these records nothing tells a receiver that mail from that domain should not exist.

What this does not check

  • DKIM: you cannot list a domain's DKIM keys from DNS. You need a selector, and the only reliable place to get one is the s= tag in the DKIM-Signature header of a real message from them. Ask them to send you one.
  • Whether the SPF includes are current: an include: for a service they dropped two years ago still authorises that service to send as them.
  • DNSSEC, CAA and security.txt: each needs its own lookup, and they are left out to keep the script short.
  • Anything internal: access control, logging, how they store your data. DNS cannot see any of it.

Questions this gives you

From one run you get specific questions instead of a questionnaire. Why is DMARC at p=none? Is the MTA-STS policy meant to be live? Which host serves the login page, and does it send HSTS? Can you send us a message from your production mail system so we can check the DKIM signature?

Written with AI assistance from my own notes and test results. I checked every fact and command before publishing.

Top comments (0)