Blogs

Outbound Internet Access via Web Proxy and AWS VPC Peering

AWS provides the mechanisms to create VPC designs that run the gamut of the complexity spectrum. You can deploy your application in a single VPC with only public subnets. Other applications may be better suited to a collection of VPCs with both public and private subnets. You can use VPCs as building blocks for large-scale distributed applications, a design decision that requires VPCs to communicate to one another. In this post, I’ll examine a VPC design pattern in which a VPC is dedicated for providing internet access using VPC peering with a web proxy.

Consider the scenario in which you maintain applications in separate VPCs. The applications require outbound access to the Internet and do not offer any Internet-facing services. When the number of VPCs is many, centralizing the egress point for all traffic to the Internet may help an organization meet specialized security requirements. The centralized point for policy enforcement can be implemented with a pair of security appliances in a VPC dedicated to providing egress to the Internet. In a networking chalk talk at re:Invent 2018, an AWS engineer referred to this design pattern as an outbound services VPC. The use of web proxies is one implementation of an outbound services VPC.

Why centralize outbound Internet access? From a security perspective, the company might want to perform URL filtering, IDS/IPS, or application-layer filtering. By centralizing outbound access, the company’s security team can manage policy in a smaller number of places. Security appliances are not needed in every application VPC, which has benefits in terms of cost and decoupling the application lifecycle from the outbound services VPC.

The applications are instantiated in other dedicated VPCs. Traffic from these VPCs must traverse the outbound services VPC to reach the Internet. Recall that transitive traffic in the VPC is prohibited: traffic that is sourced in one VPC and directed toward a second VPC must terminate in the second VPC. For this reason, you can’t rely on VPC peering alone for enabling applications VPCs to traverse the outbound services VPCs. The traffic would be dropped.

By adding a pair of proxy servers in the outbound services VPC, the limitation on the non-transitive nature of VPCs can be worked around. The servers in the application VPCs that require outbound access are configured to use the proxies in the outbound services VPC. This terminates the TCP connection in the outbound services VPC and creates a new TCP session for the proxy to Internet web server connection.

VPC peering is ideal for inter-VPC connectivity for a host of reasons.

  • VPC peering is highly available service provided as a native VPC component

  • VPC peering traffic uses the private AWS network backbone

  • VPC peering can be established between AWS regions

  • VPC peering traffic is encrypted when established between regions

  • Network throughput with VPC peering is higher than you’ll experience with AWS Managed VPN

Let’s examine a diagram of this design.

The diagram depicts EC2 servers in application VPCs. These instances must be configured to use the web proxies in the outbound services VPC. The outbound service VPC can be a simple as two web proxies running on EC2 instances. A more robust implementation would use a proxy farm fronted by an ELB.

Let’s check out how we can use a squid proxy in the outbound services VPC to proxy connections from an application VPC.

Here is the outbound VPC template.

  1. AWSTemplateFormatVersion: 2010-09-09
    Description: >-
    AWS CloudFormation template to create an Outbound VPC with a proxy for the App VPCs
    Parameters:
    KeyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
    Type: String
    SourceIpCIDR:
    Description: CIDR Range allowed to SSH to Squid Proxy
    Type: String
    MinLength: ‘9’
    MaxLength: ’18’
    Default: 0.0.0.0/0
    AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})
    ConstraintDescription: Must use a valid IP CIDR range using slash notation (e.g., x.x.x.x/y)
    AmazonLinuxAMI:
    Type: ‘AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>’
    Default: ‘/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2’
    Resources:
    OutboundVpc:
    Type: AWS::EC2::VPC
    Properties:
    CidrBlock: 172.16.0.0/24
    EnableDnsSupport: ‘true’
    EnableDnsHostnames: ‘true’
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-internet-access’
    IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-internet-access’
    IgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
    VpcId: !Ref OutboundVpc
    InternetGatewayId: !Ref IGW
    OutboundPublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-output’
    VpcId: !Ref OutboundVpc
    AvailabilityZone: !Sub ${AWS::Region}a
    MapPublicIpOnLaunch: false
    CidrBlock: 172.16.0.0/24
    InternetRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
    VpcId:
    !Ref OutboundVpc
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-internet-access’
    InternetDefaultRoute:
    Type: AWS::EC2::Route
    DependsOn:
    – IGW
    – IgwAttachment
    Properties:
    RouteTableId: !Ref InternetRouteTable
    DestinationCidrBlock: 0.0.0.0/0
    GatewayId: !Ref IGW
    InternetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
    SubnetId:
    !Ref OutboundPublicSubnet
    RouteTableId:
    !Ref InternetRouteTable
    OutboundVpcSecurityGroup:
    Type: ‘AWS::EC2::SecurityGroup’
    Description: SG to permit all traffic
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-outbound’
    GroupName: !Sub ‘${AWS::StackName}-outbound’
    GroupDescription: Allow all traffic
    VpcId: !Ref OutboundVpc
    SecurityGroupIngress:
    – IpProtocol: tcp
    CidrIp: !Ref SourceIpCIDR
    FromPort: 22
    ToPort: 22
    – IpProtocol: tcp
    CidrIp: 172.17.0.0/16
    FromPort: 3128
    ToPort: 3128
    SquidEC2Instance:
    Type: ‘AWS::EC2::Instance’
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-squid’
    KeyName: !Ref KeyName
    ImageId: !Ref AmazonLinuxAMI
    InstanceType: t3.nano
    NetworkInterfaces:
    – AssociatePublicIpAddress: true
    DeviceIndex: 0
    PrivateIpAddress: 172.16.0.100
    GroupSet: [ !Ref OutboundVpcSecurityGroup ]
    SubnetId: !Ref OutboundPublicSubnet
    UserData:
    Fn::Base64: |
    #!/bin/bash -xe
    yum install -y squid
    systemctl start squid.service
    systemctl enable squid.service
    Outputs:
    SquidProxyDNS:
    Description: DNS name for squid proxy
    Value: !GetAtt SquidEC2Instance.PublicDnsName
    InternetRouteTable:
    Description: Internet Route Table ID
    Value: !Ref InternetRouteTable
    Export:
    Name: !Sub ${AWS::StackName}-InternetRouteTable
    OutboundVpc:
    Description: VPC ID of Outbound VPC
    Value: !Ref OutboundVpc
    Export:
    Name: !Sub ${AWS::StackName}-OutboundVpc

The next template creates one application VPC with an EC2 instance for testing.

  1.  AWSTemplateFormatVersion: 2010-09-09Description: >-
    AWS CloudFormation template to create an App VPC that uses a proxy in the Outbound VPC
    for HTTP/HTTPS Internet access.
    Parameters:
    KeyName:
    Description: Name of an existing EC2 KeyPair to enable SSH access to the instances
    Type: String
    OutboundVpcStack:
    Description: Name of the Cloudformation stack used to create the Outbound VPC
    Type: String
    AppVpcCIDR:
    Description: CIDR Range for App VPC
    Type: String
    Default: 172.17.0.0/24
    Mappings:
    UbuntuRegionMap:
    us-east-1:
    Ubuntu18AMI: ami-0ac019f4fcb7cb7e6
    us-west-1:
    Ubuntu18AMI: ami-063aa838bd7631e0b
    Resources:
    AppVPC:
    Type: AWS::EC2::VPC
    Properties:
    CidrBlock: !Ref AppVpcCIDR
    EnableDnsSupport: ‘true’
    EnableDnsHostnames: ‘true’
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-app’
    AppPrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
    VpcId: !Ref AppVPC
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-app’
    AppPrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-app’
    VpcId: !Ref AppVPC
    AvailabilityZone: !Sub ${AWS::Region}a
    CidrBlock: !Ref AppVpcCIDR
    MapPublicIpOnLaunch: false
    AppVpcSecurityGroup:
    Type: ‘AWS::EC2::SecurityGroup’
    Description: SG to permit SSH for management
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-app’
    GroupName: !Sub ‘${AWS::StackName}-app’
    GroupDescription: Allow traffic from Outbound Services VPC
    VpcId: !Ref AppVPC
    SecurityGroupIngress:
    – IpProtocol: tcp
    CidrIp: 172.16.0.0/24
    FromPort: 22
    ToPort: 22
    AppPrivateRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
    SubnetId:
    !Ref AppPrivateSubnet
    RouteTableId:
    !Ref AppPrivateRouteTable
    AppEC2Instance:
    Type: ‘AWS::EC2::Instance’
    Properties:
    Tags:
    – Key: Name
    Value: !Sub ‘${AWS::StackName}-app’
    KeyName: !Ref KeyName
    ImageId: !FindInMap [ UbuntuRegionMap, !Ref ‘AWS::Region’, Ubuntu18AMI ]
    InstanceType: t3.nano
    SecurityGroupIds:
    – !Ref AppVpcSecurityGroup
    SubnetId: !Ref AppPrivateSubnet
    UserData:
    Fn::Base64: |
    #!/bin/bash -xe
    cat << EOF > /etc/apt/apt.conf.d/95proxies
    Acquire::http::proxy “http://172.16.0.100:3128/”;
    Acquire::https::proxy “https://172.16.0.100:3128/”;
    EOF
    cat << EOF >> /etc/environment
    http_proxy=”http://172.16.0.100:3128/”
    https_proxy=”http://172.16.0.100:3128/”
    EOF
    VPCPeeringConnection:
    Type: AWS::EC2::VPCPeeringConnection
    Properties:
    VpcId: !Ref AppVPC
    PeerVpcId:
    Fn::ImportValue: !Sub ‘${OutboundVpcStack}-OutboundVpc’
    AppDefaultRoute:
    Type: AWS::EC2::Route
    Properties:
    RouteTableId: !Ref AppPrivateRouteTable
    DestinationCidrBlock: 0.0.0.0/0
    VpcPeeringConnectionId: !Ref VPCPeeringConnection
    OutboundVPCRoutetoAppVPC:
    Type: AWS::EC2::Route
    Properties:
    RouteTableId:
    Fn::ImportValue: !Sub ${OutboundVpcStack}-InternetRouteTable
    DestinationCidrBlock: !Ref AppVpcCIDR
    VpcPeeringConnectionId: !Ref VPCPeeringConnection

First launch the outbound services VPC stack.

  1. aws cloudformation create-stack –stack-name outbound-vpc –template-body file://outbound_vpc_proxy.yml –parameters ParameterKey=KeyName,ParameterValue=YourSSHKeyHere –region us-east-1

Now launch a template for a single application VPC using the name of outbound services VPC stack so that the peer VPC ID can be imported.

  1. aws cloudformation create-stack –stack-name app1 –template-body file://app_vpc_proxy_access.yml –parameters ParameterKey=OutboundVpcStack,ParameterValue=outbound-vpc –region us-east-1 ParameterKey=KeyName,ParameterValue=YourSSHKeyHere

To verify that the application VPC uses the proxy to reach Internet web servers, ssh into the squid proxy as ec2-user. Next ssh to the Ubuntu EC2 instance using the “ubuntu” username. Type ‘curl https://konekti.us’ and you will retrieve the contents of the Konekti landing page.

  • AWS SSM can be used to locate the latest Amazon Linux AMI. See this blog post for more details.

  • For serving as a basic web proxy, no additional configuration for squid on Amazon Linux 2 is required.
  • Additional application VPCs can be deployed by overriding the default AppVpcCIDR parameter to use unique CIDR ranges.

The VPC design pattern described in this post is very helpful for companies that require strict security controls. If you have any questions, please contact Konekti using this form.

Author

Jeff Loughridge

Comments (2)

  1. Tom
    September 13, 2021

    Hi Jeff,
    Many thanks for really useful article! Is it possible to share the CloudFormation templates you used?

    Many thanks!

    • Jeff Loughridge
      September 13, 2021

      Tom, I’m glad you enjoyed it. The CFN is included in the body of the article.

Leave a comment

Your email address will not be published. Required fields are marked *