# DNSSEC Chain & Rollover Monitor (`monema/dnssec-chain-rollover-monitor`) Actor

Monitor public domains for DS/DNSKEY chain mismatches, DNSSEC signature expiry, and key-rollover drift with persistent cross-run history.

- **URL**: https://apify.com/monema/dnssec-chain-rollover-monitor.md
- **Developed by:** [Maarten Vreeburg](https://apify.com/monema) (community)
- **Categories:** Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 successful dnssec domain checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## DNSSEC Chain & Rollover Monitor

Monitor public DNSSEC-signed domains for parent `DS` / child `DNSKEY` mismatch, invalid DNSKEY or apex `SOA` signatures, expiring `RRSIG` records, recursive-resolver validation failure, and key-rollover drift. Results are structured for Apify schedules, datasets, webhooks, and downstream incident workflows.

This Actor is an independent DNS protocol monitor. It is not affiliated with or endorsed by Cloudflare, ICANN, IANA, any DNS operator, registry, registrar, or DNS provider. The default `cloudflare.com` target is a public interoperability/QA example only.

### Why use it?

A DNSSEC chain can fail even while the zone and nameservers are reachable. A stale parent `DS`, an incomplete key rollover, or expired signatures can make validating resolvers return `SERVFAIL`. This Actor gives you a persistent, reviewable observation instead of a one-time checker page.

It does **not** guarantee availability, global propagation, or complete DNSSEC correctness. One run observes one configured recursive-resolver path. Treat `changed`, `ds_dnskey_mismatch`, `invalid_signature`, `bogus_or_servfail`, and expiry warnings as review signals and confirm material decisions with your authoritative DNS provider, registrar, and additional independent resolvers.

### Input

```json
{
  "monitorId": "production-domains",
  "domains": [
    {
      "domain": "example.com",
      "label": "Customer portal",
      "expectedKeyTags": [12345]
    }
  ],
  "warningHours": 24,
  "timeoutSeconds": 15,
  "maxConcurrency": 3
}
````

| Field | Required | Limits | Purpose |
|---|---:|---:|---|
| `monitorId` | yes | 1–200 characters | Stable namespace for cross-run history. Reuse it on scheduled runs. |
| `domains` | yes | 1–50 unique public FQDNs | A string or object containing `domain`, optional `label`, and up to 20 expected DNSKEY key tags. |
| `warningHours` | no | 1–720; default 24 | Warn when the earliest observed DNSKEY/SOA `RRSIG` expires in less than this window. |
| `timeoutSeconds` | no | 3–30; default 15 | Bound for each DNS query/probe. |
| `maxConcurrency` | no | 1–5; default 3 | Maximum domains processed concurrently. |

The Actor accepts domain names only—not URLs, IP ranges, resolver addresses, arbitrary record types, or credentials.

### Output

One dataset row is emitted per domain. Important fields include:

- `status`: `initial_snapshot`, `unchanged`, `changed`, or non-billable `query_error`
- `posture`: `secure_observed`, `unsigned`, `island_of_security`, `missing_dnskey`, `ds_dnskey_mismatch`, `invalid_signature`, `incomplete_signature_evidence`, or `indeterminate_digest_policy`
- normalized `parent_ds` records
- normalized `child_dnskeys` with key tags, flags, algorithms, and SHA-256 fingerprints of public DNSKEY wire data (not raw key material)
- `matching_key_tags`
- `dnskey_signature_valid` and `soa_signature_valid`
- `earliest_rrsig_expiration`, `hours_until_earliest_rrsig_expiration`, and `rrsig_health`
- `recursive_validation.status`, including `secure_ad`, `insecure_no_ad`, `bogus_or_servfail`, or a bounded probe error
- `authoritative_nameserver` and the pinned public `authoritative_ip` used for child-zone evidence
- `snapshot_hash`, `previous_snapshot_hash`, and `changed_fields`
- expected-key-tag matching when configured
- `state_store_mode`, `charged_event`, and a bounded error field

Example:

```json
{
  "domain": "cloudflare.com",
  "status": "unchanged",
  "posture": "secure_observed",
  "matching_key_tags": [2371],
  "dnskey_signature_valid": true,
  "soa_signature_valid": true,
  "recursive_validation": {"status": "secure_ad", "ad": true},
  "rrsig_health": "ok",
  "authoritative_nameserver": "ns3.cloudflare.com",
  "authoritative_ip": "162.159.0.33",
  "state_store_mode": "named",
  "charged_event": "dnssec_domain_check"
}
```

Key tags and RRSIG expiry times can legitimately change during a healthy rollover or signing cycle. `changed` means “review the structural chain evidence,” not “outage” or “attack.” Routine movement of only the RRSIG expiration timestamp does not flip the persistent structural status; use `rrsig_health` and `hours_until_earliest_rrsig_expiration` for expiry alerting.

### Persistence

Successful observations are stored in the explicit named key-value store:

```text
dnssec-chain-rollover-monitor-state-v1
```

Each state key is a SHA-256 digest of `monitorId + domain`. Reuse the same `monitorId` to get `initial_snapshot → unchanged → changed` behavior across Apify runs. Failed observations do not overwrite the last successful baseline.

Changing `monitorId` intentionally creates a new baseline. The default run-scoped store is not used for monitor history.

### DNSSEC observation model and limitations

For each domain, the Actor:

1. requests parent `DS`, resolves authoritative nameservers, blocks non-global destinations, and pins direct public authoritative queries for child `DNSKEY` and apex `SOA` evidence with DNSSEC records enabled so a broken chain can still be inspected;
2. compares supported DS digests to observed DNSKEYs;
3. cryptographically validates the DNSKEY RRset's self-signature and the apex SOA signature with the observed DNSKEY set;
4. performs a separate normal recursive-resolver probe and records the `AD` result or a possible bogus/`SERVFAIL` outcome;
5. compares the normalized observation with the previous successful snapshot.

Important boundaries:

- The Actor does not run a globally distributed resolver network.
- Parent `DS` discovery and the recursive `AD` probe use the runtime resolver; child DNSKEY/SOA signatures come from one pinned public authoritative nameserver.
- Private, loopback, link-local, reserved, and otherwise non-global authoritative destinations are blocked before any direct query.
- A recursive `bogus_or_servfail` response can also reflect an upstream resolver or network failure; confirm independently.
- `secure_observed` is deliberately narrower than a guarantee: it proves matching/validating evidence in this observation, not universal propagation or future correctness.
- The Actor does not modify DNS, registrar settings, trust anchors, or keys.
- It does not enumerate subdomains or scan IP ranges.
- Public DNS responses can change between queries during normal cache and rollover windows.

### Pricing

The Actor uses pay per event:

- event: `dnssec_domain_check`
- one event per successfully queried, classified, compared, and stored domain observation
- invalid input and `query_error` rows are not charged by this custom event

A run-level maximum charge limit is honored before domains are processed and charged.

### Recommended schedule

- Routine managed DNS: every 1–6 hours
- Active KSK/ZSK rollover: every 15–60 minutes while respecting your DNS provider's guidance
- Keep `monitorId` stable
- Alert on `status = changed`, non-`secure_observed` posture for domains expected to be signed, `recursive_validation.status = bogus_or_servfail`, or `rrsig_health != ok`

### Local development

```bash
python -m venv .venv
.venv/bin/pip install -r requirements.txt
APIFY_LOCAL_STORAGE_DIR="$PWD/storage" .venv/bin/python -m src
.venv/bin/python smoke_test.py
npx --yes apify-cli validate-schema
```

Run the Actor twice with the same input and storage directory to verify named-store reuse.

# Actor input Schema

## `monitorId` (type: `string`):

Stable identifier used to keep this monitor's history separate across scheduled runs.

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

One to 50 public domain names. Each entry can include optional expected DNSKEY key tags.

## `warningHours` (type: `integer`):

Mark the earliest observed DNSKEY/SOA signature as warning when it expires within this many hours.

## `timeoutSeconds` (type: `integer`):

Bound each DNS query and validating-resolver probe.

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

Maximum number of public domains observed concurrently.

## Actor input object example

```json
{
  "monitorId": "my-dnssec-monitor",
  "domains": [
    {
      "domain": "cloudflare.com",
      "label": "Public DNSSEC QA domain"
    }
  ],
  "warningHours": 24,
  "timeoutSeconds": 15,
  "maxConcurrency": 3
}
```

# 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": [
        {
            "domain": "cloudflare.com",
            "label": "Public DNSSEC QA domain"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("monema/dnssec-chain-rollover-monitor").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": [{
            "domain": "cloudflare.com",
            "label": "Public DNSSEC QA domain",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("monema/dnssec-chain-rollover-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    {
      "domain": "cloudflare.com",
      "label": "Public DNSSEC QA domain"
    }
  ]
}' |
apify call monema/dnssec-chain-rollover-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=monema/dnssec-chain-rollover-monitor",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "DNSSEC Chain & Rollover Monitor",
        "description": "Monitor public domains for DS/DNSKEY chain mismatches, DNSSEC signature expiry, and key-rollover drift with persistent cross-run history.",
        "version": "0.1",
        "x-build-id": "OlfcBWYB5t0bHDmsc"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/monema~dnssec-chain-rollover-monitor/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-monema-dnssec-chain-rollover-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/monema~dnssec-chain-rollover-monitor/runs": {
            "post": {
                "operationId": "runs-sync-monema-dnssec-chain-rollover-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/monema~dnssec-chain-rollover-monitor/run-sync": {
            "post": {
                "operationId": "run-sync-monema-dnssec-chain-rollover-monitor",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "monitorId",
                    "domains"
                ],
                "properties": {
                    "monitorId": {
                        "title": "Monitor ID",
                        "minLength": 1,
                        "maxLength": 200,
                        "type": "string",
                        "description": "Stable identifier used to keep this monitor's history separate across scheduled runs.",
                        "default": "my-dnssec-monitor"
                    },
                    "domains": {
                        "title": "Domains",
                        "minItems": 1,
                        "maxItems": 50,
                        "type": "array",
                        "description": "One to 50 public domain names. Each entry can include optional expected DNSKEY key tags.",
                        "items": {
                            "type": "object",
                            "required": [
                                "domain"
                            ],
                            "properties": {
                                "domain": {
                                    "title": "Public domain",
                                    "description": "Public fully qualified domain name; do not enter a URL.",
                                    "type": "string",
                                    "minLength": 1,
                                    "maxLength": 253,
                                    "editor": "textfield"
                                },
                                "label": {
                                    "title": "Label",
                                    "description": "Optional human-readable service or zone label.",
                                    "type": "string",
                                    "maxLength": 200,
                                    "editor": "textfield"
                                },
                                "expectedKeyTags": {
                                    "title": "Expected DNSKEY key tags",
                                    "description": "Optional trusted key tags. Multiple values support planned overlap rotations.",
                                    "type": "array",
                                    "maxItems": 20,
                                    "uniqueItems": true,
                                    "editor": "json",
                                    "items": {
                                        "type": "integer",
                                        "minimum": 0,
                                        "maximum": 65535
                                    }
                                }
                            }
                        },
                        "default": [
                            {
                                "domain": "cloudflare.com",
                                "label": "Public DNSSEC QA domain"
                            }
                        ]
                    },
                    "warningHours": {
                        "title": "RRSIG expiry warning window in hours",
                        "minimum": 1,
                        "maximum": 720,
                        "type": "integer",
                        "description": "Mark the earliest observed DNSKEY/SOA signature as warning when it expires within this many hours.",
                        "default": 24
                    },
                    "timeoutSeconds": {
                        "title": "Per-query timeout",
                        "minimum": 3,
                        "maximum": 30,
                        "type": "integer",
                        "description": "Bound each DNS query and validating-resolver probe.",
                        "default": 15
                    },
                    "maxConcurrency": {
                        "title": "Maximum concurrent domains",
                        "minimum": 1,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Maximum number of public domains observed concurrently.",
                        "default": 3
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
