Fixing Route53 CloudWatch Logs Delivery Access Denied: DNS Query Logging Broken
Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins
TL;DR
- What broke: Route53 cannot deliver DNS query logs to CloudWatch Logs because the log group's resource-based policy either doesn't exist, excludes
route53.amazonaws.comas a principal, or has a malformed ARN condition — the delivery silently fails withAccessDeniedException. - How to fix it: Create or update the CloudWatch Logs resource policy to explicitly grant
logs:CreateLogStreamandlogs:PutLogEventsto theroute53.amazonaws.comservice principal, scoped to your target log group ARN. - Fast path: Use the Client-Side Sandbox above to paste your current resource policy and auto-generate the corrected version without sending your ARNs to a third-party server.
The Incident (What Does the Error Mean?)
When you enable Route53 query logging via console or CLI, Route53 attempts to write DNS query records to the specified CloudWatch Logs log group. The delivery agent runs under the route53.amazonaws.com service principal — not your IAM role. If the log group's resource policy doesn't explicitly authorize that principal, every DNS query log write is silently dropped.
The error surfaces in the Route53 console as:
Error: CloudWatch Logs delivery access denied.
Route53 does not have permission to write to the log group:
arn:aws:logs:us-east-1:123456789012:log-group:/aws/route53/example.com
AccessDeniedException: User: arn:aws:sts::123456789012:assumed-role/...
is not authorized to perform: logs:PutLogEvents
on resource: arn:aws:logs:us-east-1:123456789012:log-group:/aws/route53/example.com
Immediate consequence: Zero DNS query logs are being written. Your security audit trail, anomaly detection pipelines, and compliance evidence for DNS-layer activity are completely blind from the moment this misconfiguration exists.
The Attack Vector / Blast Radius
This isn't just a logging gap — it's a detection blind spot. Route53 DNS query logs are the primary data source for:
- DNS exfiltration detection — attackers tunneling data via crafted subdomains (e.g.,
base64payload.evil.attacker.com) are invisible without query logs. - C2 beaconing identification — periodic DNS lookups to DGA (Domain Generation Algorithm) domains go undetected.
- GuardDuty DNS threat findings — GuardDuty's
Backdoor:EC2/DNSDataExfiltrationandTrojan:EC2/DNSDataExfiltrationfinding types depend on Route53 query logs being delivered. A broken delivery pipeline disables these findings entirely. - Compliance failures — PCI-DSS 10.2, SOC 2 CC7.2, and HIPAA audit controls requiring DNS-layer logging are violated silently.
The blast radius: every EC2 instance, Lambda, and ECS task in the VPC making DNS queries is unmonitored. An attacker who compromises one workload can exfiltrate data via DNS for days without triggering a single alert.
How to Fix It
Root Cause
CloudWatch Logs resource policies are not automatically created when you configure Route53 query logging. Route53 requires an explicit resource policy on the log group granting the route53.amazonaws.com service principal write access. Additionally, Route53 query logging only works with log groups in us-east-1, regardless of your hosted zone's region — a second common failure point.
Basic Fix — Apply the Resource Policy via AWS CLI
- # Missing or broken resource policy (no route53 principal)
- {
- "Version": "2012-10-17",
- "Statement": []
- }
+ # Correct resource policy granting Route53 delivery access
+ {
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Sid": "Route53QueryLoggingDelivery",
+ "Effect": "Allow",
+ "Principal": {
+ "Service": "route53.amazonaws.com"
+ },
+ "Action": [
+ "logs:CreateLogStream",
+ "logs:PutLogEvents"
+ ],
+ "Resource": "arn:aws:logs:us-east-1:123456789012:log-group:/aws/route53/*:*"
+ }
+ ]
+ }
Apply it:
aws logs put-resource-policy \
--policy-name "AmazonRoute53LogsToCloudWatchLogs" \
--policy-document file://route53-cwl-resource-policy.json \
--region us-east-1
⚠️ Critical: This command must be run against
us-east-1regardless of where your hosted zone is. Route53 query log delivery always targetsus-east-1CloudWatch endpoints.
Enterprise Best Practice — Terraform with Least-Privilege ARN Scoping
- # Overly broad or missing resource policy in Terraform
- resource "aws_cloudwatch_log_resource_policy" "route53" {
- policy_name = "route53-logging"
- policy_document = jsonencode({
- Version = "2012-10-17"
- Statement = [{
- Effect = "Allow"
- Principal = { Service = "*" } # NEVER do this
- Action = ["logs:*"] # NEVER do this
- Resource = "*"
- }]
- })
- }
+ resource "aws_cloudwatch_log_group" "route53_query_logs" {
+ name = "/aws/route53/${var.hosted_zone_name}"
+ retention_in_days = 90
+ provider = aws.us_east_1 # MUST be us-east-1
+
+ tags = {
+ ManagedBy = "terraform"
+ Purpose = "route53-query-logging"
+ Compliance = "pci-dns-audit"
+ }
+ }
+
+ resource "aws_cloudwatch_log_resource_policy" "route53_query_logging" {
+ policy_name = "AmazonRoute53LogsToCloudWatchLogs-${var.hosted_zone_name}"
+ provider = aws.us_east_1
+
+ policy_document = jsonencode({
+ Version = "2012-10-17"
+ Statement = [{
+ Sid = "Route53QueryLoggingDelivery"
+ Effect = "Allow"
+ Principal = {
+ Service = "route53.amazonaws.com"
+ }
+ Action = [
+ "logs:CreateLogStream",
+ "logs:PutLogEvents"
+ ]
+ Resource = [
+ "${aws_cloudwatch_log_group.route53_query_logs.arn}:*"
+ ]
+ Condition = {
+ StringEquals = {
+ "aws:SourceAccount" = var.aws_account_id
+ }
+ ArnLike = {
+ "aws:SourceArn" = "arn:aws:route53:::hostedzone/${var.hosted_zone_id}"
+ }
+ }
+ }]
+ })
+ }
+
+ resource "aws_route53_query_log" "main" {
+ depends_on = [aws_cloudwatch_log_resource_policy.route53_query_logging]
+
+ cloudwatch_log_group_arn = aws_cloudwatch_log_group.route53_query_logs.arn
+ zone_id = var.hosted_zone_id
+ }
The Condition block with aws:SourceAccount and aws:SourceArn prevents the confused deputy problem — another AWS account's Route53 cannot write to your log group.
💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.
Prevention in CI/CD
Checkov — Catch Missing Resource Policies Pre-Merge
Checkov rule CKV_AWS_84 checks that Route53 query logging is enabled on hosted zones, but doesn't validate the resource policy. Add a custom check:
# .checkov/custom_checks/route53_cwl_resource_policy.py
# Validate that a CloudWatch Logs resource policy exists for route53.amazonaws.com
# Run: checkov -d . --external-checks-dir .checkov/custom_checks
OPA / Conftest Policy
# policy/route53_logging.rego
package route53
deny[msg] {
resource := input.resource.aws_route53_query_log[_]
not input.resource.aws_cloudwatch_log_resource_policy
msg := "Route53 query log config exists without a CloudWatch Logs resource policy. DNS audit trail will be dark."
}
deny[msg] {
policy := input.resource.aws_cloudwatch_log_resource_policy[_]
statement := policy.policy_document.Statement[_]
statement.Principal.Service != "route53.amazonaws.com"
msg := "CloudWatch Logs resource policy does not grant access to route53.amazonaws.com service principal."
}
GitHub Actions Gate
- name: Validate Route53 Logging Policy
run: |
aws logs describe-resource-policies \
--region us-east-1 \
--query "resourcePolicies[?contains(policyDocument, 'route53.amazonaws.com')] | length(@)" \
--output text | grep -v '^0$' || \
(echo "FATAL: No CloudWatch Logs resource policy found for Route53" && exit 1)
Wire this into your pre-deploy stage. A zero return from that query means your DNS logging is broken before you even push to production.