“Failed to determine the https port for redirect” in ASP.NET Core error logs

In June 2025, Microsoft released patch version 8.0.17 and 9.0.6 of .NET, which annoyingly contained a behaviourally breaking change in how HTTP header forwarding is enabled. Finding out about the change was pretty hard, as it wasn’t listed in the release notes but instead only mentioned in a GitHub issue off to the side.

Since this is a runtime change, it means that if you’re not being specific in the .NET framework version you’re pulling into your Docker image then just the act of deploying your existing application – having modified no source code or configuration at all – can result in the following behaviour:

  • An error in logs at startup, “Failed to determine the https post for redirect”
  • Sign-in errors for your users when using OpenID Connect or SAML sign-in, related to invalid redirect URLs being requested
  • URLs that your application generates (for example as redirects, or entity URLs in APIs) via ASP.NET utilities referring to HTTP rather than HTTPS

This only manifests in the following setup:

  • You’re using a proxy or load balancer in front of your application that is responsible for terminating TLS connections (like AWS Application Load Balancer)
  • You’re not using IIS to host the application

Cause and fix

.NET now validates that the incoming connection is on a whitelist of peers that are allowed to send X-Forwarded* headers to the application, when previously the absence of such a whitelist was treated as ‘accept any peer’.

The following code:

var forwardingOpts = new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedProto
};

app.UseForwardedHeaders(forwardingOpts);

behaves differently in .NET 8.0.17 and previous versions.

  • Before 8.0.17, the above meant ‘trust the X-ForwardedProto header from any source’
  • Since 8.0.17, the above meant ‘trust the X-ForwardedProto header for entries in the KnownNetworks/KnownProxies lists on the configuration only’

Since the previous behaviour was default-permissive, most applications (incorrectly) didn’t specify the set of known proxies and networks that should be trusted to supply valid X-Forwarded* headers.

The fix then is to specify one or other of KnownProxies or KnownNetworks with information describing the load balancer that’s actually terminating the client connection. For example, if we want to trust any LB within our VPC’s network and we’re using a CIDR of 10.20.0.0/16 for our VPC, we might configure KnownNetworks as:

forwardingOpts.KnownNetworks.Add(new IPNetwork(IPAddress.Parse("10.20.0.0"), 16));

Though – typically you want to be much tighter here, and potentially specify the actual IP addresses that the LB might present as rather than a broad IP range.

Cause (detailed)

When you’re using a load balancer (LB for short from here on out), clients aren’t hitting your application directly. Instead, they’re hitting the LB, which proxies or forwards the request to your application. The LB will often strip some headers from the original request before forwarding it, so the request your application sees is really only part of the actual picture.

For example, your application won’t know whether the client’s talking via HTTP or HTTPS, since it can’t see information about the actual request that was sent to the LB. From its point of view, the LB is the client, and it’s connecting over whatever port you’re exposing – which if you’re using Docker will likely be some non-TLS random port.

The LB can fill the picture in for your application by forwarding some of the information about the original client connection to the application in a special set of headers:

  • X-ForwardedFor – the original IP address of the connecting client
  • X-ForwardedHost – the host name that the client is connecting to
  • X-ForwardedProto – the protocol (HTTP or HTTPS) that the client used to connect to the LB

However this comes with security considerations – a permissive LB that just passes on these headers blindly would mean a client could connect with a fake set of X-Forwarded* headers, allowing a variety of attacks.

To mitigate this, the LB and the application need to cooperate a little:

  • The LB typically has to be configured to trust or reject incoming X-Forwarded* headers
  • The application has to be configured to accept or reject some or all X-ForwardedFor headers

Prior to .NET 8.0.17, the ForwardedHeadersMiddleware assumed that unless told otherwise it should accept X-Forwarded* headers from any source when told which headers to accept.

Since 8.0.17 the behaviour changed, so that if not told otherwise the middleware assumes that no source is permitted to send the headers.

The fix is to tell the ForwardedHeadersMiddleware exacrtly which IP addresses to expect traffic from where we’ll be accepting the X-Forwarded* headers. We do this by adding to the KnownNetworks and KnownProxies collections.

Deleting a Cloudformation stack containing a Cognito user pool can leave the user pool in an undeleteable state

When you delete a Cloudformation stack that contains stateful resources, typically Cloudformation will leave the stateful resources alone (you’ll see DELETE_SKIPPED for the resource) but delete the stateless ones.

Normally that’s what you want. But sometimes you can end up with that stateful, skipped resource being stuck in a subsequently undeleteable (or even unmodifiable) state because it still references a deleted, stateless resource like an IAM role.

This happens with Cognito in the following situation:

  • You’ve got a Cognito user pool in your stack
  • You’re defining an IAM role for Cognito to assume to send SMSes in the stack
  • The Cognito pool has deletion protection (within Cognito itself, not Cloudformation) turned on

When you delete the stack, the role gets deleted (it’s stateless, who cares) but the user pool does not. However, the user pool still has a reference to the deleted role.

Again, in a lot of cases this is not much of an issue – S3 buckets with handlers defined that have gone missing for example. But here, it means that we cannot modify the user pool.

But why do I need to modify the pool to delete it?

If you try to delete the pool through the AWS console, you’ll be asked if you want to turn off deletion protection first. You can do this by hand, too.

In both cases, the deletion process is actually two-step:

  • The user pool is modified to disable deletion protection;
  • The user pool is deleted.

With our user pool intact but its SMS-sending role destroyed, Cognito rejects any attempt to modify the user pool so long as the role cannot be found or doesn’t have the right privileges.

How do I get the pool into a modifiable/deletable state then?

You’ve some options:

  • Re-instate the role. The attempt to delete the user pool will tell you the role name in the InvalidSmsRoleTrustRelationshipException that’s described in the error banner’s ‘View details’ popover
    • If you’re doing this, the role must be named exactly the same, Cognito must have the ability to assume the role (cognito-idp.amazonaws.com as the principal), and the role must grant SMS send via SNS
  • Create a new role and modify Cognito to use it – functionally the same as the above
  • Disable SMS send from the pool so the role’s redundant before deleting

Getting the AWS Cognito advanced security authentication event for a given login

AWS Cognito advanced security lets you see authentication event details in the Cognito console and via the CLI and API. If you’re set up correctly, you’ll get:

  • Timestamp
  • The user’s ID and username
  • Event type
  • Success or failure
  • Risk level and risk decision (if adaptive security is enabled)
  • Any challenges presented during the event (like whether a password was used, or MFA was prompted)
  • A device summary – consisting of a browser name/version and operating system
  • IP address
  • City
  • Country
  • Whether compromised credentials were used
  • The user pool ID that triggered the event

You can look at auth events on a per-user basis in the console and via the CLI, but your filtering options are very limited. You cannot:

  • Query all events in a user pool by IP address
  • See whether multiple users are signing in from the same IP address
  • Identify a date-ordered list of suspicious or failed logins

If you want to do this, you probably want those events available to your own code so that you can store them in a database or similar.

You can choose to forward logs of these events to S3, Firehose or Cloudwatch Logs if you’re using the Cognito Plus plan – but the logs are generated post-hoc (that is – after the login has taken place), and asynchronously.

This means that you can’t get this kind of information directly from the authentication call your app might be making – like AdminInitiateAuth.

If you want to correlate auth events shipped to your log destination to requests to things like AdminInitiateAuth, helpfully Cognito ships the AWS requestId that was generated when making the request in the auth event payload.

So, if you want to correlate a sign-in with additional data later on (for example, if you already capture IP information and success/failure, but want to have an eventually-consistent store that includes some of the above fields) then you can record the requestId on the response’s ResponseMetadata object. Then you can hook a Lambda to Firehose or S3, parse the events as they arrive and use the request ID to identify which database records to create or update to avoid creating duplicates.

AWS Aurora Serverless V2 database doesn’t scale to zero after upgrading from Serverless V1

If you follow the migration guide to upgrade your Aurora Serverless V1 database to V2 (which a bunch of panicked folks are likely doing ahead of the deprecation date of 31st March), you might find that your new cluster doesn’t scale down to zero after the upgrade.

In my case, the fact that logical replication was enabled (rds.logical_replication) in a cluster-level paramter group prevented instances from scaling down to zero. Disabling logical replication got the correct auto-pause behaviour.

You’re directed to do this to enable blue/green deployments, but if you don’t need blue/green or else you don’t need logical replication after the upgrade then turning this off and rebooting the instances seems to cure the problem.

How do I know if my cluster is scaling to zero properly?

The easiest way to tell is to look at the Monitoring tab of the RDS page in the AWS Console, and the ACUUtilization metric in particular.

The ACUUtilization metric tells you what proportion of the maximum number of ACUs you’ve configured the cluster should scale to are in use at any given point – 100% means it’s running at 100% of the maximum capacity you configured, and 0% means it’s scaled to zero.

If you find that the chart never hits zero, but gets stuck at some specific percentage (in the above, you can see a baseline level of 25% activity) then this indicates that your cluster isn’t auto-pausing. You can determine what your minimum activity level is by dividing your minimum ACUs by your maximum – the above case has a minimum of 0.5 and a maximum of 2, so when running at 0.5 ACUs the database is using 0.5 / 2 = 25% of its capacity.

This might be because you have legitimate work being done in the database though – if you have idle database connections, the cluster will run at least at your minimum ACU level. You should also check the DatabaseConnections metric over the same time period.

If you’re seeing zero connections, but you’re still pinned to some minimum level of ACU utilisation then something might be blocking the cluster’s auto-pause functionality.

How can I tell what’s stopping the scaling to zero?

Easiest is to look at the instance logs.

  • In the RDS console, choose the instance you’re interested in
  • On the Logs & Events tab, in the Logs section at the bottom of the page find the log named ‘instance/instance.log’
  • You will probably find, every few minutes, a log entry along the lines of:
    [INFO] Auto-pause blockers registered since 2025-03-04T02:39:48.276Z: replication capability configured

CDK S3 ‘BucketDeployment’ doesn’t have to be slow – increase its memoryLimit parameter

If you’re deploying a static site to Cloudfront via CDK, you might be using the BucketDeployment construct to combine shipping a folder to S3 and causing a Cloudfront invalidation.

Behind the scenes, BucketDeployment creates a custom resource, a Lambda, that wraps a call to the AWS SDK’s s3 cp command to move files from the CDK staging area to the target S3 bucket.

While that’s happening within AWS’s infrastructure, the speed of that copy depends very strongly on the amount of resources the Lambda has – just like any other Lambda, CPU and network bandwidth scale with the requested memory limit.

The default memory limit for the custom resource Lambda is 128MiB – which is the smallest Lambda you can get, and accordingly the performance of that copy might be terrible if you have a lot of files, or large files, to transfer.

I’d strongly recommend upping that limit to 2048MiB or higher. This radically improved upload performance on two applications I deploy, with the upload rate going from @=~700KiB/s to >10MiB/s – a 10x increase.

This has a negligible cost implication as this Lambda only runs during a deployment, so shouldn’t be running all too frequently anyway. However the performance improvement is potentially dramatic for complex apps. We saw one build go from ~280s uploading to S3 come down to ~45s – an 84% reduction in that deployment step’s execution time, and about a 15% reduction in the deployment time of that stack overall – just for changing one parameter.

Bucket named ‘cdk-abcd12efg-assets-123456789-eu-west-1’ exists, but not in account 123456789. Wrong account?

When deploying a stack via CDK, you may encounter an error such as

Bucket named 'cdk-abcd12efg-assets-123456789-eu-west-1' exists, but not in account ***. Wrong account?

The most likely culprit here is that the role you’re using to deploy doesn’t have the right permissions on the staging bucket. CDK requires:

  • getBucketLocation
  • *Object
  • ListBucket

We hit this recently, and the underlying cause was that the IAM role used to deploy the stack had been amended to have a restricted set of permissions per least-privilege best practice. We’d deployed updates to the stack a number of times, but in this instance the particular change we were making required a re-upload of assets to the staging bucket, which uncovered the missing permission.

Cognito error: “Cannot use IP Address passed into UserContextData”

When using Cognito’s Advanced Security and adaptive authentication features, you need to ship contextual data about the logging-in user via the UserContextData type.

Some of this type data is collected via a Javascript snippet. However, you can also ship the user’s IP address (which the snippet cannot collect) in the same payload.

When doing so, you may get an error from Cognito:

“Cannot use IP Address passed into UserContextData”

Unhelpful error from Cognito

This is likely because you’ve not enabled ‘Accept additional user context data‘ on your user pool client – though the error message is pretty opaque.

You can do this in a number of ways:

  • Via the AWS console
  • Via the UpdateUserPoolClient CLI function
  • Via CDK, if you drop down to the Level 1 construct and set “enablePropagateAdditionalUserContextData: true” on your CfnUserPoolClient

Even the latest L2 constructs for Cognito don’t seem to support setting enablePropagateAdditionalUserContextData when controlling a user pool client via CDK, but using the L1 escape hatch is easy enough:

const cfnUserPoolClient = userPoolClient.node.defaultChild as CfnUserPoolClient;
cfnUserPoolClient.enablePropagateAdditionalUserContextData = true;

GitHub Actions, ternary operators and default values

Github Actions ‘type’ metadata on custom action or workflow inputs is, pretty much, just documentation – it doesn’t seem to be enforced, at least when it comes to supplying a default value. That means that just because you’ve claimed it’s a bool doesn’t make it so.

And worse, it seems that default values get coerced to strings if you use an expression.

At TILLIT we have custom GitHub composite actions to perform various tasks during CI. We recently hit a snag with one roughly structured as follows

name: ...
inputs:
   readonly:
      type: boolean
      default: ${{ some logic here }}

runs:
  using: "composite"
  steps:
    - name: ...
      uses: ...
      with:
        some-property: ...${{ inputs.readonly && 'true-val' || 'false-val' }}...

That mess in the some-property definition is the closest you can get in Github Actions to a ternary operator in the absence of any if-like construct, where you want to format a string based on some boolean.

In our case – the ‘true’ path was the only path ever taken. Diagnostic logging on the action showed that inputs.readonly was ‘false’. Wait, are those quotes?

Of course they are! The default value ended up being set to be a string, even though the input’s default value expression is purely boolean in nature and it’s specified as being a boolean.

The fix then is to our ternary, and to be very explicit as to the comparison being made.

with:
  some-property: ...${{ inputs.readonly == 'true' && 'true-val' || 'false-val' 

AWS SAM error “[ERROR] (rapid) Init failed error=Runtime exited with error: signal: killed InvokeID=” in VS Code

When debugging a lambda using the AWS Serverless Application Model tooling (the CLI and probably VS Code extensions), you might find that your breakpoint isn’t getting hit and you instead see an error in the debug console:

[ERROR] (rapid) Init failed error=Runtime exited with error: signal: killed InvokeID=" in VS Code

A thing to check is whether you’re running out of RAM or timing out in execution:

  • Open your launch.json file for the workspace
  • In your configuration, under the lambda section, add a specific memoryMb value – in my case 512 got me moving

This is incredibly frustrating because the debug console gives you no indication as to why the emulator terminated your lambda – but also helpful, because you can tell how large you need to specify your lambda when you deploy it ahead of time.

Invalid Request error when creating a Cloudfront response header policy via Cloudformation

I love Cloudformation and CDK, but sometimes neither will show an issue with your template until you actually try to deploy it.

Recently we hit a stumbling block while creating a Cloudfront response header policy for a distribution using CDK. The cdk diff came out looking correct, no issues there – but on deploying we hit an Invalid Request error for the stack.

An error displayed in the Cloudfront 'events' tab, indicating that there was an Invalid Request but giving no further clues
Cloudformation often doesn’t give much additional colour when you hit a stumbling block

The reason? We’d added a temporarily-disabled XSS protection header, but kept in the reporting URL so that when we turned it on it’d be correctly configured. However, Cloudfront rejects the creation of the policy if you spec a reporting URL on a disabled header setup.

The Cloudfront resource policy docs make it pretty clear this isn’t supported, but Cloudformation can’t validate it for us

A screenshot of a validation error message indicating that X-XSS-Protection cannot contain a Report URI when protection is disabled
Just jumping into the console to try creating the resource by hand is often the most effective debugging technique

How to diagnose Invalid Request errors with Cloudformation

A lot of the time the easiest way to diagnose a Invalid Request error when deploying a Cloudformation is to just do it by hand in the console in a test account, and see what breaks. In this instance, the error was very clear and it was a trivial patch to fix up the Cloudformation template and get ourselves moving.

Unfortunately, Cloudformation often doesn’t give as much context as the console when it comes to validation errors during stack creation – but hand-cranking the affected resource both gives you quicker feedback and a better feel for what the configuration options are and how they hang together.

A rule of thumb is that if you’re getting an Invalid Request back, chances are it’s essentially a validation error on what you’ve asked Cloudformation to deploy. Check the docs, simplify your test case to pinpoint the issue and don’t be afraid to get your hands dirty in the console.