CloudFront not sending custom headers to origin for additional behavior

0

Situation

I am currently in the process of migrating on of my pet projects from another provider to AWS. As a first step, I have created a CloudFront distribution sending all requests as-is to the loadbalancer my application is currently running on (external provider).

The CDK stack I started with looks like follows:

package mypackagename;

import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;
import software.amazon.awscdk.services.certificatemanager.Certificate;
import software.amazon.awscdk.services.cloudfront.*;
import software.amazon.awscdk.services.cloudfront.origins.HttpOrigin;
import software.constructs.Construct;

import java.util.List;
import java.util.Map;

public class MyServiceNameCloudfrontCdkStack extends Stack {

    public MyServiceNameCloudfrontCdkStack(final Construct scope, final String id, final StackProps props, final Config config) {
        super(scope, id, props);

        Distribution.Builder.create(this, "cloudfront")
                .priceClass(PriceClass.PRICE_CLASS_ALL)
                .httpVersion(HttpVersion.HTTP2)
                .enableIpv6(true)
                .domainNames(List.of(config.domain()))
                .certificate(Certificate.fromCertificateArn(this, "sslcert", config.sslCertArn()))
                .minimumProtocolVersion(SecurityPolicyProtocol.TLS_V1_2_2021)
                .defaultBehavior(
                        BehaviorOptions.builder()
                                .origin(
                                        HttpOrigin.Builder.create("<hostname of the LB at external provider>")
                                                .protocolPolicy(OriginProtocolPolicy.HTTP_ONLY)
                                                .httpPort(8080)
                                                .customHeaders(Map.of(
                                                        "Forwarded", String.format("host=%s;proto=https", config.domain()),
                                                        "X-Forwarded-Host", config.domain(),
                                                        "X-Forwarded-Proto", "https",
                                                        "X-Forwarded-Port", "443"
                                                ))
                                                .build()
                                )
                                .compress(true)
                                .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
                                .allowedMethods(AllowedMethods.ALLOW_ALL)
                                .cachePolicy(CachePolicy.CACHING_DISABLED)
                                .originRequestPolicy(OriginRequestPolicy.ALL_VIEWER)
                                .build()
                )
                .enableLogging(false)
                .enabled(true)
                .build();
    }

    public record Config(String domain, String sslCertArn) {}
}

With this stack, everything works as expected.

As a second step, I updated the CDK stack to have separate behaviors for each of the components I'm planning to split my logic to in the future. They all still use the same origin but with some minor changes to the behavior.

The updated CDK stack looks like follows:

package mypackagename;

import software.amazon.awscdk.Stack;
import software.amazon.awscdk.StackProps;
import software.amazon.awscdk.services.certificatemanager.Certificate;
import software.amazon.awscdk.services.cloudfront.*;
import software.amazon.awscdk.services.cloudfront.origins.HttpOrigin;
import software.constructs.Construct;

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

public class MyServiceNameCloudfrontCdkStack extends Stack {

    public MyServiceNameCloudfrontCdkStack(final Construct scope, final String id, final StackProps props, final Config config) {
        super(scope, id, props);

        // region custom response headers for the fallthrough behaviour (ui stuff)
        final ResponseHeadersPolicy uiResponseHeadersPolicy = ResponseHeadersPolicy.Builder.create(this, "ui-response-headers-policy")
                .securityHeadersBehavior(
                        ResponseSecurityHeadersBehavior.builder()
                                .frameOptions(
                                        ResponseHeadersFrameOptions.builder()
                                                .frameOption(HeadersFrameOption.DENY)
                                                .override(true)
                                                .build()
                                )
                                .contentSecurityPolicy(
                                        ResponseHeadersContentSecurityPolicy.builder()
                                                .contentSecurityPolicy(String.join("; ",
                                                        "default-src 'self'",
                                                        "connect-src 'self' https://api.guildwars2.com",
                                                        "script-src 'self' 'unsafe-inline'",
                                                        "style-src 'self' 'unsafe-inline'",
                                                        "img-src 'self' https://icons-gw2.darthmaim-cdn.com/ data:",
                                                        "frame-src https://www.youtube.com/embed/"
                                                ))
                                                .override(true)
                                                .build()
                                )
                                .build()
                )
                .build();
        // endregion

        // region the external loadbalancer origin config
        final IOrigin externalLBOrigin = HttpOrigin.Builder.create("<hostname of the LB at external provider>")
                .protocolPolicy(OriginProtocolPolicy.HTTP_ONLY)
                .httpPort(8080)
                .customHeaders(Map.of(
                        "Forwarded", String.format("host=%s;proto=https", config.domain()),
                        "X-Forwarded-Host", config.domain(),
                        "X-Forwarded-Proto", "https",
                        "X-Forwarded-Port", "443"
                ))
                .build();
        // endregion

        // region additional behaviours (everything except ui)
        final Map <String , BehaviorOptions> additionalBehaviors = new LinkedHashMap<>();
        additionalBehaviors.put(
                "/api*",
                BehaviorOptions.builder()
                        .origin(externalLBOrigin)
                        .compress(true)
                        .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
                        .allowedMethods(AllowedMethods.ALLOW_ALL)
                        .cachePolicy(CachePolicy.CACHING_DISABLED)
                        .originRequestPolicy(OriginRequestPolicy.ALL_VIEWER)
                        .build()
        );

        additionalBehaviors.put(
                "/oauth2*",
                BehaviorOptions.builder()
                        .origin(externalLBOrigin)
                        .compress(true)
                        .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
                        .allowedMethods(AllowedMethods.ALLOW_ALL)
                        .cachePolicy(CachePolicy.CACHING_DISABLED)
                        .originRequestPolicy(OriginRequestPolicy.ALL_VIEWER)
                        .build()
        );

        additionalBehaviors.put(
                "/.well-known/oauth-authorization-server",
                BehaviorOptions.builder()
                        .origin(externalLBOrigin)
                        .compress(true)
                        .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
                        .allowedMethods(AllowedMethods.ALLOW_GET_HEAD)
                        .cachePolicy(CachePolicy.CACHING_DISABLED)
                        .build()
        );
        // endregion

        Distribution.Builder.create(this, "cloudfront")
                .priceClass(PriceClass.PRICE_CLASS_ALL)
                .httpVersion(HttpVersion.HTTP2)
                .enableIpv6(true)
                .domainNames(List.of(config.domain()))
                .certificate(Certificate.fromCertificateArn(this, "sslcert", config.sslCertArn()))
                .minimumProtocolVersion(SecurityPolicyProtocol.TLS_V1_2_2021)
                .defaultBehavior(
                        BehaviorOptions.builder()
                                .origin(externalLBOrigin)
                                .compress(true)
                                .viewerProtocolPolicy(ViewerProtocolPolicy.REDIRECT_TO_HTTPS)
                                .allowedMethods(AllowedMethods.ALLOW_GET_HEAD)
                                .cachePolicy(CachePolicy.CACHING_OPTIMIZED)
                                .originRequestPolicy(OriginRequestPolicy.ALL_VIEWER)
                                .responseHeadersPolicy(uiResponseHeadersPolicy)
                                .build()
                )
                .additionalBehaviors(additionalBehaviors)
                .enableLogging(false)
                .enabled(true)
                .build();
    }

    public record Config(String domain, String sslCertArn) {}
}

Issue

Most of the changes work as expected (for example, I see that caching now takes place for the default behavior).

BUT: For /oauth2* requests, CloudFront does not send all or at least some of the defined customHeaders to the origin server. I don't know if it also affects the other behaviors, but I know for sure it does affect the /oauth2* behavior.

This is especially weird because (as expected) the resulting CloudFront Distribution shows only one Origin, which correctly lists the Custom Headers I have set in CDK code.

When rolling back to the previous version of my CDK stack everything works as expected again.

EDIT:

I further tested this weird behavior using a HTTP Echo server instead of the Loadbalancer as a origin.

CloudFront does not always send the custom header X-Forwarded-Proto to the origin. This is can be seen almost 100% on the default behavior in the updated CDK stack (since I enabled caching for this behavior there). For all other behaviors (where caching is disabled), the X-Forwarded-Proto is being sent by CloudFront frequently, but not always.

Versions

Maven Versions:

<cdk.version>2.46.0</cdk.version>
<constructs.version>[10.0.0,11.0.0)</constructs.version>

cdk.out:

{"version":"21.0.0"}
1 Answer
0

According to the docs (https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/RequestAndResponseBehaviorCustomOrigin.html), the X-Forwarded-Proto header is not officially supported.

I was able to migrate to the Forwarded header by changing by Spring configuration from

server:
  forward-headers-strategy: native

to

server:
  forward-headers-strategy: framework

The Framework-Strategy makes use of the Forwarded header, which is consistently being sent by CloudFront.

Felix
answered a year ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.

Guidelines for Answering Questions