Skip to main content

Route53 A Record Does Not Point to a Dangling IP Address

Overview

This check identifies Route 53 DNS "A" records that point to IP addresses no longer owned by your AWS account. These are called "dangling" DNS records because they point to addresses that have been released back to AWS's IP pool.

When you delete an AWS resource (like an EC2 instance or load balancer), the public IP address it used gets released. If you forget to update or remove the DNS record pointing to that IP, the record becomes "dangling" - it points to an address you no longer control.

Risk

Dangling DNS records create a serious security vulnerability called subdomain takeover. Here is what can happen:

  1. An attacker obtains your old IP: AWS reassigns released IPs to other customers. An attacker could spin up resources until they get your former IP address.

  2. They now control your subdomain: With your DNS still pointing to that IP, any traffic to your subdomain goes to the attacker's server.

  3. Potential damage:

    • Credential theft: Users logging into what they think is your site give credentials to attackers
    • Cookie hijacking: Session cookies sent to the attacker's server
    • Phishing: Attackers serve fake content that appears to come from your domain
    • Reputation damage: Malicious content hosted under your domain name

Severity: High

Remediation Steps

Prerequisites

You need:

  • Access to the AWS Console with permissions to edit Route 53 records, OR
  • AWS CLI configured with appropriate credentials
Required IAM permissions

Your IAM user or role needs these permissions:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"route53:ListHostedZones",
"route53:ListResourceRecordSets",
"route53:ChangeResourceRecordSets"
],
"Resource": "*"
}
]
}

AWS Console Method

Step 1: Find the problematic record

  1. Go to the Route 53 Console
  2. Click Hosted zones in the left menu
  3. Click on the hosted zone containing the flagged record
  4. Look for the A record identified in the Prowler finding

Step 2: Decide what to do

You have two options:

  • Delete the record if you no longer need this DNS entry
  • Update the record if you still need it but want to point it somewhere valid

Step 3a: Delete the record (if no longer needed)

  1. Select the checkbox next to the A record
  2. Click Delete record
  3. Confirm the deletion

Step 3b: Update to an Alias record (recommended if still needed)

Using an Alias record instead of a hardcoded IP prevents this problem in the future because the Alias automatically tracks the target resource.

  1. Select the A record and click Edit record
  2. Toggle Alias to ON
  3. Choose the Route traffic to destination:
    • For an ALB/NLB: Select "Alias to Application and Classic Load Balancer" or "Alias to Network Load Balancer"
    • For CloudFront: Select "Alias to CloudFront distribution"
    • For another AWS resource: Select the appropriate option
  4. Select the region (us-east-1) and the specific resource
  5. Click Save

Step 4: Wait for propagation

DNS changes typically propagate within 60 seconds for Route 53. Wait a minute, then verify the change.

AWS CLI (optional)

List your hosted zones

aws route53 list-hosted-zones --region us-east-1

List records in a specific hosted zone

aws route53 list-resource-record-sets \
--hosted-zone-id <your-hosted-zone-id> \
--region us-east-1

Delete a dangling A record

aws route53 change-resource-record-sets \
--hosted-zone-id <your-hosted-zone-id> \
--region us-east-1 \
--change-batch '{
"Changes": [{
"Action": "DELETE",
"ResourceRecordSet": {
"Name": "<record-name>",
"Type": "A",
"TTL": 300,
"ResourceRecords": [{"Value": "<dangling-ip-address>"}]
}
}]
}'

Replace:

  • <your-hosted-zone-id> with your hosted zone ID (e.g., Z1234567890ABC)
  • <record-name> with the DNS name (e.g., app.example.com)
  • <dangling-ip-address> with the IP from the failing check

Convert to an Alias record (pointing to an ALB)

aws route53 change-resource-record-sets \
--hosted-zone-id <your-hosted-zone-id> \
--region us-east-1 \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "<record-name>",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "<alb-hosted-zone-id>",
"DNSName": "<alb-dns-name>",
"EvaluateTargetHealth": false
}
}
}]
}'

Replace:

  • <your-hosted-zone-id> with your Route 53 hosted zone ID
  • <record-name> with the DNS name (e.g., app.example.com)
  • <alb-hosted-zone-id> with the ALB's hosted zone ID (see AWS ALB hosted zone IDs)
  • <alb-dns-name> with your ALB's DNS name (e.g., my-alb-1234567890.us-east-1.elb.amazonaws.com)
CloudFormation (optional)

Use this CloudFormation template to create an Alias A record that points to an AWS resource instead of a hardcoded IP:

AWSTemplateFormatVersion: '2010-09-09'
Description: Route53 Alias record to prevent dangling IP

Parameters:
HostedZoneId:
Type: String
Description: The ID of the hosted zone (e.g., Z1234567890ABC)

RecordName:
Type: String
Description: The DNS record name (e.g., app.example.com)

AliasTargetHostedZoneId:
Type: String
Description: The hosted zone ID of the alias target (e.g., ALB hosted zone)

AliasTargetDNSName:
Type: String
Description: The DNS name of the alias target (e.g., ALB DNS name)

Resources:
DNSRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId: !Ref HostedZoneId
Name: !Ref RecordName
Type: A
AliasTarget:
HostedZoneId: !Ref AliasTargetHostedZoneId
DNSName: !Ref AliasTargetDNSName
EvaluateTargetHealth: false

Deploy the template:

aws cloudformation deploy \
--template-file template.yaml \
--stack-name route53-alias-record \
--region us-east-1 \
--parameter-overrides \
HostedZoneId=<your-hosted-zone-id> \
RecordName=<record-name> \
AliasTargetHostedZoneId=<alb-hosted-zone-id> \
AliasTargetDNSName=<alb-dns-name>
Terraform (optional)

Use this Terraform configuration to create an Alias A record:

variable "hosted_zone_id" {
description = "The Route53 hosted zone ID"
type = string
}

variable "record_name" {
description = "The DNS record name (e.g., app.example.com)"
type = string
}

variable "alias_target_dns_name" {
description = "The DNS name of the alias target (e.g., ALB DNS name)"
type = string
}

variable "alias_target_zone_id" {
description = "The hosted zone ID of the alias target"
type = string
}

resource "aws_route53_record" "alias_record" {
zone_id = var.hosted_zone_id
name = var.record_name
type = "A"

alias {
name = var.alias_target_dns_name
zone_id = var.alias_target_zone_id
evaluate_target_health = false
}
}

Apply the configuration:

terraform init
terraform plan -var="hosted_zone_id=<your-zone-id>" \
-var="record_name=<your-record>" \
-var="alias_target_dns_name=<alb-dns>" \
-var="alias_target_zone_id=<alb-zone-id>"
terraform apply

Verification

After making changes, verify the fix:

  1. In the AWS Console: Go back to Route 53 > Hosted zones and confirm the record is either deleted or shows as an Alias record (indicated by an "A" with "Alias" badge)

  2. Re-run the Prowler check:

    prowler aws --check route53_dangling_ip_subdomain_takeover
Advanced verification

Check DNS resolution:

# Verify the record resolves correctly (or is removed)
nslookup <record-name>

# Or use dig for more detail
dig <record-name> A

Verify via AWS CLI:

aws route53 list-resource-record-sets \
--hosted-zone-id <your-hosted-zone-id> \
--region us-east-1 \
--query "ResourceRecordSets[?Name=='<record-name>.']"

Additional Resources

Notes

  • Prevention is better than cure: Always use Alias records instead of hardcoded IPs when pointing to AWS resources. Alias records automatically update when the underlying resource changes.

  • Elastic IPs are safer: If you must use an IP address, consider using an Elastic IP that you explicitly allocate and control. Elastic IPs remain yours until you release them.

  • Audit regularly: Set up a regular process to reconcile DNS records with actual resources. Prowler can be run on a schedule to catch dangling records.

  • This check only finds AWS IPs: The check specifically looks for IPs in AWS's public IP ranges that are not owned by your account. Non-AWS IPs are not flagged by this check.

  • Private IPs are not checked: This check only applies to public IPs. Private IP addresses in A records are not evaluated since they cannot be taken over externally.