diff --git a/.changeset/forty-spies-heal.md b/.changeset/forty-spies-heal.md new file mode 100644 index 000000000..78f47100b --- /dev/null +++ b/.changeset/forty-spies-heal.md @@ -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]+ diff --git a/src/constructs/cloudwatch/__snapshots__/ec2-alarms.test.ts.snap b/src/constructs/cloudwatch/__snapshots__/ec2-alarms.test.ts.snap index 23efbc83b..5476becd0 100644 --- a/src/constructs/cloudwatch/__snapshots__/ec2-alarms.test.ts.snap +++ b/src/constructs/cloudwatch/__snapshots__/ec2-alarms.test.ts.snap @@ -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": [ @@ -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": [ @@ -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": [ diff --git a/src/constructs/loadbalancing/alb/application-load-balancer.test.ts b/src/constructs/loadbalancing/alb/application-load-balancer.test.ts index bf6a5c519..3e85939b6 100644 --- a/src/constructs/loadbalancing/alb/application-load-balancer.test.ts +++ b/src/constructs/loadbalancing/alb/application-load-balancer.test.ts @@ -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", @@ -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", }, - ], + ]), }); }); @@ -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", + }, + ]), + }); + }); }); diff --git a/src/constructs/loadbalancing/alb/application-load-balancer.ts b/src/constructs/loadbalancing/alb/application-load-balancer.ts index d7d9ff90a..0a3f0a660 100644 --- a/src/constructs/loadbalancing/alb/application-load-balancer.ts +++ b/src/constructs/loadbalancing/alb/application-load-balancer.ts @@ -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 @@ -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"); diff --git a/src/patterns/ec2-app/__snapshots__/base.test.ts.snap b/src/patterns/ec2-app/__snapshots__/base.test.ts.snap index e42711371..9924ba78a 100644 --- a/src/patterns/ec2-app/__snapshots__/base.test.ts.snap +++ b/src/patterns/ec2-app/__snapshots__/base.test.ts.snap @@ -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": [ @@ -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": [ diff --git a/src/patterns/ec2-app/base.test.ts b/src/patterns/ec2-app/base.test.ts index 15cc4e2c5..e59f6ea95 100644 --- a/src/patterns/ec2-app/base.test.ts +++ b/src/patterns/ec2-app/base.test.ts @@ -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" }, - ], + ]), }); }); @@ -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",