Create a gist now

Instantly share code, notes, and snippets.

Battle-tested Nginx configuration for Magento (source: www.hypernode.com)
# This is an annotated subset of the Nginx configuration from our Magento production platform @ www.hypernode.com
# See https://www.byte.nl/blog/magento-cacheleak-issue
# !!!! If you are a Hypernode customer, do not use this config as it will result in duplicate statements. !!!!!
user app;
worker_processes 4;
pid /var/run/nginx.pid;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
server_tokens off;
server_names_hash_bucket_size 64;
# allows big media uploads
client_max_body_size 120m;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# GeoIP support is included in the Ubuntu 12.04 Nginx.
# This enables logging, and the following:
# if ($geoip_country_code ~ (CN|ZW) ) {
# return 403;
# }
geoip_country /usr/share/GeoIP/GeoIP.dat;
gzip on;
gzip_disable "msie6";
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip_min_length 1000;
gzip_proxied any;
gzip_types text/plain text/html application/json application/xml text/css text/js application/x-javascript;
# Determine whether a request comes from a human, a search crawler or another bot.
map $http_user_agent $is_non_search_bot {
default '';
~*(google|bing|pingdom|monitis.com|Zend_Http_Client) '';
~*(http|crawler|spider|bot|search|ForusP|Wget/|Python-urllib|PHPCrawl|bGenius) 'bot';
}
# Rate limit bots (that are not search spiders) to one PHP request per second.
# An empty '$limit_bots' would disable rate limiting for this requests
limit_req_zone $is_non_search_bot zone=bots:1m rate=1r/s;
limit_req_log_level error;
index index.html index.php;
server {
listen 80 default_server;
root /var/www;
# Android dupe request bug, https://www.byte.nl/blog/android-bug-can-kill-site-duplicate-requests
set $request_url "$scheme://$http_host$request_uri";
if ($request_url = $http_referer) {
set $request_is_referer 1;
}
if ($http_user_agent ~ 'Android ([23]|4\.[0123])') {
set $android_buggy_ua 1;
}
set $android_dupe_bug "${request_method}${android_buggy_ua}${request_is_referer}";
if ($android_dupe_bug = 'GET11') {
# http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
return 429;
}
# Denied locations require a "^~" to prevent regexes (such as the PHP handler below) from matching
# http://nginx.org/en/docs/http/ngx_http_core_module.html#location
location ^~ /app/ { return 403; }
location ^~ /includes/ { return 403; }
location ^~ /media/downloadable/ { return 403; }
location ^~ /pkginfo/ { return 403; }
location ^~ /report/config.xml { return 403; }
location ^~ /var/ { return 403; }
location ^~ /lib/ { return 403; }
location ^~ /dev/ { return 403; }
location ^~ /RELEASE_NOTES.txt { return 403; }
location ^~ /downloader/pearlib { return 403; }
location ^~ /downloader/template { return 403; }
location ^~ /downloader/Maged { return 403; }
location ~* ^/errors/.+\.xml { return 403; }
# CVE-2015-3428 / AW_Blog vulnerability
# Note the .+ at the start: We want to allow url's like
# order=create_date, which would otherwise match.
if ($arg_order ~* .+(select|create|insert|update|drop|delete|concat|alter|load)) {
return 403;
}
# Don't skip .thumbs, this is a default directory where Magento places thumbnails
# Nginx cannot "not" match something, instead the target is matched with an empty block
# http://stackoverflow.com/a/16304073
location ~ /\.thumbs {
}
# Skip .git, .htpasswd etc
location ~ /\. {
return 404;
}
set $fastcgi_root $document_root;
location / {
try_files $uri $uri/ @handler;
expires 30d;
}
# SUPEE 6285
# Only allow the new url case sensitive lowercase, deny case insensitive
location ^~ /rss/order/new {
echo_exec @handler;
}
location ^~ /rss/catalog/notifystock {
echo_exec @handler;
}
location ^~ /rss/catalog/review {
echo_exec @handler;
}
location ~* /rss/order/new {
return 403;
}
location ~* /rss/catalog/notifystock {
return 403;
}
location ~* /rss/catalog/review {
return 403;
}
## Order IS important! this is required BEFORE the PHP regex
## Allow PHP scripts in skin and JS, but render static 404 pages when skin or js file is missing
## Magento has RewriteCond %{REQUEST_URI} !^/(media|skin|js)/ in default htaccess
location ~ ^/(skin|js)/ {
location ~ \.php$ {
echo_exec @phpfpm;
}
try_files $uri $uri/ =404;
expires 30d;
}
# Disallow PHP scripts in /media/
# Also render static 404 pages for missing media
location ~ ^/media/ {
location ~ \.php$ {
return 403;
}
try_files $uri $uri/ =404;
expires 30d;
}
location @handler {
rewrite / /index.php;
}
location @fastcgi_backend {
# Bot rate limit, https://gist.github.com/supairish/2951524
# Burst=0 (default) --WdG
limit_req zone=bots;
# server_name is read-only, so we need a temp var
set $my_server_name $server_name;
if ($my_server_name = "") {
set $my_server_name $http_host;
}
try_files $uri =404;
expires off;
root $fastcgi_root;
fastcgi_read_timeout 900s;
fastcgi_index index.php;
fastcgi_pass $fastcgi_pass;
include /etc/nginx/fastcgi_params;
fastcgi_param HTTP_AUTHORIZATION $http_authorization;
fastcgi_param SERVER_NAME $my_server_name;
fastcgi_param NGINX_REQUEST_TIME $date_gmt;
# If these variables are unset, set them to an empty value here
# so they are al least defined when fastcgi_param directives are called
if ($storecode = "") {
set $storecode "";
}
if ($storetype = "") {
set $storetype "";
}
# These are set in http.magerunmaps
fastcgi_param MAGE_RUN_CODE $storecode if_not_empty;
fastcgi_param MAGE_RUN_TYPE $storetype if_not_empty;
}
location @phpfpm {
set $log_handler phpfpm;
set $fastcgi_pass 127.0.0.1:9000;
echo_exec @fastcgi_backend;
}
location @hhvm {
set $log_handler hhvm;
set $fastcgi_pass 127.0.0.1:9001;
echo_exec @fastcgi_backend;
}
# Protection against unsecured magmi installs. User-editable
# so user may set it up as they want. Must be included here
# to catch and redirect PHP files, if this was loaded in later
# (after the default php-fpm handler for .php files) then we
# would not be able to redirect the magmi .php files (which are
# the ones we really MUST redirect).
location ~* /magmi($|/) {
return https://support.hypernode.com/knowledgebase/securing-access-to-magmi/;
}
location ~ .php/ {
rewrite ^(.*.php)/ $1 last;
}
# always execute our own handler for php-fpm, to prevent serving raw php code and to have
# a default when user removes configuration from ~/nginx/
location ~ \.php$ {
echo_exec @phpfpm;
}
rewrite ^/minify/([0-9]+)(/.*.(js|css))$ /lib/minify/m.php?f=$2&d=$1 last;
rewrite ^/skin/m/([0-9]+)(/.*.(js|css))$ /lib/minify/m.php?f=$2&d=$1 last;
location /lib/minify/ { allow all; }
}
}
@magenx
magenx commented Oct 10, 2015

you should have warned all "copy-paste squad" that this config will only work for Ubuntu*:
http://askubuntu.com/questions/553937/what-is-the-difference-between-the-core-full-extras-and-light-packages-for-ngi

i just dont understand some parts of this config and other issues with it.
what kind a battle you have there :)

# GeoIP support is included in the Ubuntu 12.04 Nginx.
    # This enables logging, and the following:
    #    if ($geoip_country_code ~ (CN|ZW) ) {
    #      return 403;
    #    }

was thinking about it, but then realized that nginx is not a firewall.

 # Skip .git, .htpasswd etc
        location ~ /\. {
            return 404;
        }

if you need to protect .dot folders, just define them, this single . rule can break some requests,
ive seen it many times with some internal hashed urls, etc

    # Determine whether a request comes from a human, a search crawler or another bot.
    map $http_user_agent $is_non_search_bot {
        default '';
        ~*(google|bing|pingdom|monitis.com|Zend_Http_Client) '';
        ~*(http|crawler|spider|bot|search|ForusP|Wget/|Python-urllib|PHPCrawl|bGenius) 'bot';
    }

do not filter user_agent, it can be spoofed, protect locations it is going to, or use more advanced ip spoofing check.

  # CVE-2015-3428 / AW_Blog vulnerability
        # Note the .+ at the start: We want to allow url's like
        # order=create_date, which would otherwise match.
        if ($arg_order ~* .+(select|create|insert|update|drop|delete|concat|alter|load)) {
           return 403;
        }
# Android dupe request bug, https://www.byte.nl/blog/android-bug-can-kill-site-duplicate-requests
        set $request_url "$scheme://$http_host$request_uri";
        if ($request_url = $http_referer) {
           set $request_is_referer 1;
        }
        if ($http_user_agent ~ 'Android ([23]|4\.[0123])') {
            set $android_buggy_ua 1;
        }
        set $android_dupe_bug "${request_method}${android_buggy_ua}${request_is_referer}";
        if ($android_dupe_bug = 'GET11') {
            # http://en.wikipedia.org/wiki/List_of_HTTP_status_codes
            return 429;
        }
 location ~* /magmi($|/) {
            return https://support.hypernode.com/knowledgebase/securing-access-to-magmi/;
        }

fix application, not the web server.

we just receiving many emails, customers asking to review this config.

@x86fantini

+1 for @magenx

@magenx
magenx commented Oct 14, 2015

by the way, if no one really cares about this, i will correct this config with some little explanation:

if you keep this: _location ^~ /app/ { return 403; }

it means that nginx will care about location position. so user will naively create folder in:

_www.domain.com/folder/


and copy his magento files there, then everyone will be able to download everything, every secret file:

_www.domain.com/folder/app/etc/local.xml_

so either magento security team are noobs and full of laymans, or hypernode.com is a black hole for your business...

@digideth

@gwillem & @magenx

What changes need to be made to get this to work properly on a RHEL server?

Any help greatly appreciated 😄

@magenx
magenx commented Oct 20, 2015

you have to compile your nginx with some extra modules, which is not good, unless you keep a good record of your software changes. and this config has too much of unnecessary information, and obviously while it's trying to close some security holes it opens few from another place.... install magento security patches, this is what you have to do first.

@digideth

@magenx Thank you! Saved me from wasting a bunch of time.

@ray-moncada

Took me a while to resolve error magento nginx 502 bad gateway. But after understanding
set $fastcgi_pass 127.0.0.1:9000;
and
set $fastcgi_pass 127.0.0.1:9001;

I changed those two lines with
set fastcgi_pass unix:/var/run/php5-fpm.sock;

I was able to get the site up and running.

@tomlankhorst

When using lets-encrypt, .well-known has to be reached. Line 107-109 block access to any .folder.
How to allow the .well-known folder in Hypernode config?

@tdm4
tdm4 commented Mar 20, 2017

The limit_req zone=bots; actually applies to ALL visitors to the site. We have had to remove it because it was returning 503s on checkout/cart. Unfortunately, you can't put limit_req in an if statement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment