# Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 Description: Outbound filtering proxy Parameters: WhitelistedDomains: Type: String Default: .amazonaws.com, .debian.org Description: Whitelisted domains comma separated CustomDNS: Type: String Default: default Description: Provide optional a DNS server for domain filtering, like OpenDNS (comma separated, like 8.8.8.8,8.8.8.7) KeyName: Type: "String" Description: Name of RSA key for EC2 access for testing only. Default: '' ProxyPort: Type: String Default: 3128 Description: Port Proxy VpcId: Description: VPC ID Where the Proxy will be installed Type: "AWS::EC2::VPC::Id" PrivateSubnetIDs: Description: Private SubnetIDs where the Network LoadBalancer will be placed (Select min 2 max 3) Type: "List" PublicSubnetIDs: Description: Public SubnetIDs where the proxy will be placed (Select min 2 max 3) Type: "List" InstanceType: Description: WebServer EC2 instance type Type: String Default: t3.medium AllowedValues: - t3.nano - t3.micro - t3.small - t3.medium - t3.large - m3.medium - m3.large - m3.xlarge - m3.2xlarge - m4.large - m4.xlarge - m4.2xlarge - m5.large - m5.xlarge - m5.2xlarge - c3.large - c3.xlarge - c4.large ConstraintDescription: must be a valid EC2 instance type. NetworkAllowedCIDR: Description: CIDR allowed in Proxy Security Group. The allowed block size is between a /32 netmask and /8 netmask Type: String Default: 172.31.0.0/16 AllowedPattern: ^[.0-9]*\/([89]|[12][0-9]|3[0-2])$ LatestAmiId: Type: 'AWS::SSM::Parameter::Value' Default: '/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2' Description: AMI ID pointer in SSM. Default latest AMI Amazon Linux2. Metadata: 'AWS::CloudFormation::Interface': ParameterGroups: - Label: default: Proxy parameter Parameters: - WhitelistedDomains - CustomDNS - ProxyPort - InstanceType - LatestAmiId - KeyName - Label: default: Network parameter Parameters: - VpcId - PrivateSubnetIDs - PublicSubnetIDs - NetworkAllowedCIDR ParameterLabels: WhitelistedDomains: default: Allowed domains (whitelisted) CustomDNS: default: Custom DNS servers ProxyPort: default: Proxy Port InstanceType: default: Instance Type LatestAmiId: default: AMI ID KeyName: default: SSH Key name VpcId: default: VPC ID PrivateSubnetIDs: default: Private Subnet IDs PublicSubnetIDs: default: Public Subnet IDs NetworkAllowedCIDR: default: Allowed client CIRD Conditions: AddSSHKey: !Not - !Equals - '' - !Ref KeyName Resources: OutboundProxyRole: Type: AWS::IAM::Role Properties: RoleName: !Sub "Outbound-proxy-${AWS::StackName}" AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - ec2.amazonaws.com Action: - sts:AssumeRole Path: "/" Policies: - PolicyName: LogRolePolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:DescribeLogStreams Resource: - !GetAtt OutboundProxyLogGroup.Arn - PolicyName: AssociateEIP PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:AssociateAddress - ec2:Describe* Resource: - "*" - PolicyName: RevokeAuthorizeSG PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:RevokeSecurityGroupIngress - ec2:AuthorizeSecurityGroupIngress - ec2:Describe* Resource: - "*" - PolicyName: GetSecret PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - secretsmanager:GetSecretValue Resource: - !Ref WhitelistedSitesSecret - PolicyName: CloudWatchMetric PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - cloudwatch:PutMetricData Resource: - "*" WhitelistedSitesSecret: Type: 'AWS::SecretsManager::Secret' Properties: Name: Proxy-Domains-Whitelisting Description: This secret contains the proxy whitelisted domains SecretString: !Ref WhitelistedDomains FixedEIPa: Type: AWS::EC2::EIP Properties: Domain: vpc FixedEIPb: Type: AWS::EC2::EIP Properties: Domain: vpc FixedEIPc: Type: AWS::EC2::EIP Properties: Domain: vpc FixedEIPd: Type: AWS::EC2::EIP Properties: Domain: vpc LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internal Type: network Name: OutboundProxyLoadBalancer Subnets: !Ref PrivateSubnetIDs NetworkLoadBalancerTargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: Name: OutboundProxyTargetGroup Port: !Ref ProxyPort Protocol: TCP VpcId: !Ref VpcId TargetGroupAttributes: - Key: deregistration_delay.timeout_seconds Value: 60 Tags: - Key: Name Value: SMARTProxyTargetGroup LoadBalancerListenerHTTPS: Type: AWS::ElasticLoadBalancingV2::Listener Properties: DefaultActions: - Type: forward TargetGroupArn: !Ref NetworkLoadBalancerTargetGroup LoadBalancerArn: !Ref LoadBalancer Port: !Ref ProxyPort Protocol: TCP OutboundProxyProfile: Type: AWS::IAM::InstanceProfile Properties: Path: "/" InstanceProfileName: !Sub "Proxy-EC2-${AWS::StackName}" Roles: - !Ref OutboundProxyRole OutboundProxySecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow access to Outbound Proxy VpcId: !Ref VpcId SecurityGroupIngress: - CidrIp: !Ref NetworkAllowedCIDR FromPort: !Ref ProxyPort ToPort: !Ref ProxyPort IpProtocol: tcp OutboundProxyASG: Type: AWS::AutoScaling::AutoScalingGroup Properties: VPCZoneIdentifier: !Ref PublicSubnetIDs Cooldown: 120 LaunchConfigurationName: !Ref OutboundProxyLaunchConfig MaxSize: 3 MinSize: 1 TargetGroupARNs: - Ref: "NetworkLoadBalancerTargetGroup" TerminationPolicies: - OldestInstance Tags: - Key: Name PropagateAtLaunch: 'true' Value: outbound-proxy - Key: AppVersion PropagateAtLaunch: 'true' Value: 1.0.0 - Key: ApplicationID PropagateAtLaunch: 'true' Value: outbound-proxy CreationPolicy: ResourceSignal: Timeout: PT15M Count: '1' UpdatePolicy: AutoScalingScheduledAction: IgnoreUnmodifiedGroupSizeProperties: true AutoScalingRollingUpdate: MinInstancesInService: 1 MaxBatchSize: 1 PauseTime: PT15M WaitOnResourceSignals: 'true' SuspendProcesses: - ScheduledActions ScaleOutPolicy: Type: AWS::AutoScaling::ScalingPolicy Properties: AdjustmentType: ChangeInCapacity AutoScalingGroupName: Ref: OutboundProxyASG Cooldown: '90' ScalingAdjustment: '1' CPUAlarmHigh: Type: AWS::CloudWatch::Alarm Properties: EvaluationPeriods: '1' Statistic: Average Threshold: '80' AlarmDescription: Alarm if CPU too high (50%) or metric disappears indicating instance is down Period: '60' AlarmActions: - Ref: ScaleOutPolicy Namespace: AWS/EC2 Dimensions: - Name: AutoScalingGroupName Value: Ref: OutboundProxyASG ComparisonOperator: GreaterThanThreshold MetricName: CPUUtilization ScaleInPolicy: Type: AWS::AutoScaling::ScalingPolicy Properties: AdjustmentType: ChangeInCapacity AutoScalingGroupName: Ref: OutboundProxyASG Cooldown: '90' ScalingAdjustment: '-1' CPUAlarmLow: Type: AWS::CloudWatch::Alarm Properties: EvaluationPeriods: '1' Statistic: Average Threshold: '10' AlarmDescription: Alarm if CPU low (10%) or metric disappears indicating instance is down Period: '60' AlarmActions: - Ref: ScaleInPolicy Namespace: AWS/EC2 Dimensions: - Name: AutoScalingGroupName Value: Ref: OutboundProxyASG ComparisonOperator: LessThanThreshold MetricName: CPUUtilization OutboundProxyLaunchConfig: Type: AWS::AutoScaling::LaunchConfiguration Metadata: Comment: Configures Outbound Proxy AWS::CloudFormation::Init: config: files: "/root/update-dns.sh": content: !Sub | # DNS List comma delimited dns_list="${CustomDNS}" # # check if default if [[ $dns_list == "default" ]]; then exit fi # # split to list array=(${!dns_list//,/ }) int_list=`ls /etc/sysconfig/network-scripts/ifcfg-* | grep -v "\-lo$\|old$"` # for all interfaces except lookback for int in ${!int_list[@]} do # remove spaces $int=${!int//[[:blank:]]/} echo "working on $int" # make tmp file without DNS settings grep -ve "PEERDNS=\|DNS.=" $int > ./tmp.int.conf grep -v "nameserver" /etc/resolv.conf > ./tmp.resolv.conf echo "PEERDNS=yes" >> ./tmp.int.conf counter=1 for i in ${!array[@]} do echo "DNS${!counter}=${!i}" >> ./tmp.int.conf echo "nameserver ${!i}" >> ./tmp.resolv.conf ((counter++)) done # update the interface config mv $int ${!int}.old cp ./tmp.int.conf $int done # update the resolv.conf mv /etc/resolv.conf /etc/resolv.conf.old cp ./tmp.resolv.conf /etc/resolv.conf # clear squid cache if squid is running. Relevant for dns content filtering # systemctl status squid && systemctl stop squid && rm -rf /var/spool/squid/ && squid -z && systemctl start squid echo "done" mode: '000755' owner: "root" group: "root" "/etc/awslogs/awscli.conf": content: !Sub | [plugins] cwlogs = cwlogs [default] region = ${AWS::Region} mode: '000755' owner: "root" group: "root" "/root/fetch-config-cron.sh": content: !Sub | aws secretsmanager get-secret-value --secret-id ${WhitelistedSitesSecret} --region ${AWS::Region} > ~/.tmp.hosts upstreamVersion=$(grep VersionId ~/.tmp.hosts) hostVersion=$(cat ~/configVersion) || hostVersion="0" # update if config if [[ $upstreamVersion != $hostVersion ]]; then mv /etc/squid/squid.allowed.sites.txt /etc/squid/squid.allowed.sites.txt.old grep SecretString ~/.tmp.hosts | sed 's/^.*SecretString\": \"\(.*\)\"\,/\1/' | tr -d " " | tr "," "\n" > /etc/squid/squid.allowed.sites.txt grep VersionId ~/.tmp.hosts > ~/configVersion systemctl restart squid echo "Squid config updated" logger "Squid config updated by cron-job from AWS secret store ${WhitelistedSitesSecret}" fi mode: '000755' owner: "root" group: "root" "/root/get-stats-cron.sh": content: !Sub | #!/bin/bash # # Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0# # # gets statistics from squid proxy and pushes them to CloudWatch # ### region=`curl --silent http://169.254.169.254/latest/dynamic/instance-identity/document | grep region | cut -f 4 -d '"'` instanceId=`curl --silent http://169.254.169.254/latest/meta-data/instance-id` squidclient -h localhost cache_object://localhost/ mgr:5min | grep "client_http.request\|client_http.hits\|client_http.errors\|client_http.kbytes_in\|client_http.kbytes_out\|server.all." | while read line ; do name=`echo $line | cut -d "=" -f 1` value=`echo $line | cut -d "=" -f 2 | sed "s/[^0-9\.]*//g" ` aws cloudwatch put-metric-data --metric-name "$name" --namespace Proxy --dimensions InstanceID="$instanceId" --value "$value" --region $region done mode: '000755' owner: "root" group: "root" "/etc/squid/squid.allowed.sites.txt": content: | .amazon.com mode: '000400' owner: "root" group: "root" "/etc/squid/squid.conf": content: !Sub | # Recommended minimum configuration: # # Example rule allowing access from your local networks. # Adapt to list your (internal) IP networks from where browsing # should be allowed acl localnet src 10.0.0.0/8 # RFC1918 possible internal network acl localnet src 172.16.0.0/12 # RFC1918 possible internal network acl localnet src 192.168.0.0/16 # RFC1918 possible internal network acl localnet src fc00::/7 # RFC 4193 local private network range acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines acl localnet src 127.0.0.1 # The Instance Metadata Service # (https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html#instance-metadata-limiting-access) acl imds dst 169.254.169.254 acl SSL_ports port 443 acl Safe_ports port 80 # http acl Safe_ports port 21 # ftp acl Safe_ports port 443 # https acl Safe_ports port 70 # gopher acl Safe_ports port 210 # wais acl Safe_ports port 1025-65535 # unregistered ports acl Safe_ports port 280 # http-mgmt acl Safe_ports port 488 # gss-http acl Safe_ports port 591 # filemaker acl Safe_ports port 777 # multiling http acl CONNECT method CONNECT # # Recommended minimum Access Permission configuration: # # Deny requests to the Instance Metadata Service http_access deny imds # Deny requests to certain unsafe ports http_access deny !Safe_ports # Deny CONNECT to other than secure SSL ports http_access deny CONNECT !SSL_ports # Only allow cachemgr access from localhost http_access allow localhost manager http_access deny manager # Deny requests to services running on localhost http_access deny to_localhost # # INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS # # Example rule allowing access from your local networks. # Adapt localnet in the ACL section to list your (internal) IP networks # from where browsing should be allowed acl allowed_http_sites dstdomain "/etc/squid/squid.allowed.sites.txt" http_access allow allowed_http_sites #http_access allow localnet #http_access allow localhost # And finally deny all other access to this proxy http_access deny all # Squid normally listens to port 3128, but needs to be parametrized here http_port 0.0.0.0:${ProxyPort} ssl-bump cert=/etc/squid/cert.pem acl allowed_https_sites ssl::server_name "/etc/squid/squid.allowed.sites.txt" acl step1 at_step SslBump1 acl step2 at_step SslBump2 acl step3 at_step SslBump3 ssl_bump peek step1 all ssl_bump peek step2 allowed_https_sites ssl_bump splice step3 allowed_https_sites ssl_bump terminate step2 all # Uncomment and adjust the following to add a disk cache directory. #cache_dir ufs /var/spool/squid 100 16 256 # Leave coredumps in the first cache dir coredump_dir /var/spool/squid # # Add any of your own refresh_pattern entries above these. # refresh_pattern ^ftp: 1440 20% 10080 refresh_pattern ^gopher: 1440 0% 1440 refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 refresh_pattern . 0 20% 4320 "/etc/awslogs/awslogs.conf": content: !Sub | [general] state_file = /var/lib/awslogs/agent-state [/var/log/squid/access.log] file = /var/log/squid/access.log log_group_name = ${OutboundProxyLogGroup} log_stream_name = {instance_id}/squid_access.log #datetime_format = %d/%b/%Y:%H:%M:%S mode: '000400' owner: "root" group: "root" "/etc/cfn/cfn-hup.conf": content: !Sub | [main] stack= ${AWS::StackId} region=${AWS::Region} interval=5 mode: "000400" owner: "root" group: "root" "/etc/cfn/hooks.d/cfn-auto-reloader.conf": content: !Sub | [cfn-auto-reloader-hook] triggers=post.update path=Resources.OutboundProxyLaunchConfig.Metadata.AWS::CloudFormation::Init action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource OutboundProxyLaunchConfig --region ${AWS::Region} runas=root mode: "000400" owner: "root" group: "root" Properties: AssociatePublicIpAddress: True ImageId: !Ref LatestAmiId InstanceType: !Ref InstanceType KeyName: !If - AddSSHKey - !Ref KeyName - !Ref "AWS::NoValue" SecurityGroups: - !Ref OutboundProxySecurityGroup IamInstanceProfile: Ref: OutboundProxyProfile UserData: Fn::Base64: !Sub | #!/bin/bash -xe yum -y install python-pip yum -y install python-setuptools yum install -y awscli # install squid yum install -y squid # install the CloudWatch Logs agent yum install -y awslogs # Get the latest CloudFormation package easy_install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-latest.tar.gz # Start cfn-init /opt/aws/bin/cfn-init -s ${AWS::StackId} -r OutboundProxyLaunchConfig --region ${AWS::Region} || error_exit 'Failed to run cfn-init' # Start up the cfn-hup daemon to listen for changes to the launch configuration metadata /opt/aws/bin/cfn-hup || error_exit 'Failed to start cfn-hup' # start the cloud watch agent systemctl start awslogsd # get the IP allocation id EIPs=(${FixedEIPa.AllocationId} ${FixedEIPb.AllocationId} ${FixedEIPc.AllocationId} ${FixedEIPd.AllocationId}) for i in ${!EIPs[@]}; do out=$(aws ec2 describe-addresses --region ${AWS::Region} --allocation-ids $i) if [[ $out != *AssociationId* ]]; then freeEIP=$i break fi done # bind the address echo "binding EIP" aws ec2 associate-address --region ${AWS::Region} --instance-id $(curl -s http://169.254.169.254/latest/meta-data/instance-id) --allocation-id $freeEIP --allow-reassociation || error_exit 'Failed to Associate Elastic IP' # generate dummy certificate openssl req -x509 -newkey rsa:4096 -keyout /etc/squid/cert.pem -out /etc/squid/cert.pem -days 3650 -subj "/C=XX/ST=XX/L=squid/O=squid/CN=squid" -nodes # get the whitelisted domain /root/fetch-config-cron.sh # start squit systemctl start squid # cron to update whitelist if needed every 5 min echo "*/5 * * * * /root/fetch-config-cron.sh" | crontab - # cron to to get and push proxy stats (crontab -l ; echo "*/5 * * * * /root/get-stats-cron.sh") | crontab - # set up DNS if needed if [[ ${CustomDNS} != "default" ]]; then /root/update-dns.sh fi # All done so signal success /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackId} --resource OutboundProxyASG --region ${AWS::Region} echo "User data done" OutboundProxyLogGroup: Type: AWS::Logs::LogGroup Properties: RetentionInDays: 30 LogGroupName: !Sub "Proxy-${AWS::StackName}" Outputs: CloudWatchLogGroupName: Description: The name of the CloudWatch log group for outbound proxy Value: !Ref OutboundProxyLogGroup Export: Name: Proxy-CloudWatchLogGroupName OutboundProxyDomain: Description: Proxy DNS name to be used in the clients Value: !GetAtt LoadBalancer.DNSName Export: Name: Proxy-Domain OutboundProxyPort: Description: Port of the Proxy Value: !Ref ProxyPort Export: Name: Proxy-Port EgressIP1: Description: Outbound Proxy source IP (first) Value: !Ref FixedEIPa Export: Name: Proxy-Egress-IP-1 EgressIP2: Description: Outbound Proxy source IP (second) Value: !Ref FixedEIPb Export: Name: Proxy-Egress-IP-2 EgressIP3: Description: Outbound Proxy source IP (third) Value: !Ref FixedEIPc Export: Name: Proxy-Egress-IP-3 EgressIP4: Description: Outbound Proxy source IP (fourth) Value: !Ref FixedEIPd Export: Name: Proxy-Egress-IP-4 SecurityGroupProxy: Description: Proxy security group Value: SecurityGroup_Proxy Export: Name: Proxy-SecurityGroup LinuxProxySettings: Description: Linux proxy settings. Copy and paste to your shell to set the proxy Value: !Sub "export http_proxy=http://${LoadBalancer.DNSName}:${ProxyPort} && export https_proxy=$http_proxy" Export: Name: LinuxProxySettings