Skip to content

Commit

Permalink
feat: Harden load balancers (#2223)
Browse files Browse the repository at this point in the history
* feat: Add TLS version and cipher suite headers

* feat: drop invalid header fields in HTTP requests

* chore: Add changeset description

* test: Update tests

* chore: update semver and clarify invalid headers
  • Loading branch information
marsavar authored Mar 7, 2024
1 parent f5d08fd commit 5fead41
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 6 deletions.
6 changes: 6 additions & 0 deletions .changeset/forty-spies-heal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@guardian/cdk": major
---

- Load balancers now add headers with information about the TLS version and cipher suite used during negotiation
- Load balancers now drop invalid headers before forwarding requests to the target. Invalid headers are described as HTTP header names that do not conform to the regular expression [-A-Za-z0-9]+
24 changes: 24 additions & 0 deletions src/constructs/cloudwatch/__snapshots__/ec2-alarms.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ exports[`The GuAlb4xxPercentageAlarm construct should create the correct alarm r
"Key": "deletion_protection.enabled",
"Value": "true",
},
{
"Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled",
"Value": "true",
},
{
"Key": "routing.http.drop_invalid_header_fields.enabled",
"Value": "true",
},
],
"Scheme": "internal",
"SecurityGroups": [
Expand Down Expand Up @@ -245,6 +253,14 @@ exports[`The GuAlb5xxPercentageAlarm construct should create the correct alarm r
"Key": "deletion_protection.enabled",
"Value": "true",
},
{
"Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled",
"Value": "true",
},
{
"Key": "routing.http.drop_invalid_header_fields.enabled",
"Value": "true",
},
],
"Scheme": "internal",
"SecurityGroups": [
Expand Down Expand Up @@ -480,6 +496,14 @@ exports[`The GuUnhealthyInstancesAlarm construct should create the correct alarm
"Key": "deletion_protection.enabled",
"Value": "true",
},
{
"Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled",
"Value": "true",
},
{
"Key": "routing.http.drop_invalid_header_fields.enabled",
"Value": "true",
},
],
"Scheme": "internal",
"SecurityGroups": [
Expand Down
40 changes: 37 additions & 3 deletions src/constructs/loadbalancing/alb/application-load-balancer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { Match, Template } from "aws-cdk-lib/assertions";
import { Vpc } from "aws-cdk-lib/aws-ec2";
import { GuTemplate, simpleGuStackForTesting } from "../../../utils/test";
import type { AppIdentity } from "../../core";
import { GuApplicationLoadBalancer } from "./application-load-balancer";
import {
DROP_INVALID_HEADER_FIELDS_ENABLED,
GuApplicationLoadBalancer,
TLS_VERSION_AND_CIPHER_SUITE_HEADERS_ENABLED,
} from "./application-load-balancer";

const vpc = Vpc.fromVpcAttributes(new Stack(), "VPC", {
vpcId: "test",
Expand Down Expand Up @@ -54,12 +58,12 @@ describe("The GuApplicationLoadBalancer class", () => {
new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
LoadBalancerAttributes: [
LoadBalancerAttributes: Match.arrayWith([
{
Key: "deletion_protection.enabled",
Value: "true",
},
],
]),
});
});

Expand All @@ -70,4 +74,34 @@ describe("The GuApplicationLoadBalancer class", () => {

Template.fromStack(stack).hasOutput("ApplicationLoadBalancerTestingDnsName", {});
});

it("adds headers that include the TLS version and the cipher suite used during negotiation", () => {
const stack = simpleGuStackForTesting();

new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
LoadBalancerAttributes: Match.arrayWith([
{
Key: TLS_VERSION_AND_CIPHER_SUITE_HEADERS_ENABLED,
Value: "true",
},
]),
});
});

it("drops invalid headers before forwarding requests to the target", () => {
const stack = simpleGuStackForTesting();

new GuApplicationLoadBalancer(stack, "ApplicationLoadBalancer", { ...app, vpc });

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
LoadBalancerAttributes: Match.arrayWith([
{
Key: DROP_INVALID_HEADER_FIELDS_ENABLED,
Value: "true",
},
]),
});
});
});
18 changes: 18 additions & 0 deletions src/constructs/loadbalancing/alb/application-load-balancer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ import type { ApplicationLoadBalancerProps, CfnLoadBalancer } from "aws-cdk-lib/
import { GuAppAwareConstruct } from "../../../utils/mixin/app-aware-construct";
import type { AppIdentity, GuStack } from "../../core";

/**
* Adds the following headers to each request before forwarding it to the target:
* - `x-amzn-tls-version`, which has information about the TLS protocol version negotiated with the client
* - `x-amzn-tls-cipher-suite`, which has information about the cipher suite negotiated with the client
*
* Both headers are in OpenSSL format.
*/
export const TLS_VERSION_AND_CIPHER_SUITE_HEADERS_ENABLED = "routing.http.x_amzn_tls_version_and_cipher_suite.enabled";

/**
* Indicates whether HTTP headers with invalid header fields are removed by the load balancer.
* Invalid headers are described as HTTP header names that do not conform to the regular expression [-A-Za-z0-9]+
*/
export const DROP_INVALID_HEADER_FIELDS_ENABLED = "routing.http.drop_invalid_header_fields.enabled";

interface GuApplicationLoadBalancerProps extends ApplicationLoadBalancerProps, AppIdentity {
/**
* If your CloudFormation does not define the Type of your Load Balancer, you must set this boolean to true to avoid
Expand All @@ -27,6 +42,9 @@ export class GuApplicationLoadBalancer extends GuAppAwareConstruct(ApplicationLo
constructor(scope: GuStack, id: string, props: GuApplicationLoadBalancerProps) {
super(scope, id, { deletionProtection: true, ...props });

this.setAttribute(TLS_VERSION_AND_CIPHER_SUITE_HEADERS_ENABLED, "true");
this.setAttribute(DROP_INVALID_HEADER_FIELDS_ENABLED, "true");

if (props.removeType) {
const cfnLb = this.node.defaultChild as CfnLoadBalancer;
cfnLb.addPropertyDeletionOverride("Type");
Expand Down
16 changes: 16 additions & 0 deletions src/patterns/ec2-app/__snapshots__/base.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,14 @@ exports[`the GuEC2App pattern can produce a restricted EC2 app locked to specifi
"Key": "deletion_protection.enabled",
"Value": "true",
},
{
"Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled",
"Value": "true",
},
{
"Key": "routing.http.drop_invalid_header_fields.enabled",
"Value": "true",
},
],
"Scheme": "internet-facing",
"SecurityGroups": [
Expand Down Expand Up @@ -1388,6 +1396,14 @@ exports[`the GuEC2App pattern should produce a functional EC2 app with minimal a
"Key": "deletion_protection.enabled",
"Value": "true",
},
{
"Key": "routing.http.x_amzn_tls_version_and_cipher_suite.enabled",
"Value": "true",
},
{
"Key": "routing.http.drop_invalid_header_fields.enabled",
"Value": "true",
},
],
"Scheme": "internet-facing",
"SecurityGroups": [
Expand Down
6 changes: 3 additions & 3 deletions src/patterns/ec2-app/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,12 +656,12 @@ describe("the GuEC2App pattern", function () {
});

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
LoadBalancerAttributes: [
LoadBalancerAttributes: Match.arrayWith([
{ Key: "deletion_protection.enabled", Value: "true" },
{ Key: "access_logs.s3.enabled", Value: "true" },
{ Key: "access_logs.s3.bucket", Value: { Ref: "AccessLoggingBucket" } },
{ Key: "access_logs.s3.prefix", Value: "access-logging-prefix" },
],
]),
});
});

Expand All @@ -685,7 +685,7 @@ describe("the GuEC2App pattern", function () {
});

Template.fromStack(stack).hasResourceProperties("AWS::ElasticLoadBalancingV2::LoadBalancer", {
LoadBalancerAttributes: Match.arrayEquals([
LoadBalancerAttributes: Match.arrayWith([
{
Key: "deletion_protection.enabled",
Value: "true",
Expand Down

0 comments on commit 5fead41

Please sign in to comment.