Reverse DNS at scale for the entire AWS organization
Patrick Zink
AWS Lead Architect | 14x AWS Certified | AWS Ambassador | AWS Community Builder
Admittedly, reverse DNS is not the most exciting topic ever, but it is essential in certain situations, such as Kerberos authentication. Reverse DNS (rDNS) is the process of mapping an IP address back to a hostname, which is the opposite of the more common forward DNS lookup that maps a hostname to an IP address.
AWS automatically enables the "Autodefined rules for reverse DNS resolution" feature for all VPCs. This feature creates reverse DNS entries for all IP addresses within a VPC, formatted as follows: ip-192-168-1-40.eu-central-1.compute.internal. However, this is not sufficient for scenarios like Kerberos authentication, where host.example.com should resolve to, for example, 192.168.1.40, and the reverse lookup should resolve back to host.example.com instead of ip-192-168-1-40.eu-central-1.compute.internal.
The big challenge is how to automate the creation of PTR records for reverse DNS, ideally for an entire AWS Organization.
In this article, I will demonstrate a solution to automate the creation of reverse DNS entries (PTR) across an entire AWS Organization. Whenever an A-record is created in a Route 53 Private Hosted Zone, the corresponding PTR in the reverse lookup zone (in-addr.arpa) is automatically generated. These PTR records are accessible across the entire AWS Organization and, if desired, from on-premises environments as well.
Quick overview of the solution
Event-Based Update Process:
Scheduled Update Process
(This process is important to initially create all PTRs if A-records already exist in the AWS Organization)
Resolving PTR Records within the AWS Organization
Resolving PTR Records from On-Premises
Detailed Solution
Setting Up Route53 Private Hosted Zones for Reverse Lookup Zones
The Private Hosted Zones (PHZ) for reverse DNS lookups must follow the in-addr.arpa schema. This means the domain name is the IP address blocks in reverse order followed by .in-addr.arpa For example, 168.192.in-addr.arpa is the reverse lookup zone for 192.168.. Since CIDR notation cannot be used here, multiple PHZs need to be created if you want reverse lookup zones only for specific subnets like 192.168.1.0/24 and 192.168.2.0/24.
PHZ for 192.168.1.0/24:
PHZ for 192.168.2.0/24
ReverseLookup1921681:
Type: AWS::Route53::HostedZone
Properties:
HostedZoneConfig:
Comment: "Reverse Lookup Zone for 192.168.1.0/24"
Name: "1.168.192.in-addr.arpa"
VPCs:
- VPCId: !Ref NetworkVpc
VPCRegion: "eu-central-1"
Setting Up Route53 Resolver Rule and RAM Share
To enable VPCs to resolve reverse DNS entries (PTR), a Route53 resolver rule must be created in the network account. This rule will be shared across the entire organization using AWS Resource Access Manager (RAM). The resolver rules must be associated with each VPC that needs the capability to reverse resolve DNS names.
InboundResolverForwardingRuleReverseDNSLookup1921681:
Type: AWS::Route53Resolver::ResolverRule
Properties:
DomainName: "1.168.192.in-addr.arpa."
Name: "1-168-192.in-addr.arpa"
ResolverEndpointId: !Ref OutboundResolverEndpoint
RuleType: FORWARD
TargetIps:
- Ip:
Port: 53
- Ip:
Port: 53
ResolverForwardingRuleShare:
Type: AWS::RAM::ResourceShare
Properties:
AllowExternalPrincipals: False
Name: route-53-resolver-share
Principals:
- !Sub arn:aws:organizations::${OrgAccountId}:organization/${OrgId}
ResourceArns:
- !GetAtt InboundResolverForwardingRuleReverseDNSLookup1921681.Arn
Important! AWS Automatic Activation of "Autodefined Rules for Reverse DNS Resolution"
AWS automatically enables the "Autodefined rules for reverse DNS resolution" feature for all VPCs. This feature can be found under Route 53 -> Resolver -> VPCs.
领英推荐
Consequently, reverse DNS entries for all IP addresses within a VPC are automatically created, formatted as follows: ip-192-168-1-40.eu-central-1.compute.internal.
nslookup 192.168.1.40
192-168-1-40.in-addr.arpa name = ip-10-0-1-40.eu-central-1.compute.internal.
The issue here is that this automatic resolution can sometimes be preferred over the Route 53 resolver rule, preventing our PTRs from the Private Hosted Zone from being resolved. To ensure our PTRs from the Private Hosted Zone are resolved correctly, this feature should be disabled for all VPCs that want to use our reverse DNS solution. This can be automated using the following example Python Boto3 command, which can be executed for each VPC:
def update_resolver_config(vpcId, r53_client):
try:
resolverConfig = r53_client.get_resolver_config(ResourceId=vpcId)
if resolverConfig['ResolverConfig']['AutodefinedReverse'] != 'DISABLE':
r53_client.update_resolver_config(
ResourceId=vpcId,
AutodefinedReverseFlag='DISABLE'
)
print(f'Reverse DNS lookups disabled for VPC {vpcId}')
except ClientError as e:
print(f'Failed to update resolver config for VPC {vpcId}: {e}')
Creation of Inbound and Outbound Resolvers
The Route 53 Inbound and Outbound resolvers should be created in the network account. Additionally, the designated security group must have DNS port 53 open for both TCP and UDP traffic.
InboundResolverEndpoint:
Type: AWS::Route53Resolver::ResolverEndpoint
Properties:
Direction: INBOUND
IpAddresses:
- SubnetId: !Ref DnsSubnetA
Ip: !GetAtt IPAddressesCustomResource.FifthIpSubnetA
- SubnetId: !Ref DnsSubnetB
Ip: !GetAtt IPAddressesCustomResource.FifthIpSubnetB
Name: inbound-resolver-1
SecurityGroupIds:
- !Ref DnsSecurityGroup
OutboundResolverEndpoint:
Type: AWS::Route53Resolver::ResolverEndpoint
Properties:
Direction: OUTBOUND
IpAddresses:
- SubnetId: !Ref DnsSubnetA
Ip: !GetAtt IPAddressesCustomResource.SixthIpSubnetA
- SubnetId: !Ref DnsSubnetB
Ip: !GetAtt IPAddressesCustomResource.SixthIpSubnetB
Name: outbound-resolver-1
SecurityGroupIds:
- !Ref DnsSecurityGroup
DnsSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: String
GroupName: 'DnsSecurityGroup'
SecurityGroupEgress:
- IpProtocol: udp
FromPort: 53
ToPort: 53
CidrIp: 192.168.0.0/16
- IpProtocol: tcp
FromPort: 53
ToPort: 53
CidrIp: 192.168.0.0/16
SecurityGroupIngress:
- IpProtocol: udp
FromPort: 53
ToPort: 53
CidrIp: 192.168.0.0/16
- IpProtocol: tcp
FromPort: 53
ToPort: 53
CidrIp: 192.168.0.0/16
- IpProtocol: icmp
FromPort: -1
ToPort: -1
CidrIp: 192.168.0.0/16
VpcId: !Ref DnsVpc
Create EventBridge Rule in All Accounts
To forward the Route53 "ChangeResourceRecordSets" events from all accounts to the network account, an EventBridge rule needs to be created in each account. It is crucial to create this rule in the us-east-1 region because Route53 is a global service, and the CloudTrail events originate there. For the EventBridge target, the network account will be selected, and there is an option to choose a different region at this stage.
AutomatedPTRforReverseDNSlookup:
Type: "AWS::Events::Rule"
Condition: IsNotNetworkAccount
Properties:
Description: forwards the ChangeResourceRecordSets event to the Network Account
EventPattern:
source:
- "aws.route53"
detail-type:
- "AWS API Call via CloudTrail"
detail:
eventSource:
- "route53.amazonaws.com"
eventName:
- "ChangeResourceRecordSets"
Name: "AutomatedPTRforReverseDNSlookup"
Targets:
- Arn: !Sub "arn:aws:events:eu-central-1:${NetworkAccount}:event-bus/default"
Id: "AutomatedPTRforReverseDNSlookup"
RoleArn: !GetAtt EventBridgeRuleRole.Arn
EventBridgeRuleRole:
Type: "AWS::IAM::Role"
Properties:
RoleName: "EventBridgeRuleRole"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service:
- "events.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
RolePolicies:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: "EventBridgeRuleRolePolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "events:PutEvents"
Resource: !Sub "arn:aws:events:eu-central-1:${NetworkAccount}:event-bus/default"
Roles:
- Ref: "EventBridgeRuleRole"
Development of Central Update PTR Logic
In the Network Account, an EventBridge rule for "ChangeResourceRecordSets" events needs to be set up. The target for this rule should be an SQS queue. The Lambda function will be triggered by this SQS queue using an EventSourceMapping.
EventBridge rule for event-based and scheduled:
EventBridgeRule:
Type: "AWS::Events::Rule"
Properties:
Description: "Create a PTR record in reverse DNS zones whenever an A record in a PHZ is created, modified, or deleted."
State: ENABLED
EventPattern: !Sub
- |
{
"source": [
"aws.route53"
],
"detail-type": [
"AWS API Call via CloudTrail"
],
"detail": {
"eventSource": [
"route53.amazonaws.com"
],
"eventName": [
"ChangeResourceRecordSets"
]
}
}
- { eventNames: !Join [ '", "', !Ref Events ] }
Name: AutomatedPTRforReverseDNSlookup
Targets:
- Arn: !GetAtt SQSQueue.Arn
Id: AutomatedPTRforReverseDNSlookup
ScheduledEventBridgeRule:
Type: "AWS::Events::Rule"
Properties:
Description: "Checks once a day if all PTR Records are set up correctly"
State: ENABLED
ScheduleExpression: !Ref Schedule
Name: !Sub "AutomatedPTRforReverseDNSlookupScheduled"
Targets:
- Arn: !GetAtt SQSQueue.Arn
Id: AutomatedPTRforReverseDNSlookup
SQS Queue:
SQSQueue:
Type: "AWS::SQS::Queue"
Properties:
QueueName: AutomatedPTRforReverseDNSlookup
MessageRetentionPeriod: 1000
ReceiveMessageWaitTimeSeconds: 0
VisibilityTimeout: 950
SQSQueuePolicy:
Type: "AWS::SQS::QueuePolicy"
Properties:
PolicyDocument: !Sub |
{
"Version": "2012-10-17",
"Id": "${SQSQueue.Arn}/SQSDefaultPolicy",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sqs:SendMessage",
"Resource": "${SQSQueue.Arn}",
"Condition": {
"ArnEquals": {
"aws:SourceArn": [
"${CloudWatchRule.Arn}",
"${ScheduledCloudWatchRule.Arn}"
]
}
}
}
]
}
Queues:
- !Ref SQSQueue
Lambda Function, Lambda Role and EventSourceMapping:
LambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
FunctionName: AutomatedPTRforReverseDNSlookup
Description: Creates a PTR record in reverse DNS zones when an A record in a PHZ is created, modified, or deleted.
Handler: !Sub ${PythonFileName}.lambda_handler
Role: !GetAtt LambdaRole.Arn
MemorySize: 128
Timeout: 900
Runtime: "python3.12"
Code:
S3Bucket: !Ref S3Bucket
S3Key: !Ref S3Key
Environment:
Variables:
LambdaRole: !Ref CrossAccountLambdaRole
OrganizationAccountId: !Ref OrganizationAccount
ReverseLookupZone1921681Id: !Ref ReverseLookupZone1921681
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: AutomatedPTRforReverseDNSlookupRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: AutomatedPTRforReverseDNSlookupRolePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- logs:CreateLogGroup
Resource: "*"
- Effect: Allow
Action:
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- Fn::Sub: 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
- Effect: Allow
Action:
- sqs:ReceiveMessage
- sqs:DeleteMessage
- sqs:GetQueueAttributes
Resource: !Sub "arn:aws:sqs:${AWS::Region}:${AWS::AccountId}:AutomatedPTRforReverseDNSlookup"
- Effect: Allow
Action:
- sts:AssumeRole
Resource: !Sub "arn:aws:iam::*:role/${CrossAccountLambdaRole}"
- Effect: "Allow"
Action:
- route53:ChangeResourceRecordSets
- route53:ListResourceRecordSets
- route53:GetHostedZone
Resource:
- !Sub "arn:aws:route53:::hostedzone/${ReverseLookupZone1921681}"
- Effect: Allow
Action:
- logs:CreateLogDelivery
- logs:DeleteLogDelivery
Resource: "*"
EventSourceMapping:
Type: "AWS::Lambda::EventSourceMapping"
Properties:
BatchSize: 10
Enabled: true
EventSourceArn: !GetAtt SQSQueue.Arn
FunctionName: !Ref LambdaFunction
The Lambda function processes CloudTrail events for the "ChangeResourceRecordSets" action. It checks if the event involves an A-record and verifies whether the IP address is within one of the predefined Route53 Private Hosted Zones (PHZ) for reverse lookup. If the IP address matches, the function creates or deletes the corresponding PTR record based on the event action.
Additionally, the Lambda processes a scheduled event triggered daily by EventBridge. This scheduled event iterates through all accounts in the AWS Organization to find Route53 Private Hosted Zones. It collects all A-records into a Python list. At the end of the iteration, the Lambda checks if a PTR record exists for each A-record found; if not, it creates one.
Below is the Python code for the Lambda function:
import json
import boto3
import os
from ipaddress import ip_address
from botocore.exceptions import ClientError
# Environment variables for the zone IDs
REVERSELOOKUPZONE1921681ID = os.environ['ReverseLookupZone1921681Id']
# Environment variable for ORG AccountID
ORGACCOUNTID = os.environ['OrganizationAccountId']
# Mapping IP prefixes to zone IDs
zone_mapping = {
'192.168.1': REVERSELOOKUPZONE10200ID
}
# Initialize the Route53 client
route53_client = boto3.client('route53')
def lambda_handler(event, context):
messages = [json.loads(record['body']) for record in event['Records']]
for message in messages:
try:
cleanedEvent = json.loads(message['Message'])
except:
cleanedEvent = message
print(f'CleanedEvent: {cleanedEvent}')
if cleanedEvent["detail-type"] == "AWS API Call via CloudTrail":
print('API Call')
# Nur aufrufen, wenn der Event ein A-Record betrifft
if is_a_record_event(cleanedEvent):
handle_called_event(cleanedEvent, context)
elif cleanedEvent["detail-type"] == "Scheduled Event":
print('Scheduled')
handle_scheduled_event(cleanedEvent, context)
def is_a_record_event(event_detail):
eventName = event_detail['detail']['eventName']
if eventName == 'ChangeResourceRecordSets':
changes = event_detail['detail']['requestParameters']['changeBatch']['changes']
for change in changes:
if change.get('resourceRecordSet', {}).get('type') == 'A':
return True
return False
def handle_called_event(event, context):
eventName = event['detail']['eventName']
if eventName == 'ChangeResourceRecordSets':
changes = event['detail']['requestParameters']['changeBatch']['changes']
for change in changes:
action = change['action']
resource_record_set = change.get('resourceRecordSet', {})
resource_records = resource_record_set.get('resourceRecords', [])
dns_name = resource_record_set.get('name', 'Unknown DNS Name')
for record in resource_records:
ip_address = record['value']
record_info = {'DNS Name': dns_name, 'IP Address': ip_address}
if action == 'CREATE':
manage_ptr_record(record_info, action='CREATE')
elif action == 'DELETE':
manage_ptr_record(record_info, action='DELETE')
def manage_ptr_record(record_info, action):
ip_addr_str = record_info['IP Address']
dns_name = record_info['DNS Name']
if not ip_addr_str.startswith(('192.168.1'')):
print(f'The IP {ip_addr_str} is not in the valid range.')
return
zone_prefix = ip_addr_str.split('.')[1]
zone_id = zone_mapping.get('192.' + zone_prefix)
if not zone_id:
print(f"No zone ID found for the IP prefix 192.{zone_prefix}.")
return
ip_blocks = ip_addr_str.split('.')
reversed_ip_blocks = ip_blocks[::-1]
reversed_ip = '.'.join(reversed_ip_blocks) + '.in-addr.arpa.'
ptr_record_name = dns_name
# überprüfe, ob der PTR-Eintrag bereits existiert
paginator = route53_client.get_paginator('list_resource_record_sets')
record_exists = False
try:
for page in paginator.paginate(HostedZoneId=zone_id):
for record_set in page['ResourceRecordSets']:
if (record_set['Type'] == 'PTR' and
record_set['Name'].rstrip('.') == reversed_ip.rstrip('.') and
any(rr['Value'].rstrip('.') == ptr_record_name.rstrip('.') for rr in record_set.get('ResourceRecords', []))):
record_exists = True
break
if record_exists:
# Schleife verlassen, wenn der Eintrag gefunden wurde
break
except ClientError as e:
print(f"An error occurred during PTR record check: {e}")
return
if not record_exists and action == 'DELETE':
print(f"No existing PTR record for {ip_addr_str} to delete.")
return
# If it does not exist and the action is CREATE, or if it exists and the action is DELETE, implement the change.
if (not record_exists and action == 'CREATE') or (record_exists and action == 'DELETE'):
change_batch = {
'Comment': f'{action} PTR record for {ip_addr_str}',
'Changes': [{
'Action': action,
'ResourceRecordSet': {
'Name': reversed_ip,
'Type': 'PTR',
'TTL': 300,
'ResourceRecords': [{'Value': ptr_record_name}]
}
}]
}
try:
response = route53_client.change_resource_record_sets(
HostedZoneId=zone_id,
ChangeBatch=change_batch
)
print(f'Successfully {action} PTR record for {ip_addr_str} to name {ptr_record_name} : {response}')
except ClientError as e:
print(f'An error occurred: {e}')
else:
if action == 'CREATE':
print(f"PTR record for {ip_addr_str} to {ptr_record_name} already exists. Skipping CREATE.")
elif action == 'DELETE':
print(f"PTR record for {ip_addr_str} to {ptr_record_name} does not exist. Skipping DELETE.")
def handle_scheduled_event(event, context):
# List to store all A-Record data
a_records_list = []
## Iterate through all accounts
for accountId in get_accounts():
print(f'############## CHECKING Account {accountId} ##############')
region = 'eu-central-1'
roleName = os.environ['LambdaRole']
route53_client = get_session(accountId, region, roleName).client('route53')
# Paginate through all the hosted zones
hosted_zones_paginator = route53_client.get_paginator('list_hosted_zones')
for hosted_zones_page in hosted_zones_paginator.paginate():
for hosted_zone in hosted_zones_page['HostedZones']:
hosted_zone_id = hosted_zone['Id']
print(f'############## Checking Hosted Zone {hosted_zone_id} ##############')
record_sets_paginator = route53_client.get_paginator('list_resource_record_sets')
for record_sets_page in record_sets_paginator.paginate(HostedZoneId=hosted_zone_id):
for record_set in record_sets_page['ResourceRecordSets']:
if record_set['Type'] == 'A': # Check for 'A' records
# Use get() with a default value to avoid KeyError
resource_records = record_set.get('ResourceRecords', [])
# Store the A-Record data in the list
for ip_record in resource_records:
a_records_list.append({
'DNS Name': record_set['Name'],
'IP Address': ip_record['Value']
})
print(f"A-Record found: {record_set['Name']} - {ip_record}")
# Process each A-Record in the list using the function 'manage_ptr_record'
for a_record in a_records_list:
manage_ptr_record(a_record, action='CREATE')
def get_accounts():
roleName = os.environ['LambdaRole']
org_client = get_session(ORGACCOUNTID, 'eu-central-1', roleName).client('organizations')
accountList = []
accountPaginator = org_client.get_paginator('list_accounts')
accountIterator = accountPaginator.paginate()
for accounts in accountIterator:
for account in accounts['Accounts']:
if account['Status'] == "ACTIVE":
accountList.append(account['Id'])
return accountList
def get_session(accountId, region, roleName):
sts_client = boto3.client('sts')
assumed_role_object = sts_client.assume_role(
RoleSessionName='xyz', RoleArn=f'arn:aws:iam::{accountId}:role/{roleName}')
credentials = assumed_role_object['Credentials']
access_key_id = credentials['AccessKeyId']
secret_access_key = credentials['SecretAccessKey']
session_token = credentials['SessionToken']
new_session = boto3.Session(
aws_access_key_id=access_key_id,
aws_secret_access_key=secret_access_key,
aws_session_token=session_token,
region_name=region
)
return new_session
The IAM role for each account, used by the Lambda function in the network account for the daily scheduler, requires the following permissions:
ReverseDNSLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: CrossAccountReverseDNSLambdaRoleRole
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
AWS:
- !Sub 'arn:aws:iam::${NetworkAccountId}:root'
Action:
- sts:AssumeRole
MaxSessionDuration: 3600
Path: /
Policies:
- PolicyName: CrossAccountReverseDNSLambdaRoleRolePolicy
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- organizations:ListAccounts
- organizations:DescribeAccount
Resource: "*"
- Effect: Allow
Action:
- route53:ListResourceRecordSets
- route53:GetHostedZone
- route53:ListHostedZones
Resource: "*"
- Effect: Allow
Action:
- iam:PassRole
Resource: 'arn:aws:iam::*:role/*'
Final Remarks
Some of the CloudFormation stacks need to be deployed as StackSets across the entire AWS Organization. Therefore, it is recommended to use tools such as CfCT or LZA, especially if ControlTower is in use.
I hope I was able to demonstrate a potential solution to automate the creation of reverse DNS entries (PTR) across an entire AWS Organization. Additionally, how the reverse DNS entries can be resolved within the AWS Organization and from on-premises.
Feel free to reach out to me if you have any questions.