Wednesday, November 13, 2019

Cross-account sharing of a PrivateLink endpoint using Private Hosted Zones and CloudFormation

Introduction

It is possible to concentrate all your PrivateLink endpoints in one account, then share them with other accounts and access them through a Transit Gateway.

This reduces the consumption of private IPs and makes everything cleaner. You also do not have to pay a hourly fee for these endpoints in each of your accounts, although you still have to consider the transit fees involved to bring the data in another VPC.

This is done using Route53 and Private Hosted Zones (PHZs). James Levine's post Integrating AWS Transit Gateway with AWS PrivateLink and Amazon Route 53 Resolver explains very clearly how you can achieve this. I'll spare the details but basically, this lets you override the DNS addresses of the endpoints within your accounts to point to your private address instead of the public one.

Go read James' article first, then come back here for implementation details.

A sample use case

The use case that made me do this initially is AWS Systems Manager. I wanted to be able to use its Session Manager feature to open interactive sessions on EC2 instances, in multiple accounts. Since I wanted to prevent routing SSM data through the internet, combined with the fact that it required many VPC endpoints, I decided to concentrate them in one account.

As documented, four VPC interface endpoints (i.e. PrivateLink endpoints) are needed for this: ssm, ssmmessages, ec2 and ec2messages. There is also a fifth endpoint for S3, but that one is a gateway endpoint and it needs to be defined in each of your accounts.

When an EC2 instance tries to communicate with the ssm endpoint, its agent looks up that endpoint's  DNS address and by default, it gets the public IP address for your region. For example:

$ nslookup ssm.ca-central-1.amazonaws.com

Non-authoritative answer:
Name:   ssm.ca-central-1.amazonaws.com
Address: 52.94.100.144

But what do you do if you have defined a PrivateLink endpoint for ssm.ca-central-1.amazonaws.com in another account, and you wish to use it through a peering connection or a Transit Gateway? James explains how to configure a DNS hosted zone to fool everything in that VPC into using a private address. A lookup in this EC2 instance will then give this result:

$ nslookup ssm.ca-central-1.amazonaws.com

Non-authoritative answer:
Name:   ssm.ca-central-1.amazonaws.com
Address: 192.168.0.10

where 192.168.0.10 is the private IP address assigned to the VPC endpoint.

This works because that DNS record is, in fact, an alias record on the regional address of your endpoint. For example, ssm.ca-central-1.amazonaws is aliased to vpce-0123456789abcdef-01234abcd.ssm.ca-central-1.vpce.amazonaws.com.

If you went further and defined your VPC in multiple AZs, you should have multiple addresses :

$ nslookup ssm.ca-central-1.amazonaws.com

Non-authoritative answer:
Name:   ssm.ca-central-1.amazonaws.com
Address: 192.168.0.10, 192.168.1.10

N.B. I'm still not sure what the impact of a failure in one AZ is in this scenario as I'm well-aware that using DNS as a failover mechanism isn't great and can involve timeouts.  I hope that any unavailable IP address will be removed dynamically from the regional address... I don't have the answer to this one.


Configuring a PrivateLink endpoint with a PHZ in CloudFormation

Here are the three resources you need to put in your CF template to deploy an endpoint and a private hosted zone. Once you have these in place, it is a matter of repeating the same code for ssmmessagesec2 and ec2messages.

Security Group
The first thing you need is to define a security group to make your endpoint accessible on the port it needs (usually TCP/443):

  ssmendpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: "SG for SSM endpoint"
      GroupName: "SSMSG"
      SecurityGroupIngress:
        - CidrIp: 10.10.0.0/16
          IpProtocol: tcp
          FromPort: 443
          ToPort: 443
      Tags:
        - Key: Name
          Value: "My SSM Endpoint SG"
      VpcId:
        Fn::ImportValue: "VPC-id-outputvariable-from-another-template"

Note here that I've imported the VPC ID using an output variable that comes from another CF template. You can hardcode it or input it as a parameter if you prefer.

PrivateLink Endpoint
Then, you define the PrivateLink endpoint itself:

  ssmendpointVPC:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ssm
      SecurityGroupIds:
        - !Ref ssmendpointSG
      SubnetIds:
        - Fn::ImportValue:
            !Sub "AZ1-subnet-id-outputvariable-from-another-template"
        - Fn::ImportValue:
            !Sub "AZ2-subnet-id-outputvariable-from-another-template"
      VpcId:
        Fn::ImportValue:
          !Sub "VPC-id-outputvariable-from-another-template"

Private hosted zone
As for the private zone, these two entries need to be configured (you'll need to replace the region with yours):

  phzssmcacentral1amazonawscom:
    Type: AWS::Route53::HostedZone
    Properties:
      Name: ssm.ca-central-1.amazonaws.com
      VPCs:
      - VPCId:
          Fn::ImportValue: "VPC-id-outputvariable-from-another-template"
        VPCRegion: "ca-central-1"

  phzaliasrecordssmcacentral1amazonawscom:
    Type: AWS::Route53::RecordSet
    Properties:
      AliasTarget:
        HostedZoneId: !Select [ '0', !Split [ ':', !Select [ '0', !GetAtt ssmendpointVPC.DnsEntries ]]]
        DNSName: !Select [ '1', !Split [ ':', !Select [ '0', !GetAtt ssmendpointVPC.DnsEntries ]]]
      HostedZoneId: !Ref phzssmcacentral1amazonawscom
      Name: ssm.ca-central-1.amazonaws.com.
      Type: A

Note the multiple selectors under AliasTarget. The combination of these selectors extracts specific fields from AWS::EC2::VPCEndpoint that are made available as attributes once CloudFormation deploys the endpoint, namely:

  • The hosted zone ID for the endpoint
  • The regional DNS address of the endpoint (as opposed to the one pointing to the AZ itself)

Sharing a PHZ across accounts

Once the PHZ is deployed, you need it to share it with your accounts. Unfortunately, you cannot do this with CloudFormation. The procedure is explained in the KB article How do I associate a Route 53 private hosted zone with a VPC on a different AWS account?. It explains how to do this using the CLI.

Good luck.


No comments: