Detection rules › Kusto
GCP Audit Logs - DNSSEC Disabled on Managed DNS Zone
Detects when DNSSEC (DNS Security Extensions) is disabled on a Google Cloud DNS managed zone. DNSSEC provides cryptographic authentication of DNS data, preventing DNS spoofing and cache poisoning attacks. Adversaries may disable DNSSEC to enable DNS-based command and control, phishing campaigns, or to redirect traffic to malicious infrastructure without cryptographic validation. This rule monitors DNS zone patch operations where DNSSEC state changes from ON to OFF.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Resource Development | T1584.002 Compromise Infrastructure: DNS Server |
| Stealth | T1562.001 Impair Defenses: Disable or Modify Tools |
| Command & Control | T1071.004 Application Layer Protocol: DNS |
Event coverage
| Provider | Event | Title |
|---|---|---|
| GCP-dns.googleapis.com | Dns-ManagedZones-Patch | Patch managed zone |
| GCP-dns.googleapis.com | Dns-ManagedZones-Update | Update managed zone |
Rule body kusto
id: 9129a43e-e204-4a9a-969e-d8861ce3437c
name: GCP Audit Logs - DNSSEC Disabled on Managed DNS Zone
description: |
'Detects when DNSSEC (DNS Security Extensions) is disabled on a Google Cloud DNS managed zone.
DNSSEC provides cryptographic authentication of DNS data, preventing DNS spoofing and cache poisoning attacks.
Adversaries may disable DNSSEC to enable DNS-based command and control, phishing campaigns, or
to redirect traffic to malicious infrastructure without cryptographic validation.
This rule monitors DNS zone patch operations where DNSSEC state changes from ON to OFF.'
severity: High
status: Available
requiredDataConnectors:
- connectorId: GCPAuditLogsDefinition
dataTypes:
- GCPAuditLogs
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- DefenseEvasion
- CommandAndControl
- ResourceDevelopment
relevantTechniques:
- T1562.001
- T1071.004
- T1584.002
tags:
- GCP
- DNS
- DNSSEC
- Cloud Security
query: |
GCPAuditLogs
| where ServiceName == "dns.googleapis.com"
| where MethodName in ("dns.managedZones.update", "dns.managedZones.patch")
| where GCPResourceType == "dns_managed_zone" and Severity == "NOTICE"
| extend
ResponseJson = parse_json(Response),
RequestMetadataJson = parse_json(RequestMetadata),
AuthInfoJson = parse_json(AuthenticationInfo)
| extend ZoneContext = ResponseJson.operation.zoneContext
| where isnotempty(ZoneContext)
| extend
OldDnsSecState = tostring(ZoneContext.oldValue.dnssecConfig.state),
NewDnsSecState = tostring(ZoneContext.newValue.dnssecConfig.state)
| where OldDnsSecState == "ON" and NewDnsSecState == "OFF"
| extend
ManagedZoneName = extract(@"managedZones/([^/]+)", 1, GCPResourceName),
DnsName = tostring(ResponseJson.managedZone.dnsName),
ZoneId = tostring(ResponseJson.managedZone.id),
ZoneDescription = tostring(ResponseJson.managedZone.description),
Visibility = tostring(ResponseJson.managedZone.visibility),
OperationId = tostring(ResponseJson.operation.id),
CallerIpAddress = tostring(RequestMetadataJson.callerIp),
AuthEmail = tostring(AuthInfoJson.principalEmail)
| extend
AccountName = tostring(split(PrincipalEmail, "@")[0]),
AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
| project TimeGenerated,
PrincipalEmail,
AuthEmail,
ProjectId,
ManagedZoneName,
DnsName,
ResourceName = GCPResourceName,
Visibility,
ZoneId,
ZoneDescription,
OperationId,
CallerIpAddress,
MethodName,
ServiceName,
Severity,
LogName,
InsertId,
AccountName,
AccountUPNSuffix
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: PrincipalEmail
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
- entityType: CloudApplication
fieldMappings:
- identifier: Name
columnName: ProjectId
- identifier: InstanceName
columnName: ResourceName
- entityType: DNS
fieldMappings:
- identifier: DomainName
columnName: DnsName
customDetails:
ProjectId: ProjectId
ManagedZoneName: ManagedZoneName
DnsName: DnsName
ResourceName: ResourceName
Visibility: Visibility
ZoneId: ZoneId
alertDetailsOverride:
alertDisplayNameFormat: "DNSSEC Disabled on DNS Zone {{ManagedZoneName}} ({{DnsName}}) by {{PrincipalEmail}}"
alertDescriptionFormat: |-
User {{PrincipalEmail}} disabled DNSSEC on DNS managed zone {{ManagedZoneName}} ({{DnsName}}).
This action removes cryptographic validation of DNS responses and may indicate an attempt to facilitate DNS-based attacks.
Investigate immediately to determine if this change was authorized and assess potential security impact.
Review DNS query logs for suspicious activity and consider re-enabling DNSSEC if unauthorized.
version: 1.0.0
kind: Scheduled
Stages and Predicates
Stage 1: source
GCPAuditLogs
Stage 2: where
| where ServiceName == "dns.googleapis.com"
Stage 3: where
| where MethodName in ("dns.managedZones.update", "dns.managedZones.patch")
Stage 4: where
| where GCPResourceType == "dns_managed_zone" and Severity == "NOTICE"
Stage 5: extend
| extend
ResponseJson = parse_json(Response),
RequestMetadataJson = parse_json(RequestMetadata),
AuthInfoJson = parse_json(AuthenticationInfo)
Stage 6: extend
| extend ZoneContext = ResponseJson.operation.zoneContext
Stage 7: where
| where isnotempty(ZoneContext)
Stage 8: extend
| extend
OldDnsSecState = tostring(ZoneContext.oldValue.dnssecConfig.state),
NewDnsSecState = tostring(ZoneContext.newValue.dnssecConfig.state)
Stage 9: where
| where OldDnsSecState == "ON" and NewDnsSecState == "OFF"
Stage 10: extend
| extend
ManagedZoneName = extract(@"managedZones/([^/]+)", 1, GCPResourceName),
DnsName = tostring(ResponseJson.managedZone.dnsName),
ZoneId = tostring(ResponseJson.managedZone.id),
ZoneDescription = tostring(ResponseJson.managedZone.description),
Visibility = tostring(ResponseJson.managedZone.visibility),
OperationId = tostring(ResponseJson.operation.id),
CallerIpAddress = tostring(RequestMetadataJson.callerIp),
AuthEmail = tostring(AuthInfoJson.principalEmail)
Stage 11: extend
| extend
AccountName = tostring(split(PrincipalEmail, "@")[0]),
AccountUPNSuffix = tostring(split(PrincipalEmail, "@")[1])
Stage 12: project
| project TimeGenerated,
PrincipalEmail,
AuthEmail,
ProjectId,
ManagedZoneName,
DnsName,
ResourceName = GCPResourceName,
Visibility,
ZoneId,
ZoneDescription,
OperationId,
CallerIpAddress,
MethodName,
ServiceName,
Severity,
LogName,
InsertId,
AccountName,
AccountUPNSuffix
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
GCPResourceType | eq |
|
MethodName | in |
|
NewDnsSecState | eq |
|
OldDnsSecState | eq |
|
ServiceName | eq |
|
Severity | eq |
|
ZoneContext | is_not_null |
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
AccountName | project |
AccountUPNSuffix | project |
AuthEmail | project |
CallerIpAddress | project |
DnsName | project |
InsertId | project |
LogName | project |
ManagedZoneName | project |
MethodName | project |
OperationId | project |
PrincipalEmail | project |
ProjectId | project |
ResourceName | project |
ServiceName | project |
Severity | project |
TimeGenerated | project |
Visibility | project |
ZoneDescription | project |
ZoneId | project |