# Domain & Email Security Audit: SPF, DMARC, DKIM, SSL, DNS (`rod_analytics/domain-security-audit`) Actor

Audit any domain's email and web security in seconds: SPF, DMARC, DKIM, DNSSEC, MTA-STS, BIMI, CAA, SSL certificate expiry and HTTP security headers. A-F grade plus prioritized fixes. Bulk lists or one domain at a time for AI agents.

- **URL**: https://apify.com/rod\_analytics/domain-security-audit.md
- **Developed by:** [Rod Services](https://apify.com/rod_analytics) (community)
- **Categories:** Developer tools, Lead generation
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event + usage

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

### What does Domain & Email Security Audit do?

**Domain & Email Security Audit** checks the **email deliverability and web security setup of any domain** in one to two seconds and gives it an **A to F grade** with a prioritized list of fixes. One run covers:

- **Email authentication:** SPF (with the 10 DNS lookup limit), DMARC policy, DKIM keys and key size, MTA-STS, TLS-RPT and BIMI.
- **DNS:** A, AAAA, MX, NS, TXT and CAA records, plus DNSSEC.
- **Web security:** SSL/TLS certificate (issuer, expiry, days left, SANs, TLS version, chain), HTTP security headers (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy) and the HTTP to HTTPS redirect.
- **Optional:** domain registration and expiry dates from RDAP.

It uses only **public DNS, one TLS handshake and a few HTTP requests** per domain. No API keys, no third-party paid services. Costs **$1.50 per 1,000 domains**.

Paste a list of domains for a **bulk DMARC, SPF and SSL check**, or call it **one domain at a time as a real-time API** from your app or **AI agent**. Run it on the Apify platform with API access, scheduling, integrations (Make, Zapier, n8n, Google Sheets, Slack, webhooks) and monitoring.

Try it now: press **Start** with the prefilled example (apify.com, github.com, google.com). It finishes in a few seconds.

### Why use Domain & Email Security Audit?

- **Email deliverability.** Gmail and Yahoo require SPF, DKIM and DMARC for bulk senders. Find missing or broken records before your mail lands in spam.
- **MSPs and IT consultants.** Audit every client domain on a schedule. Get alerts when a certificate is about to expire or someone weakens DMARC.
- **Security audits and pentest prep.** Quick external posture check: spoofing risk, TLS hygiene, missing headers, no CAA, no DNSSEC.
- **Lead qualification for security and email vendors.** Grade thousands of prospect domains. Companies with DMARC p=none or no SPF are warm leads for your product.
- **Vendor and supply-chain risk.** Score the domains of your suppliers and partners.
- **AI agents and LLM tools.** A fast, predictable JSON endpoint. An agent asks "is example.com protected against email spoofing?" and gets findings with fixes it can explain.

### How to check SPF, DMARC, DKIM and SSL for a list of domains

1. Open the **Input** tab.
2. Paste domains into **Domains**, one per line. `example.com`, `https://www.example.com/page` and `jane@example.com` all work.
3. Optional: add your own **DKIM selectors**. Common ones are always checked.
4. Optional: untick checks you do not need.
5. Press **Start**.
6. Open the **Output** tab. Use the **Overview**, **Email security**, **Web and TLS** or **Findings** view. Download results as JSON, CSV, Excel or HTML, or fetch them by API.

### Input

All fields are on the Input tab. Only `domains` must be filled. Without it the run returns one free help row.

| Field            | Type             | Default | Description                                                                                              |
| ---------------- | ---------------- | ------- | -------------------------------------------------------------------------------------------------------- |
| `domains`        | array of strings |         | Domains, URLs or email addresses. Duplicates and a leading `www.` are removed. IDN names are supported.  |
| `dkimSelectors`  | array of strings | `[]`    | Extra DKIM selectors. Always checked: google, selector1, selector2, default, k1, s1, mail, dkim.         |
| `checks`         | array of strings | all     | Any of dns, spf, dmarc, dkim, dnssec, mtaSts, bimi, ssl, headers, redirect. Only these affect the grade. |
| `includeRdap`    | boolean          | `false` | Add registrar and expiry dates from RDAP. Low volume only, 1 lookup per second, 500 per run.             |
| `maxConcurrency` | integer          | `10`    | Domains audited in parallel (1 to 50).                                                                   |
| `timeoutSecs`    | integer          | `10`    | Timeout per TLS or HTTP connection (3 to 60). A whole domain is capped at 45 s.                          |

```json
{
    "domains": ["apify.com", "github.com", "google.com"],
    "dkimSelectors": ["mandrill", "20230601"],
    "checks": ["spf", "dmarc", "dkim", "ssl", "headers"],
    "includeRdap": false,
    "maxConcurrency": 10
}
```

### Output

One item per domain. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel. Shortened example:

```json
{
    "domain": "github.com",
    "grade": "A",
    "score": 90,
    "summary": { "critical": 0, "high": 0, "medium": 0, "low": 5, "info": 4 },
    "spf": { "found": true, "all": "~all", "dnsLookups": 10, "voidLookups": 0 },
    "dmarc": { "found": true, "policy": "quarantine", "pct": 100, "rua": ["mailto:dmarc@github.com"] },
    "dkim": [
        { "selector": "google", "keyType": "rsa", "keyBits": 2048 },
        { "selector": "selector1", "keyType": "rsa", "keyBits": 1024 }
    ],
    "dnssec": { "signed": false, "validated": false },
    "mtaSts": { "found": false, "tlsRpt": { "found": false } },
    "bimi": { "found": false },
    "ssl": {
        "issuer": "Sectigo Limited / Sectigo Public Server Authentication CA DV E36",
        "validTo": "2026-11-29T23:59:59.000Z",
        "daysLeft": 63,
        "protocol": "TLSv1.3",
        "chainOk": true,
        "hostnameMatch": true
    },
    "headers": {
        "hsts": "max-age=31536000; includeSubdomains; preload",
        "xFrameOptions": "deny",
        "permissionsPolicy": null
    },
    "redirect": { "redirectsToHttps": true, "status": 301, "location": "https://github.com/" },
    "findings": [
        {
            "severity": "low",
            "check": "spf",
            "message": "SPF uses 10 of 10 allowed DNS lookups.",
            "recommendation": "Leave headroom. Adding one more provider may break SPF."
        },
        {
            "severity": "low",
            "check": "dmarc",
            "message": "DMARC policy is p=quarantine.",
            "recommendation": "Move to p=reject for full protection."
        },
        {
            "severity": "low",
            "check": "dkim",
            "message": "DKIM selectors \"selector1\", \"k1\" use a 1024-bit RSA key.",
            "recommendation": "1024-bit keys still pass DKIM, but 2048-bit is recommended (RFC 8301). Rotate when convenient."
        }
    ],
    "checkedAt": "2026-09-27T10:31:05.670Z",
    "error": null
}
```

### Data fields

| Field      | What it contains                                                                                     |
| ---------- | ---------------------------------------------------------------------------------------------------- |
| `grade`    | A to F from `score`. See [How the grade is calculated](#how-the-grade-is-calculated).                |
| `score`    | 0 to 100. Starts at 100 and loses points per finding.                                                |
| `dns`      | A, AAAA, MX, NS, TXT and CAA records (CAA inherited from parent names). `nullMx` marks "0 ." MX.     |
| `spf`      | Record, parsed mechanisms, `all` qualifier, recursive DNS lookup count, void lookups, includes.      |
| `dmarc`    | Record, policy, subdomain policy, pct, rua and ruf, alignment. Falls back to the parent domain.      |
| `dkim`     | Keys found per selector with key type, RSA key size, test mode and revoked flag.                     |
| `dnssec`   | DS record present and resolver-validated (AD flag), via DNS-over-HTTPS.                              |
| `mtaSts`   | MTA-STS TXT record, fetched policy (mode, mx, max\_age) and TLS-RPT record.                           |
| `bimi`     | BIMI record with logo and VMC URLs.                                                                  |
| `ssl`      | Issuer, subject, validFrom, validTo, daysLeft, SANs, TLS protocol, cipher, chain and hostname check. |
| `headers`  | HSTS (max-age, includeSubDomains, preload), CSP, X-Frame-Options, and the other security headers.    |
| `redirect` | Whether `http://` redirects to `https://`, with the redirect chain.                                  |
| `rdap`     | Registrar, created, expires, daysToExpiry, status. Only when `includeRdap` is on.                    |
| `findings` | `severity`, `check`, `message` and `recommendation`, worst first.                                    |
| `error`    | Set when the domain could not be audited, for example it has no DNS records. Not charged.            |

### How the grade is calculated

Every domain starts at **100 points**. Each finding subtracts points by severity:

| Severity | Points | Examples                                                                                   |
| -------- | ------ | ------------------------------------------------------------------------------------------ |
| critical | -30    | SPF `+all`, expired certificate, expired domain registration                               |
| high     | -15    | no SPF, no DMARC, SPF over 10 lookups, certificate for the wrong name, cert expires soon   |
| medium   | -7     | DMARC `p=none` or partial quarantine, no DKIM key found, no HSTS, no HTTPS redirect        |
| low      | -2     | no CAA, no DNSSEC, no MTA-STS, missing CSP or nosniff, DMARC `p=quarantine`, 1024-bit DKIM |
| info     | 0      | no BIMI, no Referrer-Policy, SPF `~all` while DMARC enforces                               |

Low findings are hygiene items, so they **count at most 6 points per check**. Ten small header gaps never cost more than one missing HSTS header. High and critical findings are never capped.

| Score | Grade | Meaning                                                                 |
| ----- | ----- | ----------------------------------------------------------------------- |
| 90+   | A     | Strong. Only hygiene items left.                                        |
| 80-89 | B     | Good. One real gap or several hygiene items.                            |
| 65-79 | C     | Needs work. Usually a missing SPF, DMARC or HTTPS protection.           |
| 50-64 | D     | Weak. Several real gaps. Any critical finding also caps the grade at D. |
| < 50  | F     | Spoofable or broken. Fix the high and critical findings first.          |

Context rules keep the grade fair:

- A domain **without MX, or with a null MX** (`0 .`), is treated as a non-mail domain. Missing DKIM, MTA-STS and BIMI are not penalised. SPF `-all` and DMARC `p=reject` are still expected, because spoofing does not need an MX.
- SPF `~all` is only info when DMARC is at `quarantine` or `reject` with `pct=100`. That combination is common and safe.
- Certificate expiry warnings scale with the certificate lifetime. A 6-day certificate with 4 days left is fine. A 90-day certificate is flagged below 14 days (high) and 30 days (medium).
- If TLS works but the homepage times out, the site most likely blocks data-center traffic. That is info, not a penalty.

Every finding has a `recommendation`, so the grade is always explained by the `findings` list.

### Use it as an API for AI agents (Standby mode)

The Actor also runs as an always-ready HTTP API. Send one domain, get one JSON result back:

```
GET https://rod-analytics--domain-security-audit.apify.actor/?domain=example.com
Authorization: Bearer <YOUR_APIFY_TOKEN>
```

Optional query parameters: `dkimSelectors=s1,s2`, `checks=spf,dmarc,ssl`, `includeRdap=true`, `timeoutSecs=10`. `url=` works as an alias of `domain=`.

- **200** with a graded result: billed as one audited domain.
- **200** with `error` set (for example the domain has no DNS records): not billed.
- **400** for an invalid domain, **402** when your maximum cost per run is reached. Not billed.

It works well as a tool in LangChain, CrewAI, n8n AI agents or any MCP client.

### How much does a domain security audit cost?

Pricing is **pay per event**: **$1.50 per 1,000 audited domains** ($0.0015 per domain) plus a $0.001 start fee per run. Domains that fail (no DNS records, invalid names) are not charged. The prefilled 3-domain example costs under one cent. Platform usage is included. The Apify free plan's $5 monthly credit covers over 3,000 audits.

Set **Maximum cost per run** on the run options to cap spending. The Actor never charges past the limit. It stops cleanly and the status message says how many domains were skipped.

### Tips and advanced options

- **Find more DKIM keys.** DKIM selectors cannot be listed from DNS. Open a received email, find `s=` in its `DKIM-Signature` header, and add it to `dkimSelectors`.
- **Go faster.** Turn off checks you do not need. `spf` and `dmarc` alone take well under a second per domain.
- **Big lists.** Raise `maxConcurrency` to 20 to 30. 1,000 domains take about 5 to 8 minutes at 256 MB.
- **Monitoring.** Schedule a daily run and add a webhook or Slack integration. Alert on `ssl.daysLeft < 14` or on a grade drop.
- **Subdomains.** Enter `mail.example.com` to audit it directly. DMARC falls back to the organizational domain like real receivers do.

### FAQ, disclaimers and support

**Is this legal?** Yes. The Actor only reads public DNS records, performs a normal TLS handshake and fetches the homepage headers, the same things any mail server or browser does. It does not scan ports, brute-force anything or send email.

**Why is RDAP off by default?** Registries publish RDAP for occasional lookups. Verisign's RDAP terms of service (.com and .net) forbid high-volume automated queries. The option is limited to 1 request per second and 500 domains per run, and results are cached. Use it for small lists only.

**Why does a site show HTTP 403?** Some sites block data-center traffic with a bot-protection page. The header results then describe that page. The Actor adds an info finding when this happens.

**Why did DKIM say "not found" when I sign my mail?** Your provider uses a selector that is not in the common list. Add it to `dkimSelectors`.

**What happens if my input is invalid?** The run still ends **SUCCEEDED** and you pay only the small start fee. No domain is charged. The dataset gets one help row, and the run status message says the same:

```json
{
    "error": true,
    "errorCode": "EMPTY_INPUT",
    "message": "No domains in input.",
    "howToFix": "Add at least one domain to \"domains\", e.g. [\"apify.com\"]. ..."
}
```

`EMPTY_INPUT` means no domain was given. `INVALID_INPUT` means none of the entries is a public domain name (IP addresses, `localhost` and test TLDs are skipped). In a list that mixes good and bad entries, the good ones are audited and each bad one gets a free row with its `error` text.

**Known limits.** No SMTP connection to MX servers, no port scan, no blacklist lookup. The registrable-domain logic is a heuristic, so rare public suffixes may fall back incorrectly. IPv4 is used for all connections.

Found a bug or want another check? Open an issue on the **Issues** tab. Need a custom audit, white-label report or integration? Get in touch through the Actor page.

# Actor input Schema

## `domains` (type: `array`):

Domains to audit, one per line (commas and spaces also separate entries). Bare domains (example.com), subdomains (mail.example.com), URLs (https://www.example.com/page), email addresses (jane@example.com) and IDN names (münchen.de) are accepted. A leading www. is removed so email checks run on the mail domain. Invalid entries are listed in the results with an error and are not charged.

## `dkimSelectors` (type: `array`):

DKIM keys can only be found if you know the selector. These common selectors are always checked: google, selector1, selector2, default, k1, s1, mail, dkim. Add your own here, for example the one in the DKIM-Signature header (s=) of an email you received.

## `checks` (type: `array`):

Leave everything selected for a full audit. Switch off checks you do not need to make runs faster. Only selected checks affect the grade.

## `includeRdap` (type: `boolean`):

Adds registrar, creation and expiry dates from public RDAP (rdap.org). Low volume only: limited to 1 lookup per second and 500 per run. Registries such as Verisign (.com, .net) forbid high-volume automated RDAP queries in their terms, so leave this off for large lists.

## `maxConcurrency` (type: `integer`):

How many domains to audit in parallel.

## `timeoutSecs` (type: `integer`):

Maximum wait for each TLS or HTTP connection. A whole domain audit is capped at 45 seconds.

## Actor input object example

```json
{
  "domains": [
    "apify.com",
    "github.com",
    "google.com"
  ],
  "dkimSelectors": [],
  "checks": [
    "dns",
    "spf",
    "dmarc",
    "dkim",
    "dnssec",
    "mtaSts",
    "bimi",
    "ssl",
    "headers",
    "redirect"
  ],
  "includeRdap": false,
  "maxConcurrency": 10,
  "timeoutSecs": 10
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `findings` (type: `string`):

No description

## `results` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "domains": [
        "apify.com",
        "github.com",
        "google.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("rod_analytics/domain-security-audit").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "domains": [
        "apify.com",
        "github.com",
        "google.com",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("rod_analytics/domain-security-audit").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "domains": [
    "apify.com",
    "github.com",
    "google.com"
  ]
}' |
apify call rod_analytics/domain-security-audit --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rod_analytics/domain-security-audit"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/YKiBPmiXmFFlOASLs/builds/eaItO0k9icvy5bY6C/openapi.json
