A nginx configuration for Drupal

While nginx is highly configurable, you might encounter some problems and pitfalls when configuring it with Drupal. This post contains our configuration and solutions to those problems.

Before reading on please consider the original resources in the nginx wiki or some other suggested configurations, e.g. this one on Github. Some problems we found:

  • nginx does extra URL encoding when passing query parameters to Drupal. This is bad especially for "+" characters in queries that do not translate to spaces in Drupal (as reported here). Therefore we cannot use the generic "location /" directive but use a generic regular expression to extract the query, in this case the "$1" variable is not encoded by nginx.
  • Private files cannot be downloaded fluently, i.e. they often pause for 30 seconds before they continue. It seems to be a problem related to HTTP keep-alive connections, because disabling them with "keepalive_requests 0;" solved the issue.
  • Image cache may cause problems if the images are not generated yet, so an additional "try_files $uri @drupal" statement for images redirects the request to Drupal in case an image is not found. Also works for Drupal 7, where image cache is part of Drupal core.

The strategy in the configuration is to first handle/secure files (robots.txt, PHP files), then protect or configure other relevant paths and to have a catch-all directive at the end. Here is the content of drupal.conf that can be included in a "server{}" block:

# common Drupal configuration options.
# Make sure to set $socket to a fastcgi socket.

        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }

        ###
        ### support for http://drupal.org/project/robotstxt module
        ###
        location = /robots.txt {
                access_log off;
                try_files $uri @drupal;
        }

        # no access to php files in subfolders.
        location ~ .+/.*\.php$ {
                return 403;
        }

        location ~* \.(inc|engine|install|info|module|sh|sql|theme|tpl\.php|xtmpl|Entries|Repository|Root|jar|java|class)$ {
                deny all;
        }

        location ~ \.php$ {
                # Required for private files, otherwise they slow down extremely.
                keepalive_requests 0;
                include fastcgi.conf;
        }

        # private files protection
        location ~ ^/sites/.*/private/ {
                access_log off;
                deny all;
        }

        location ~* ^(?!/system/files).*\.(js|css|png|jpg|jpeg|gif|ico)$ {
                # If the image does not exist, maybe it must be generated by drupal (imagecache)
                try_files $uri @drupal;
                expires 7d;
                log_not_found off;
        }

        ###
        ### deny direct access to backups
        ###
        location ~* ^/sites/.*/files/backup_migrate/ {
                access_log off;
                deny all;
        }

        location ~ ^/(.*) {
                try_files $uri /index.php?q=$1&$args;
        }

        location @drupal {
                # Some modules enforce no slash (/) at the end of the URL
                # Else this rewrite block wouldn't be needed (GlobalRedirect)
                rewrite ^/(.*)$ /index.php?q=$1;
        }

And here is the configuration for passing the request to PHP FastCGI (fastcgi.conf):

# common fastcgi configuration for PHP files

                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_intercept_errors on;
                fastcgi_pass $socket;
                # workaround as fastcgi_param cannot be used inside if statements
                set $https "";
                if ($scheme = https) {
                  set $https on;
                }
                fastcgi_param HTTPS $https;
                fastcgi_read_timeout 6000;
                # set expires header for private files.
                if ($args ~* \.(js|css|png|jpg|jpeg|gif|ico)) {
                    expires 7d;
                }

 

Categories: 

Comments

You should always check if a file passed to the php handler actually exists before passing it to the php socket. If you don't, people can upload a file called lala.gif containing a php script and then request something like /uploads/lala.gif/dud.php. The contents of lala.gif will then be executed as php.

https://nealpoole.com/blog/2011/04/setting-up-php-fastcgi-and-nginx-dont...

Add a

try_files $uri =404;

At the top of the php fastcgi block

Thanks for that tip, but in our configuration PHP files execution is explicitly forbidden in any subfolder. Even if a malicious image is uploaded, it cannot get to the document root. Even if it gets somehow to the document root, it cannot be executed as PHP file, because one has to add a "/dud.php" to the path and the first rule prevents executing PHP files in subfolders again.

So this config is perfectly safe.

I just came over the need of having clean-URLs for Drupal installations in  sub-directories. See here.

Drupal does not rely on PATH_INFO (contrary to WP for example). You're exposing yourself to the cgi.fix_pathinfo "bug" that has bitten numerous WP sites by making use of it and got hacked. Also the private files can be handled in a much smarter way by using: http://wiki.nginx.org/X-accel. There's even a drupal module for it ;)

Have a look at the config file of barracuda project it has lots of good stuff: http://drupalcode.org/project/barracuda.git/blob/HEAD:/aegir/conf/nginx_...