.conf for nginx

Last Revised: October 2, 2021

If you use nginx, apart from the general server configuration file, you will probably need a configuration file for each of the websites you work on, and here is a much more complex configuration.

In this example we are going to use a example.com domain, so all www.example.com traffic will be redirected to example.com.

Detailed configuration by blocks

HTTP Configuration

The idea is that nothing from the site responds with HTTP, but only with HTTPS, so any request that arrives will be redirected automatically.

server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;
  return 301 https://example.com$request_uri;
  access_log off;
}

HTTPS configuration for www

Since we are only going to make the “sin-www” responsible, we will tell all traffic that arrives by www to make a redirect. In this case we have to activate all the part of certificates and SSL / TLS.

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 208.67.222.222 8.8.8.8 valid=300s;
  resolver_timeout 2s;
  add_header Referrer-Policy "strict-origin-when-cross-origin";
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;
  add_header X-XSS-Protection "1; mode=block";
  server_name www.example.com;
  return 301 https://example.com$request_uri;
  access_log off;
}

General settings

A bit like the previous block, the general configuration of a secure site.

  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  # SSL
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  # SSL OCSP Stapling
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 208.67.222.222 8.8.8.8 valid=300s;
  resolver_timeout 2s;
  # Security headers
  add_header Referrer-Policy "strict-origin-when-cross-origin";
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;
  add_header X-XSS-Protection "1; mode=block";
  #logs
  access_log /var/log/nginx/example.com-access.log combined buffer=64k flush=5m;
  error_log /var/log/nginx/example.com-error.log;

Route and default settings

We will tell you which domain you have to respond to, and which files you have to read in order. As a general rule we will find in WordPress index files.php, and if it does not exist, there is usually an index.html.

  #CONFIG
  server_name example.com;
  root /webs/example.com;
  index index.php index.html index.htm;

Cache Configuration (WP Super Cache)

If you have a caching plugin, it will not be able to self-configure as it usually does with Apache’s .htaccess, so we will have to add the configuration manually. In this example it is the settings for WP Super cache, but you should review the configuration of the plugin you use.

  set $cache_uri $request_uri;
  if ($request_method = POST) {
    set $cache_uri 'null cache';
  }
  if ($query_string != "") {
    set $cache_uri 'null cache';
  }
  if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-*.php)") {
    set $cache_uri 'null cache';
  }
  if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
    set $cache_uri 'null cache';
  }
  set $cachefile "/wp-content/cache/supercache/$http_host/$cache_uri/index.html";
  if ($https ~* "on") {
    set $cachefile "/wp-content/cache/supercache/$http_host/$cache_uri/index-https.html";
  }

Blocking / Permissions of .ht<loquesea>

By default we will block external access to the configuration files, and this one. Although we will leave access to the “Well-Known”.

  location ~ /.well-known {
    allow all;
  }
  location ~ /.ht {
    deny all;
  }

Static files

We will give some life to the static files, both so that they do not give errors, and to configure their cache.

  location ~ /favicon.(ico|png) {
    log_not_found off;
    access_log off;
  }
  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }
  location ~* .(bmp|bz2|cur|doc|docx|exe|gif|eot|gz|htc|ico|jpeg|jpg|mid|midi|mp3|mp4|ogg|ogv|otf|png|ppt|pptx|rar|rtf|svg|svgz|tar|tgz|ttf|wav|webm|woff|woff2|xls|xlsx|zip)$ {
    expires max;
    add_header Cache-Control "public";
    log_not_found off;
    access_log off;
  }
  location ~* .(atom|css|js|rss)$ {
    expires 7d;
    add_header Cache-Control "public";
    log_not_found off;
    access_log off;
  }
  location ~* .(?:ttf|eot|woff|otf)$ {
    add_header Access-Control-Allow-Origin "*";
  }

Blocking access to files with data

With these blocks we prevent access to certain system files, WordPress configurations or change the response of what the web server returns by default.

  location ~* readme.(html|txt) {
    deny all;
  }
  location ~* (licencia|license|LICENSE|olvasdel|lisenssi|liesmich).(html|txt) {
    deny all;
  }
  location ~* ^/wp-config {
    deny all;
  }
  location ~* ^/wp-cron.php {
    deny all;
  }
  location ~* ^/wp-admin/(install|setup-config|upgrade).php {
    deny all;
  }
  location ~* ^/wp-admin/maint/repair.php {
    deny all;
  }
  location ~* ^/wp-links-opml.php {
    deny all;
  }
  location ~* ^/wp-content/mu-plugins/$ {
    return 404;
  }
  location ~* ^/wp-content/(plugins|themes)/(.+)/$ {
    return 404;
  }
  location ~* ^/wp-content/(?:uploads|files)/.+.(html|js|php|shtml|swf)$ {
    deny all;
  }
  location ~* ^/wp-content/plugins/.+.(aac|avi|bz2|cur|docx?|eot|exe|flv|gz|heic|htc|m4a|midi?|mov|mp3|mp4|mpe?g|ogg|ogv|otf|pdf|pptx?|rar|rtf|tar|tgz|tiff?|ttc|wav|wmv|xlsx?|zip) {
    deny all;
  }
  location ~* sftp-config.json {
    deny all;
  }
  location ~* (access|error)_log {
    deny all;
  }
  location ~* installer-log.txt {
    deny all;
  }
  location ~* ^/wp-content/debug.log {
    deny all;
  }
  location ~* (^#.*#|.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$ {
    deny all;
  }

Mitigation CVE-2018-6389

As a general rule, it is better to block the possible attack that can be carried out easily with the loading of all the scripts. To avoid this it is better to block its charge.

  location ~* load-(scripts|styles).php {
    deny all;
  }

Blocking XML-RPC

If we do not use this technology (for example, from WordPress Apps or we have pingbacks disabled, for security it is a good option.

  # BLOCK XML-RPC
  location ~* /xmlrpc.php {
    deny all;
  }

Blocking users in the REST-API

To avoid in a quick and simple way to obtain the list of WordPress users, we can limit access to this request. Don’t block it if you’re using this REST-API functionality.

  # BLOCK USERS API
  location ~* ^/wp-json/wp/v2/users/ {
    deny all;
  }

Configuring requests

We configure the server so that all requests that arrive (and have not been previously blocked) are answered by the system. In this case, the cache request system is included, which would be priopatrious to other requests.

  location / {
    try_files $cachefile $uri $uri/ /index.php?$args;
  }

Running PHP

We will configure PHP so that you can respond to WordPress requests.

  # ROOT PHP
  location ~ .php$ {
    fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
    fastcgi_index index.php;
    fastcgi_buffers 256 16k;
    fastcgi_buffer_size 128k;
    fastcgi_busy_buffers_size 256k;
    fastcgi_temp_file_write_size 256k;
    fastcgi_hide_header X-Powered-By;
    fastcgi_hide_header X-Pingback;
    fastcgi_hide_header Link;
    fastcgi_intercept_errors off;
    fastcgi_split_path_info ^(.+.php)(/.+)$;
    try_files $fastcgi_script_name =404;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PHP_ADMIN_VALUE open_basedir=$document_root/:/usr/lib/php/:/tmp/;
    fastcgi_param PATH_INFO $path_info;
    set $path_info $fastcgi_path_info;
    include fastcgi.conf;
  }

Full content of the .conf

The complete file would be as follows:

# All HTTP traffic will be sent to HTTPS
server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;
  return 301 https://example.com$request_uri;
  access_log off;
}
# REDIRECTION
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  # SSL
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  # SSL OCSP Stapling
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 208.67.222.222 8.8.8.8 valid=300s;
  resolver_timeout 2s;
  # Security headers
  add_header Referrer-Policy "strict-origin-when-cross-origin";
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;
  add_header X-XSS-Protection "1; mode=block";
  server_name www.example.com;
  return 301 https://example.com$request_uri;
  access_log off;
}
# REAL SITE
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  # SSL
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
  include /etc/letsencrypt/options-ssl-nginx.conf;
  # SSL OCSP Stapling
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 208.67.222.222 8.8.8.8 valid=300s;
  resolver_timeout 2s;
  # Security headers
  add_header Referrer-Policy "strict-origin-when-cross-origin";
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
  add_header X-Frame-Options DENY;
  add_header X-Content-Type-Options nosniff;
  add_header X-XSS-Protection "1; mode=block";
  #logs
  access_log /var/log/nginx/example.com-access.log combined buffer=64k flush=5m;
  error_log /var/log/nginx/example.com-error.log;
  #CONFIG
  server_name example.com;
  root /webs/example.com;
  index index.php index.html index.htm;
  set $cache_uri $request_uri;
  if ($request_method = POST) {
    set $cache_uri 'null cache';
  }
  if ($query_string != "") {
    set $cache_uri 'null cache';
  }
  if ($request_uri ~* "(/wp-admin/|/xmlrpc.php|/wp-*.php)") {
    set $cache_uri 'null cache';
  }
  if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_logged_in") {
    set $cache_uri 'null cache';
  }
  set $cachefile "/wp-content/cache/supercache/$http_host/$cache_uri/index.html";
  if ($https ~* "on") {
    set $cachefile "/wp-content/cache/supercache/$http_host/$cache_uri/index-https.html";
  }
  location / {
    try_files $cachefile $uri $uri/ /index.php?$args;
  }
  # HIDDEN FILES
  location ~ /.well-known {
    allow all;
  }
  location ~ /.ht {
    deny all;
  }
  location ~ /favicon.(ico|png) {
    log_not_found off;
    access_log off;
  }
  location = /robots.txt {
    allow all;
    log_not_found off;
    access_log off;
  }
  location ~* .(bmp|bz2|cur|doc|docx|exe|gif|eot|gz|htc|ico|jpeg|jpg|mid|midi|mp3|mp4|ogg|ogv|otf|png|ppt|pptx|rar|rtf|svg|svgz|tar|tgz|ttf|wav|webm|woff|woff2|xls|xlsx|zip)$ {
    expires max;
    add_header Cache-Control "public";
    log_not_found off;
    access_log off;
  }
  location ~* .(atom|css|js|rss)$ {
    expires 7d;
    add_header Cache-Control "public";
    log_not_found off;
    access_log off;
  }
  location ~* .(?:ttf|eot|woff|otf)$ {
    add_header Access-Control-Allow-Origin "*";
  }
  location ~* readme.(html|txt) {
    deny all;
  }
  location ~* (licencia|license|LICENSE|olvasdel|lisenssi|liesmich).(html|txt) {
    deny all;
  }
  location ~* ^/wp-config {
    deny all;
  }
  location ~* ^/wp-cron.php {
    deny all;
  }
  location ~* ^/wp-admin/(install|setup-config|upgrade).php {
    deny all;
  }
  location ~* ^/wp-admin/maint/repair.php {
    deny all;
  }
  location ~* ^/wp-links-opml.php {
    deny all;
  }
  location ~* ^/wp-content/mu-plugins/$ {
    return 404;
  }
  location ~* ^/wp-content/(plugins|themes)/(.+)/$ {
    return 404;
  }
  location ~* ^/wp-content/(?:uploads|files)/.+.(html|js|php|shtml|swf)$ {
    deny all;
  }
  location ~* ^/wp-content/plugins/.+.(aac|avi|bz2|cur|docx?|eot|exe|flv|gz|heic|htc|m4a|midi?|mov|mp3|mp4|mpe?g|ogg|ogv|otf|pdf|pptx?|rar|rtf|tar|tgz|tiff?|ttc|wav|wmv|xlsx?|zip) {
    deny all;
  }
  location ~* sftp-config.json {
    deny all;
  }
  location ~* (access|error)_log {
    deny all;
  }
  location ~* installer-log.txt {
    deny all;
  }
  location ~* ^/wp-content/debug.log {
    deny all;
  }
  location ~* (^#.*#|.(bak|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$ {
    deny all;
  }
  location ~* load-(scripts|styles).php {
    deny all;
  }
  # BLOCK XML-RPC
  location ~* /xmlrpc.php {
    deny all;
  }
  # BLOCK USERS API
  location ~* ^/wp-json/wp/v2/users/ {
    deny all;
  }
  # ROOT PHP
  location ~ .php$ {
    fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
    fastcgi_index index.php;
    fastcgi_buffers 256 16k;
    fastcgi_buffer_size 128k;
    fastcgi_busy_buffers_size 256k;
    fastcgi_temp_file_write_size 256k;
    fastcgi_hide_header X-Powered-By;
    fastcgi_hide_header X-Pingback;
    fastcgi_hide_header Link;
    fastcgi_intercept_errors off;
    fastcgi_split_path_info ^(.+.php)(/.+)$;
    try_files $fastcgi_script_name =404;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param PHP_ADMIN_VALUE open_basedir=$document_root/:/usr/lib/php/:/tmp/;
    fastcgi_param PATH_INFO $path_info;
    set $path_info $fastcgi_path_info;
    include fastcgi.conf;
  }
}

Seguir con Seguridad para WordPress


About this document

This document is regulated by the EUPL v1.2 license, published in WP SysAdmin and created by Javier Casares. Please, if you use this content in your website, your presentation or any material you distribute, remember to mention this site or its author, and having to put the material you create under EUPL license.

WordPress System Administration Services

Do you have a high traffic WordPress website? Are you an Agency with servers with cPanel, Plesk or other panel on which you maintain WordPress for your clients?

If so, and you are interested in a professional WordPress infrastructure maintenance and performance improvement service for your or your clients’ websites, contact me.