Yohan Beschi
Developer, Cloud Architect and DevOps Advocate
Protecting an AWS ALB behind an AWS Cloudfront Distribution
Reducing the number of entry points into VPCs reduce the surface of possible attacks. Which in the end makes our infrastructures a lot more secure. When an AWS Cloudfront distribution has an AWS Application LoadBalancer (ALB) as an origin, the ALB must be public (internet-facing) and therefore, is by default accessible on all the ports defined by our listeners (usually 80 and 443).
At first glance this does not seem problematic. Without a Cloudfront distribution these ports are already opened to the world (0.0.0.0/0). But in reality we are still exposed to (Distributed) Denial-Of-Service (DoS and DDoS) and Denial-of-Wallet (DoW) attacks.
- Cloudfront helps mitigate DDoS attacks
- A Web Application Firewall (WAF) can prevent DoS attacks with a Rate Based Rule, which will limit DoW as-well
- And with Bugdet Alerts we can be notified of unusual expenses
We could have one WAF on the Cloudfront distribution and another one on the ALB, but this would double the costs and increase the latency, and Cloudfront could still be by-passed.
In the following chart we have 2 different ways of reaching an unsecured internet-facing ALB:
The ojbective of this article is to show how to get rid-off the red lines.
Clouformation templates
All the source code presented in this article is available in a Github repository.
To make the deployment of the Cloudformation templates easier we are using Ansible. The article Cloudformation with Ansible explains how it works.
The repository is organized as follow:
vars
- configuration used by Ansible to deploy the Cloudformation templatesinitialization
- create a bucket to store assets used during the provisioning and another one to store frontend resourcesstep_xxx
- different ways to configure a Cloudfront distribution, an ALB and a WAFlambdas
- AWS Lambdas used in the infrastructure
To have a simple example, for this article the Cloudfront distribution forwards all requests to the ALB and the targets are Nginx servers serving static resources from an S3 Bucket.
Please note that by deploying these stacks, you will be charged for the following resources:
- 1 NAT Gateway
- 1 S3 VPC endpoint
- 1 ALB
- 1 WAF
- 1 t3.nano EC2 instance
Unsecured infrastructure
Starting from an initial infrastructure (step_0) where an ALB has a Security group
opened to the world and two Listener
s which forward all requests to the targets without any restrictions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
AlbSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: alb-sg
GroupDescription: Allows access to the Reverse Proxy ALB
VpcId: !Ref VpcId
SecurityGroupIngress:
- CidrIp: 0.0.0.0/0
IpProtocol: tcp
FromPort: 80
ToPort: 80
Description: Allow Connection from the World
- CidrIp: 0.0.0.0/0
IpProtocol: tcp
FromPort: 443
ToPort: 443
Description: Allow Connection from the World
HttpAppListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ReverseProxyTargetGroup
LoadBalancerArn: !Ref ReverseProxyLoadBalancer
Port: 80
Protocol: HTTP
HttpsAppListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: forward
TargetGroupArn: !Ref ReverseProxyTargetGroup
LoadBalancerArn: !Ref ReverseProxyLoadBalancer
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref AlbCertificateArn
The network flow for a regular user would be:
- example.com
- Cloudfront
- app.example.com
- ALB
- EC2 instance
The ALB being an internet-facing ALB, we can access the application using:
- app.example.com
- the ALB DNS
- the ALB IPs
Having a reverse proxy being able to serve multiple applications routed by Nginx using the Host
header is even worse. Not having example.com
in the Host
header means that Nginx will fallback to its default page.
Furthermore, nothing forbid anyone to have its own Cloudfront distribution (or any reverse proxy) and use app.example.com
as an origin.
Futhermore, in this example we are using CNAMEs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CloudfrontRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref PublicHostedZoneId
RecordSets:
- Name: !Ref PublicDns
Type: CNAME
TTL: 60
ResourceRecords:
- !GetAtt CloudFrontDistribution.DomainName
ReverseProxyRecord:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref PublicHostedZoneId
RecordSets:
- Name: !Ref ReverseProxyDns
Type: CNAME
TTL: 60
ResourceRecords:
- !GetAtt ReverseProxyLoadBalancer.DNSName
A simple dig
command can return the DNS of the Cloudfront distribution and the ALB. Both DNS allowing us to access the
application. It is one reason why Aliases should be preferred when possible, using the principle of “Security by obfuscation”,
which is very weak but is better than nothing.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CloudfrontRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref PublicHostedZoneId
RecordSets:
- Name: !Ref PublicDns
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt CloudFrontDistribution.DomainName
ReverseProxyRecord:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref PublicHostedZoneId
RecordSets:
- Name: !Ref ReverseProxyDns
Type: A
AliasTarget:
DNSName: !GetAtt ReverseProxyLoadBalancer.DNSName
HostedZoneId: !GetAtt ReverseProxyLoadBalancer.CanonicalHostedZoneID
Restricting access to our ALB
The first (and most important) thing to secure is our ALB. Only Cloudfront should be able to access it.
To achieve this, the ALB Security Group should only allow access from Cloudfront IPs (step_1).
Cloudfront being distributed, it has dozens of dynamic IPs. Fortunately, AWS publishes the IPs used by Cloudfront and has even an SNS Topic triggered every time the IP ranges change. Furthermore, AWS provides the lambda that can update our Security Groups automatically.
The lambda provided by AWS using Python 2 and requiring to have specific Security Group names (cloudfront_g
and cloudfront_r
), in the github repository dedicated to this article you will find the lambda ported to Python 3 and using a tag SecurityGroupType
, instead of Name
.
As mentioned before, we need more than one Security Group, due to the limit of 60 rules per Security Group. We can have one Security Group by Cloudfront IP type (Global or Regional) and port (80 or 443):
- Cloudfront Global HTTP
- Cloudfront Global HTTPS
- Cloudfront Regional HTTP
- Cloudfront Regional HTTPS
Until now the ALB allowed access to the port 80, which is useless as Cloudfront has been configured to access the origin only in HTTPS:
1
2
3
4
5
6
Origins:
- Id: ReverseProxy
DomainName: !Ref ReverseProxyDns
CustomOriginConfig:
OriginProtocolPolicy: https-only
OriginReadTimeout: 60
So we will :
- remove the HTTP Listener (
HttpAppListener
) - all the Ingress Rules of the current ALB Security Group
- add 2 new Security Groups
The 3 Security Groups should be defined as below:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
AlbSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: alb-sg
GroupDescription: Allows access to the Reverse Proxy ALB
VpcId: !Ref VpcId
AlbHttpsCloudfrontGSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: alb-cloudfront-global-https-sg
GroupDescription: Cloudfront Global HTTPS
VpcId: !Ref VpcId
Tags:
- Key: SecurityGroupType
Value: cloudfront_g
- Key: AutoUpdate
Value: true
- Key: Protocol
Value: https
AlbHttpsCloudfrontRSG:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: alb-cloudfront-regional-https-sg
GroupDescription: Cloudfront Regional HTTPS
VpcId: !Ref VpcId
Tags:
- Key: SecurityGroupType
Value: cloudfront_r
- Key: AutoUpdate
Value: true
- Key: Protocol
Value: https
The three tags on the Security Groups will help the Lambda identify which Security Groups to update.
Moreover, having a Security Group without any rule is useful to allow access to a resource from the holder of the Security Group.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
ReverseProxyLoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Scheme: internet-facing
Type: application
Subnets: !Ref AlbSubnets
LoadBalancerAttributes:
- Key: idle_timeout.timeout_seconds
Value: 100
SecurityGroups:
- !Ref AlbSecurityGroup
- !Ref AlbHttpsCloudfrontGSG
- !Ref AlbHttpsCloudfrontRSG
ReverseProxySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: application-sg
GroupDescription: Allows access to the Reverse Proxy
VpcId: !Ref VpcId
SecurityGroupIngress:
- SourceSecurityGroupId: !Ref AlbSecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
Description: Allow Connection from the Reverse Proxy ALB
ReverseProxySecurityGroup
allows access to any resource holding the Security Group AlbSecurityGroup
on port 80.
Now only Cloudfront can access the ALB. Unfortunately, by any Cloudfront Distribution. And we can still use the DNS of the Cloudfront Distribution (not only example.com
).
To configure the lambda and SNS subscription, please refer to the Cloudformation template and the README in the repository.
Signing a Cloudfront Distribution request to the origin
To make a Cloudfront Distribution the only source of truth for an ALB is quite simple. The Cloudfront Distribution must send a custom header to the origin (the ALB) and the ALB should forward the requests, only if the custom header is present in the request with the appropriate value, much like an API Token (step_2).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- Id: ReverseProxy
DomainName: !Ref ReverseProxyDns
CustomOriginConfig:
OriginProtocolPolicy: https-only
OriginReadTimeout: 60
OriginCustomHeaders:
- HeaderName: x-com-token
HeaderValue: !Ref SecurityToken
[...]
HttpsSecuredListenerRule:
Type: AWS::ElasticLoadBalancingV2::ListenerRule
Properties:
Actions:
- Type: forward
TargetGroupArn: !Ref ReverseProxyTargetGroup
Conditions:
- Field: http-header
HttpHeaderConfig:
HttpHeaderName: x-com-token
Values:
- !Ref SecurityToken
ListenerArn: !Ref HttpsAppListener
Priority: 1
Where the parameter SecurityToken
is a shared value between Cloudfront and the ALB.
Finally, we need to change the default action of the listener:
1
2
3
4
5
6
7
8
HttpsAppListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- Type: fixed-response
FixedResponseConfig:
StatusCode: 403
[...]
If a request doesn’t match the HttpsSecuredListenerRule
defined previously, the ALB will return a 403 error response.
Hosts Whitelisting
The last improvement we need is to allow access to the Cloudfront Distribution only with our domain name. And the best way to do this is to use a WAF and attach it to the Cloudfront Distribution (step_3). We could check the Host
header at the ALB level, but this check must be done as close to the user as possible, in other words, in edge locations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
WafValidHostsCondition:
Type: AWS::WAF::ByteMatchSet
Properties:
Name: waf-validhosts
ByteMatchTuples:
- FieldToMatch:
Type: HEADER
Data: host
TargetString: !Ref PublicDns
TextTransformation: NONE
PositionalConstraint: EXACTLY
WafValidHostsRule:
Type: AWS::WAF::Rule
Properties:
Name: waf-validhosts-rule
MetricName: WafValidHostsRule
Predicates:
- DataId: !Ref WafValidHostsCondition
Negated: true
Type: ByteMatch
WebAcl:
Type: AWS::WAF::WebACL
Properties:
Name: globalwebacl
DefaultAction:
Type: ALLOW
MetricName: GlobalWebACL
Rules:
- Action:
Type: BLOCK
Priority: 1
RuleId: !Ref WafValidHostsRule
Conclusion
With little effort with have secured a Cloudfront Distribution and an ALB behind it.
To summarize the steps have been as follow:
- Using Aliases instead of CNAMEs when possible
- Restricting access to the internet-facing ALB to Cloudfront only and only on HTTPS port 443
- Allowing access to the internet-facing ALB only to our Cloudfront distribution by exchanging a secret
- Using a WAF to allow access to our Cloudfront distribution from whitelisted Hosts