? Nginx "map" is super useful!
— Chris Fidao (@fideloper) May 23, 2018
? Nginx sets incoming headers to variables ($http_foo)
So, here we set X-Forwarded-Proto to the value of $cloudfront_proto that "map" creates by testing for two possible "proto" headers!https://t.co/VkNRMpDDqr pic.twitter.com/6CAXEhiA29
Tiny note here: This video's audio quality isn't as good as usual (and I apologize for my clicky keyboard! That won't be the usual :D
When we use our applications behind some sort of proxy, we usually need to make the application aware it's behind a proxy.
This lets the application know to use the Forwarded
or the X-Forwarded-*
headers to know the protocol/schema (http vs https), port, and real client IP address.
For Laravel, the TrustedProxy package does this. It uses the underlying Symfony HTTP Request class and sets a proxy as "trusted". If the proxy is trusted, and has either Forwarded
or X-Forwarded-*
headers set, then the application uses those headers.
The Problem
The problem is that Symfony (as of Symfony 4) currently only allows those two header sets to be used.
- The
Forwarded
header, which contains all the client information in the header value (client ip, protocol, port, host) - The
X-Forwarded-*
headers (-Port
,-Proto
,-For
,-Host
)
However, we aren't always lucky enough to have these two headers be available to use. Notably, AWS Cloudfront only provides the Cloudfront-Forwarded-Proto
header for passing along the schema (http vs https).
Cloudfront will, however, add the
X-Forwarded-For
header. I'm not sure why they strip out the otherX-Forwarded-*
headers. You can see Cloudfront's header behavior here.
The Solution
So, in our case, our application won't correctly read the Cloudfront-Forwarded-Proto
header that our web server receives.
Luckily, we can use Nginx to remap the Cloudfront header to a header of our choosing!
We'll do this while still avoiding "if" statements inside of Nginx configuration.
Our tool of choice here will be Nginx's map
feature.
The first thing to note is that map
is in the http
context, not within the server
context, so we need to configure it outside of any particular server {...}
block. This may mean that it could affect other or all server
's configured within Nginx.
Map
Here's what it looks like!
upstream app {
server 127.0.0.1:8080;
}
map $http_cloudfront_forwarded_proto $cloudfront_proto {
default "http";
https "https";
}
server {
listen 80 default_server;
root /var/www/html;
index index.html index.htm;
server_name front.example.org;
charset utf-8;
location / {
include proxy_params;
proxy_set_header X-Forwarded-Proto $cloudfront_proto;
proxy_pass http://app;
proxy_redirect off;
# Handle Web Socket connections
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Here Nginx is proxying to an application listening on http port 8080.
Outside of the server {...}
block (which is inside of Nginx's http {...}
block, altho that's not necessariy obvious) we set a map:
map $http_cloudfront_forwarded_proto $cloudfront_proto {
default "http";
https "https";
}
The variable $http_cloudfront_forwarded_proto
is provided for us. Nginx takes any HTTP headers, lower-cases them, and converts dashes to underscores. They become accessible as variables starting with $http_
.
So, our map
function here is reading values for HTTP header Cloudfront-Forwarded-Proto
via $http_cloudfront_forwarded_proto
. We're telling Nginx to map another value onto a new variable that we created named $cloudfront_proto
.
Inside of the map, everything on the left are possible values for the $http_cloudfront_forwarded_proto
variable. If one matches, then the value on the right is given to variable $cloudfront_proto
.
Using map
lets us:
- Set a default
- Arbitrarily set the value of our mapped variable
In this case, we're defaulting to http
when Cloudfront-Forwarded-Proto
is either missing or has a value other than https
.
Passing the Header
In our server
configuration, we pass that header off to our application via proxy_set_header
:
location / {
include proxy_params;
proxy_set_header X-Forwarded-Proto $cloudfront_proto;
proxy_pass http://app;
proxy_redirect off;
}
We create header X-Forwarded-Proto
(which our application will use!) and give it the value of our mapped/created variable $cloudfront_proto
.
Edge Cases
One issue with our configuration above is that it would ignore any value of X-Forwaded-Proto
if it was present!
To take advantage of the case where X-Forwarded-Proto
was included (and so we don't ignore it), I use this more complex mapping:
map "$http_cloudfront_forwarded_proto:$http_x_forwarded_proto" $cloudfront_proto {
default "http";
":http" "http";
":https" "https";
"http:" "http";
"https:" "https";
"https:http" "https";
"http:https" "https";
"https:https" "https";
}
Some things to note:
- We can create a string from multiple variables to map from. In this case, I combine
X-Forwaded-Proto
andCloudfront-Forwarded-Proto
, concatenating them together with a:
in betwen. - I then test possible combinations of the resulting string, with a preference to use
https
in the cases where both headers are set but conflict with eachother.
Since our default is http
, we can actually get rid of any of the combinations that result in the value http
being used:
map "$http_cloudfront_forwarded_proto:$http_x_forwarded_proto" $cloudfront_proto {
default "http";
":https" "https";
"https:" "https";
"https:http" "https";
"http:https" "https";
"https:https" "https";
}
It's still a bit ugly, but it works!
This can be be simplified greatly with the use of a regular expression:
map "$http_cloudfront_forwarded_proto:$http_x_forwarded_proto" $cloudfront_proto {
default "http";
"~https" "https";
}
That's much cleaner and provides the same functionality. Any instance of "https" in the header value will set the X-Forwarded-Proto
header value to https
.
FastCGI & PHP
In my example above, we had Nginx proxying over HTTP to our application code. What if we're using FastCGI instead (for example, to proxy to a PHP application using PHP-FPM)?
FastCGI in Nginx has no equivalent of proxy_set_header
, since it doesn't actually send an HTTP request to PHP. Instead, Nginx (following FastCGI spec and PHP convention) converts headers to proxy_params
, which get sent to PHP-FPM.
For PHP, we can set a new proxy param that would get read as an HTTP header by PHP. These all start with HTTP_
within PHP's global $_SERVER
variable.
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
fastcgi_param HTTP_X_FORWARDED_PROTO $cloudfront_proto;
}
Here we used fastcgi_param to include what PHP will read as the HTTP header X-Forwarded-Proto
!
We set X-Forwarded-Proto
to the value of our mapped variable $cloudfront_proto
via this line in our configuration:
fastcgi_param HTTP_X_FORWARDED_PROTO $cloudfront_proto;
Result
Now our application can read the X-Forwared-Proto
header that it expects to find! We can use this technique to map any custom/unexpected headers to ones that are more commonly used/expected in our applications this way.