Generating HTTPS URLs with Symfony on DigitalOcean App Platform
I’ve been dealing with a frustrating issue in a Symfony application hosted on DigitalOcean App Platform. Since it took me quite a while to find the solution, I thought I’d share it here in case it helps someone else.
By default, every application running on App Platform includes Cloudflare CDN as a reverse proxy. This provides a number of benefits, especially if your application sets appropriate cache-control and etag headers. Cloudflare will use these to cache your application’s responses at the edge, thus reducing load on your servers. So far, so good.
Cloudflare also terminates TLS, which means the traffic is decrypted before being forwarded to DigitalOcean. This presents a problem for my application. It thinks it’s receiving traffic over HTTP rather than HTTPS. As a result, Symfony generates URLs with the http:// scheme rather than https://. This isn’t a major problem, since everything is configured to redirect to https://, but it requires an additional network request, and it’s one of those tiny details that will bother me until I figure out how to fix it.
It’s one of those tiny details that will bother me until I figure out how to fix it.
Meanwhile, Symfony’s documentation isn’t clear about how to solve this. Three pages came up in my search for a solution:
- How to Force HTTPS or HTTP for different URLs
- Routing: Forcing HTTPS on Generated URLs
- How to Configure Symfony to Work behind a Load Balancer or Reverse Proxy
The first isn’t relevant to my problem. It’s about forcing HTTPS or HTTP for different URLs, not about generating HTTPS URLs when behind a reverse proxy. Plus, it has this scary warning at the bottom of the page:
Forcing HTTPS while using a reverse proxy or load balancer requires a proper configuration to avoid infinite redirect loops.
The second one indicates it’s for use when generating URLs in non-HTTP requests (e.g., from CLI tools, cron jobs, email sending tasks, etc.). It also includes the same scary warning:
If your server runs behind a proxy that terminates SSL, make sure to configure Symfony to work behind a proxy.
The configuration for the scheme is only used for non-HTTP requests. The schemes option together with incorrect proxy configuration will lead to a redirect loop.
That warning also links to the third page, which is relevant to my problem. However, anything involving network configuration and CIDR notation has always intimidated me. I was afraid of breaking connectivity to the application, and the only way I could find out whether it worked was to try it in production.
In the end, I needed to make two small changes to my application, neither of which are explicitly documented anywhere:
-
I replaced
x-forwarded-forwith the value ofdo-connecting-ip.The note on using
do-connecting-ipinstead ofx-forwarded-foris buried deep in the DigitalOcean documentation, and if you’ve never encountered it, it can come as a surprise. I’ve been around long enough to know this is a common thing that reverse proxies do, so I knew what to look for.For Symfony to find the client’s correct IP address, you need to rewrite this header early in the request lifecycle. I did this by adding the following to my
index.phpabove where the Symfony kernel is instantiated:// Map DigitalOcean's custom IP header to the standard x-forwarded-for header. if (isset($_SERVER['HTTP_DO_CONNECTING_IP'])) { $_SERVER['HTTP_X_FORWARDED_FOR'] = $_SERVER['HTTP_DO_CONNECTING_IP']; }I only knew to do this because Symfony shows an example with a different
x-forwarded-*header. -
I added the following to my
config/packages/framework.yamlfile:framework: trusted_proxies: '127.0.0.1,100.64.0.0/10' trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port']What’s important to note here is the value
100.64.0.0/10in the list of trusted proxies. I could have usedREMOTE_ADDRas suggested in the documentation, but that felt insecure. I tried usingPRIVATE_SUBNETS, also suggested in the documentation, but DigitalOcean uses a subnet not found in Symfony’s list of private subnets.What’s more, I couldn’t find this subnet listed in the DigitalOcean documentation. However, it’s defined in RFC 6598 as the reserved IPv4 prefix for Shared Address Space. I stumbled on it when I noticed the
REMOTE_ADDRvalue on requests to my application began with100.127, which wasn’t in Symfony’s list. So I searched to find whether it had any special meaning, and sure enough, it does.
After making these changes, $request->getClientIp() now returns the correct client IP address instead of the IP address of the ingress proxy. I no longer need to use $request->getHeaders()->get('do-connecting-ip') for that value. I am also able to use {{ url('home') }} in my Twig templates instead of the hacky and ugly https://{{ url('home', {}, true) }} I was using before.
Leave a Comment
Replying to · Cancel
Feel free to leave a comment. Comments will appear above. They are heavily moderated, so please be patient while I approve them.