PCG logo
Article

Reverse DNS at scale for the entire AWS organization

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

image-c3f5f6152505

Event-Based Update Process:

1. An EventBridge Rule forwards the "ChangeResourceRecordSets" event from Route53 to the central Network Account.

2. The events are collected in an SQS queue and processed by a Lambda function.

3. The Lambda function checks if the event was an A-record and if here is a corresponding Reverse Lookup Zone for the IP address.

4. Depending on the event, the PTR record is created or deleted.

Scheduled Update Process

(This process is important to initially create all PTRs if A-records already exist in the AWS Organization)

1. EventBridge schedules the Lambda function in the Network Account to run once daily.

2. The Lambda function detects the detail type "Scheduled Event."

3. The Lambda iterates through all accounts in the AWS Organization and searches for Route53 Private Hosted Zones.

4. All found A-records are written into a Python list.

5. At the end, it checks if there is already a PTR for each found A-record. If not, it creates one.

Resolving PTR Records within the AWS Organization

1. The Route53 Private Hosted Zones for the reverse lookup zones, e.g., 168.192.in-addr.arpa., are associated with the Network Account VPC.

2. The Network Account has both a Route53 Inbound Resolver and an Outbound Resolver.

3. In the Network Account, there is a Route53 Resolver Rule that uses the Outbound Resolver and targets the Inbound Resolver’s IP address. This rule is shared with the entire AWS Organization using AWS Resource Access Manager (RAM).

4. The Project VPCs are associated with the Route53 Resolver Rules.

5. As a result, all reverse DNS queries for 168.192.in-addr.arpa. are sent to the Inbound Resolver, which can resolve them.

Resolving PTR Records from On-Premises

1. On the on-premises DNS server, conditional forwarding is configured for, e.g., 168.192.in-addr.arpa. The target is set to the Inbound Resolver.

2. The Inbound Resolver is aware of the Route53 Private Hosted Zones, so it can resolve the PTR records and respond to the on-premises DNS server.

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:

  • Domain Name: 1.168.192.in-addr.arpa
  • IP Range Covered: 192.168.1.0 to 192.168.1.255

PHZ for 192.168.2.0/24

  • Domain Name: 2.168.192.in-addr.arpa
  • IP Range Covered: 192.168.2.0 to 192.168.2.255
yaml
Code Copied!copy-button
 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.

yaml
Code Copied!copy-button
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.

image-61c5ea10214f

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.

text
Code Copied!copy-button
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:

python
Code Copied!copy-button
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.

yaml
Code Copied!copy-button
 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.

yaml
Code Copied!copy-button
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:

yaml
Code Copied!copy-button
 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:

yaml
Code Copied!copy-button
 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:

yaml
Code Copied!copy-button
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:

python
Code Copied!copy-button
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:

yaml
Code Copied!copy-button
 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 CfCTExternal Link or LZAExternal Link, 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.


Services Used

Continue Reading

Article
Automation
Automated Control Rollout in AWS Control Tower

Control Tower Controls help you to set up guardrails making your environment more secure and helping you ensuring governance across all OUs and accounts.

Learn more
News
Above the Clouds: PCG's Stellar Performance at the AWS LeadMaster Challenge 2024

Wow, what a triumph! Public Cloud Group has just swept the AWS Summit 2024 Lead Master Challenge.

Learn more
Article
AWS Events 2025: The Future is Cloud

As a leading AWS Premier Partner, we're thrilled to present the exciting lineup of AWS events for 2025.

Learn more
Article
Protecting Lambda URLs with Cognito, IAM, Lambda@Edge and CDK

In this article, we’ll look at how to secure Lambda URLs using IAM access control. With complete code to try yourself!

Learn more
See all

Let's work together

United Kingdom
Arrow Down