Nginx: Proper way to use different reverse proxy upstream based on user-agent without using if

Written by - 3 comments

Published on - Listed in Nginx


Using if in Nginx's configs is considered bad, even called evil (see if is evil). Although a lot of Nginx configs exist where conditions are set using if (and they mostly work, too!), it is encouraged to use another approach.

In this particular scenario, traffic being identified from a certain user-agent should be using a different backend/upstream than the default traffic. An if rule would have been very easy:

server {
[...]
    if ($http_user_agent ~ mybot ) {
        proxy_pass http://127.0.0.1:8090;
    }

    proxy_pass http://127.0.0.1:8080;
[...]
}

But once again: We don't want to use if!

map to the rescue!

In the past an almost similar solution had to be achieved; see article different proxy_pass upstream depending on client ip address in Nginx. The difference? A client IP address or range should use a "geo" map (to be able to use ranges). User agents are strings therefore map can be used without a problem.

The map should be defined before the server context (definitely not within):

map "$http_user_agent" $targetupstream {
  default        http://127.0.0.1:8080;
  "~^mybot"      http://127.0.0.1:8090;
}

What this does is:

  • Nginx takes the User-Agent header from the HTTP request (using $http_user_agent variable) and compares the value with column 1 of the map.
  • In this map, column 1 has two entries: "default" and "~^mybot".
  • If the user-agent from the request matches the regular expression of "~^mybot", it will save column 2 of that match (http://127.0.0.1:8090) as variable $targetupstream. This variable can be used in configs following this map definition.
  • If the user-agent does not match any entries, Nginx will use the "default" entry (saving http://127.0.0.1:8080 as $targetupstream variable).

Dynamic proxy_pass

The map definition above means that the upstream is now stored in a variable $targetupstream. This means: The upstream URL is now dynamic! Nginx will do the mapping according to (here) the request's user-agent. All that needs to be done is to use the variable as proxy_pass:

# HTTPS track
server {
[...]
  location / {
    include /etc/nginx/proxy.conf;
    proxy_set_header X-Forwarded-Proto https;
    proxy_pass $targetupstream;
  }
[...]
}

Pretty awesome, right? Oh, and guess what? We didn't even use if! =)


Add a comment

Show form to leave a comment

Comments (newest first)

Samuel from wrote on Mar 1st, 2021:

Hello Claudio,
thank you for your kind answer!

proxy1 & 2 are defined above in the file as upstreams like so :

upstream proxy1 {
server localhost:3009;
}

i checked the variables by sending them in a debug header like so :

add_header X-debug-message "$targetupstream" always;

which gives me the proper variable, so the map & regex are working as intended, but something down the road isn't..

La nuit porte conseil as they say, maybe i'll have more luck tomorrow!

thanks again,
samuel


ck from Switzerland wrote on Mar 1st, 2021:

Hi Samuel. The include I am using (proxy.conf) just contains a couple of proxy settings, such as proxy_set_header X-Forwarded-Host and others. This include is not relevant to using this map.
Your config seems to look alright, I cannot spot any issue with it. Except maybe the definition of "proxy1" and "proxy2" somewhere but if you use DNS or IP addresses this should work.


Samuel from wrote on Mar 1st, 2021:

hi! i'm trying to replicate on my own server but can't get it to work. Could you please give me a hint on what i'm doing wrong?


map "$http_user_agent" $targetupstream {
default http://proxy1;
"~^Mozilla" http://proxy2;
}

server {
listen 80;
server_name xxx.xx www.xxx.xx;
return 301 https://xxx.xx;
}

server {
listen 443 ssl;
listen [::]:443 ssl;
server_name xxx.xx www.xxx.xx;

location / {
proxy_set_header X-Forwarded-Proto https;
proxy_pass $targetupstream;
}

[...]

}

nginx runs without issue but just redirects me to the default root seemingly without executing the /location block.

I'm ommiting the "include" line of your code because i have all my code in the same file (sites-enabled/default). Could that be the issue?

thank you!


RSS feed

Blog Tags:

  AWS   Android   Ansible   Apache   Apple   Atlassian   BSD   Backup   Bash   Bluecoat   CMS   Chef   Cloud   Coding   Consul   Containers   CouchDB   DB   DNS   Database   Databases   Docker   ELK   Elasticsearch   Filebeat   FreeBSD   Galera   Git   GlusterFS   Grafana   Graphics   HAProxy   HTML   Hacks   Hardware   Icinga   Influx   Internet   Java   KVM   Kibana   Kodi   Kubernetes   LVM   LXC   Linux   Logstash   Mac   Macintosh   Mail   MariaDB   Minio   MongoDB   Monitoring   Multimedia   MySQL   NFS   Nagios   Network   Nginx   OSSEC   OTRS   Office   OpenSearch   PGSQL   PHP   Perl   Personal   PostgreSQL   Postgres   PowerDNS   Proxmox   Proxy   Python   Rancher   Rant   Redis   Roundcube   SSL   Samba   Seafile   Security   Shell   SmartOS   Solaris   Surveillance   Systemd   TLS   Tomcat   Ubuntu   Unix   VMWare   VMware   Varnish   Virtualization   Windows   Wireless   Wordpress   Wyse   ZFS   Zoneminder