Skip to content

DNS Security

DNS Security provides DNS-level threat prevention for your managed fleet by integrating with third-party DNS filtering providers and enforcing allowlist/blocklist policies. The system ingests DNS security events from connected providers, categorizes threats, logs blocked queries, and gives you aggregated statistics to understand threat exposure across your organization.

You manage DNS Security from the DNS Security page in the dashboard sidebar. The page has tabs for an overview, blocked-event feed, policy management, and provider integrations. The API endpoints below back the same workflows for scripts and automations.

Integrations connect Breeze to your existing DNS filtering infrastructure. Each integration is scoped to an organization and stores encrypted API credentials for the target provider. Once connected, Breeze periodically syncs DNS security events from the provider and stores them in the dns_security_events table, where they can be queried, filtered, and analyzed. Policies define domain-level allowlists and blocklists that are pushed back to the provider through a sync pipeline.


  1. Connect a DNS filter provider by creating an integration with the provider’s API credentials. Breeze encrypts all secrets at rest.

  2. Breeze syncs events from the provider on a configurable interval. Each DNS query event is stored with its action (allowed, blocked, redirected), threat category, source device, and domain.

  3. Create policies to define allowlists and blocklists. Policies are linked to a specific integration and contain domain entries with optional reasons.

  4. Policies sync to the provider through BullMQ. When you add or remove domains, a sync job is enqueued to push changes upstream to the DNS provider.

  5. Analyze threat data using the stats, top-blocked, and events endpoints. Breeze automatically aggregates data for efficient querying over long time ranges.

Action Description
allowed DNS query was resolved normally
blocked DNS query was blocked by the provider’s filtering rules
redirected DNS query was redirected to a different address (e.g., a block page or sinkhole)
Category Description
malware Domain associated with malware distribution or command-and-control infrastructure
phishing Domain used in phishing campaigns to harvest credentials or personal data
botnet Domain linked to botnet command-and-control networks
cryptomining Domain associated with unauthorized cryptocurrency mining
ransomware Domain used in ransomware delivery or payment infrastructure
spam Domain associated with spam campaigns
adware Domain serving intrusive advertising or adware payloads
adult_content Domain hosting adult or explicit content
gambling Domain hosting online gambling services
social_media Social media platform (typically used for content filtering policies)
streaming Streaming media platform (typically used for bandwidth management policies)
unknown Category not determined by the provider

Breeze supports the following DNS filter providers:

Provider Enum Value Required Config
Cisco Umbrella umbrella apiKey, apiSecret, config.organizationId
Cloudflare Gateway cloudflare apiKey, config.accountId
DNSFilter dnsfilter apiKey
Pi-hole pihole apiKey, config.apiEndpoint, optional config.piholeVersion
AdGuard Home adguard_home apiKey, apiSecret, config.apiEndpoint
OpenDNS opendns apiKey
Quad9 quad9 apiKey

For AdGuard Home, apiKey is the admin username and apiSecret is the admin password (the provider uses HTTP Basic Auth). config.apiEndpoint is the base URL of your AdGuard Home instance (e.g., https://adguard.example.com).

Pi-hole supports both the legacy v5 admin API and the newer v6 session-based API. Set config.piholeVersion to v5 or v6 to match your Pi-hole installation’s major version.

The meaning of apiKey also changes between versions: for Pi-hole v5, apiKey is the legacy admin API token (found under the Pi-hole web interface’s API/Web settings). For Pi-hole v6, apiKey instead holds the app password — a separate v6-specific credential generated in Pi-hole’s admin settings for API access. These two credential types are not interchangeable — pasting a v5 admin token into a v6 integration (or an app password into a v5 integration) will cause authentication to fail.

Terminal window
POST /dns-security/integrations
Content-Type: application/json
Authorization: Bearer <token>
{
"orgId": "uuid",
"provider": "umbrella",
"name": "Contoso Umbrella",
"description": "Cisco Umbrella for Contoso HQ",
"apiKey": "your-api-key",
"apiSecret": "your-api-secret",
"config": {
"organizationId": "umbrella-org-id",
"syncInterval": 60,
"retentionDays": 90,
"categories": ["malware", "phishing", "botnet"]
},
"isActive": true
}
Field Type Description
orgId UUID Organization ID. Auto-resolved for org-scoped tokens
provider string DNS provider: umbrella, cloudflare, dnsfilter, pihole, adguard_home, opendns, quad9
name string Human-readable name (max 200 chars)
description string Optional description (max 2,000 chars)
apiKey string Provider API key (required, max 5,000 chars). For Pi-hole, the expected value depends on config.piholeVersion — see below
apiSecret string Provider API secret (required for Cisco Umbrella)
config.organizationId string Provider’s organization ID (required for Cisco Umbrella)
config.accountId string Provider’s account ID (required for Cloudflare Gateway)
config.apiEndpoint string Custom API endpoint URL (required for Pi-hole)
config.piholeVersion string Pi-hole API version: v5 or v6. Defaults to v5 if unset. Determines whether apiKey is treated as the legacy admin token (v5) or the app password (v6)
config.syncInterval integer Event sync interval in minutes (5-1440, default varies by provider)
config.retentionDays integer Event retention period in days (1-365)
config.categories string[] Threat categories to sync (max 100 entries)
config.blocklistId string Provider-specific blocklist identifier for policy sync
config.allowlistId string Provider-specific allowlist identifier for policy sync
isActive boolean Whether the integration is active (default true)

To immediately sync events from a provider instead of waiting for the next scheduled interval:

Terminal window
POST /dns-security/integrations/:id/sync

The response includes a jobId for tracking the sync task in BullMQ.

Terminal window
DELETE /dns-security/integrations/:id

Policies define domain-level allowlists and blocklists that are enforced through the connected DNS provider. Each policy is tied to a specific integration and organization.

Terminal window
POST /dns-security/policies
Content-Type: application/json
Authorization: Bearer <token>
{
"orgId": "uuid",
"integrationId": "uuid",
"name": "Malware Blocklist",
"description": "Block known malicious domains",
"type": "blocklist",
"domains": [
{ "domain": "malware-example.com", "reason": "Known C2 domain" },
{ "domain": "phishing-site.net", "reason": "Reported phishing" }
],
"categories": ["malware", "phishing", "botnet"],
"isActive": true
}
Field Type Required Description
orgId UUID No Organization ID. Auto-resolved for org-scoped tokens
integrationId UUID Yes The DNS filter integration this policy applies to
name string Yes Human-readable name (max 200 chars)
description string No Optional description (max 2,000 chars)
type string Yes blocklist or allowlist
domains array No Domain entries (max 500). Each with domain (required) and reason (optional)
categories string[] No Threat categories to include in this policy (max 50)
isActive boolean No Whether the policy is active (default true)

When a policy is created or updated, its syncStatus is set to pending. A BullMQ job is enqueued to push the changes to the DNS provider. The status progresses as follows:

Status Meaning
pending Changes queued, waiting for sync to the provider
synced Policy successfully pushed to the provider
error Sync failed. Check syncError for details

Add or remove domains from an existing policy without replacing the entire domain list:

Terminal window
PATCH /dns-security/policies/:id/domains
Content-Type: application/json
{
"add": [
{ "domain": "new-bad-domain.com", "reason": "Reported by threat intel feed" }
],
"remove": ["old-domain.net"]
}

At least one domain must be provided in add or remove. Domains are normalized to lowercase with trailing dots stripped. Duplicate domains are silently ignored. The response includes the updated domain count and lists of successfully added and removed domains.


DNS security events are the individual records of DNS queries processed by your DNS filter provider. Each event captures the queried domain, the action taken, the threat category (if applicable), and the source device.

Terminal window
GET /dns-security/events?start=2026-02-01T00:00:00Z&end=2026-02-15T00:00:00Z&action=blocked&category=malware&limit=50
Parameter Type Description
start ISO 8601 Start of time window (inclusive)
end ISO 8601 End of time window (inclusive)
action string Filter by action: allowed, blocked, redirected
category string Filter by threat category
domain string Partial domain match (case-insensitive)
deviceId UUID Filter events for a specific device
integrationId UUID Filter events from a specific integration
limit integer Results per page (1-500, default 100)
offset integer Pagination offset (default 0)

Each event in the response includes:

Field Type Description
id UUID Unique event identifier
orgId UUID Organization ID
integrationId UUID Source integration
deviceId UUID Linked device (nullable if device not matched)
deviceHostname string Hostname of the linked device (from join)
timestamp ISO 8601 When the DNS query occurred
domain string Queried domain name
queryType string DNS query type (e.g., A, AAAA, CNAME)
action string allowed, blocked, or redirected
category string Threat category (nullable)
threatType string Provider-specific threat classification
sourceIp string IP address of the querying device
sourceHostname string Hostname of the querying device (from provider)
providerEventId string Unique event ID from the DNS provider
metadata object Additional provider-specific data

When a blocked event arrives with a known threat category (malware, phishing, botnet, ransomware, cryptomining), Breeze automatically raises a high-severity alert for the affected device. You don’t need a rule for this — the alert fires from the DNS Security pipeline itself.

To avoid alert storms when the same device hits the same threat category repeatedly, alerts are deduplicated per (device, category) for a 60-minute cooldown window. A second malware block on the same device within the window updates the existing alert’s “last seen” timestamp rather than raising a new one.

Threat events are also published on the internal event bus so other consumers (custom webhooks, future integrations) can subscribe to dns.threat.blocked without polling the events endpoint.


Get aggregated DNS security statistics including total queries, blocked rates, top blocked domains, top categories, and top offending devices:

Terminal window
GET /dns-security/stats?start=2026-02-01T00:00:00Z&end=2026-02-15T00:00:00Z&topN=20
Parameter Type Description
start ISO 8601 Start of time window
end ISO 8601 End of time window
integrationId UUID Filter by integration
deviceId UUID Filter by device
action string Filter by action type
category string Filter by threat category
topN integer Number of top entries to return (1-100, default 10)

The response includes:

{
"summary": {
"totalQueries": 125000,
"blockedQueries": 3200,
"allowedQueries": 121500,
"redirectedQueries": 300,
"blockedRate": 2.56
},
"topBlockedDomains": [...],
"topCategories": [...],
"topDevices": [...],
"source": "aggregated"
}

Get a ranked list of the most frequently blocked domains:

Terminal window
GET /dns-security/top-blocked?start=2026-02-01T00:00:00Z&end=2026-02-15T00:00:00Z&limit=20

Each entry includes the domain, its threat category, the block count, and the number of distinct devices that attempted to reach it.


Method Path Description
GET /dns-security/integrations List all integrations for the organization
POST /dns-security/integrations Create a new DNS filter integration (requires MFA, orgs:write)
DELETE /dns-security/integrations/:id Delete integration and all associated data (requires MFA, orgs:write)
POST /dns-security/integrations/:id/sync Trigger immediate event sync from the provider
Method Path Description
GET /dns-security/events List DNS security events with filtering and pagination
Method Path Description
GET /dns-security/stats Aggregated statistics: totals, block rates, top domains/categories/devices
GET /dns-security/top-blocked Ranked list of most frequently blocked domains
Method Path Description
GET /dns-security/policies List all DNS policies for the organization
POST /dns-security/policies Create a new allowlist or blocklist policy (requires orgs:write)
PATCH /dns-security/policies/:id/domains Add or remove domains from a policy (requires orgs:write)

Integration created but events are not appearing. After creating an integration, an initial sync job is enqueued via BullMQ. If the sync scheduling fails, the creation response includes a warning field explaining the issue. Verify that Redis and BullMQ workers are running. Check the integration’s lastSyncStatus and lastSyncError fields via GET /dns-security/integrations to see if the sync completed or encountered an error. Also confirm that the API key and provider configuration are correct.

Policy sync stuck in pending status. Policy syncs are dispatched through BullMQ. If BullMQ is down or backlogged, the sync will remain in pending state. Restart the DNS sync worker and check the syncError field on the policy for specific error details. If the provider API returns an authentication error, verify the integration’s API credentials.

Stats endpoint returning unexpected results. For time windows longer than 7 days, statistics are sourced from the dns_event_aggregations table rather than raw events. Aggregations are computed periodically and may lag behind real-time data. The source field in the response (raw or aggregated) indicates which data source was used. For the most current data over short periods, use a time window of 7 days or less.

Time range query rejected with 400 error. The maximum query window for events and statistics is 90 days. If your start-to-end range exceeds this limit, narrow the time range. Also verify that both start and end are valid ISO 8601 timestamps and that start is before end.

Domains not blocking after adding to a blocklist policy. After adding domains to a policy, verify the syncStatus is synced (not pending or error). If the sync failed, check the syncError field. Domains are normalized to lowercase with trailing dots removed before storage and sync. If the provider requires a specific domain format, verify it matches the normalized form. Note that DNS caching at the client or resolver level may delay enforcement of new rules.

MFA required error when creating or deleting integrations. Creating and deleting DNS filter integrations require MFA verification. Ensure the authenticated user has completed MFA for the current session. The requireMfa() middleware returns a 403 if MFA has not been verified.

Pi-hole v6 sync fails with HTTP 429 and no_seats. Pi-hole v6’s admin API is session-based and only allows a limited number of concurrent API sessions (“seats”). If a sync attempt fails with HTTP 429 and an error body containing no_seats, all available session seats are currently in use — this is not a generic rate limit, so retrying immediately usually won’t help. This typically happens when other clients (Pi-hole’s own admin web UI, another integration, or a stale session that hasn’t expired yet) are holding the remaining seats. Wait for existing sessions to expire, close other concurrent connections to the Pi-hole API, and then retry the sync. If this happens repeatedly, consider reducing how many other tools or browser tabs stay logged into the Pi-hole admin interface at the same time.