When installing WordPress on NGINX, it’s a good idea to ensure that the site you’re about to install is configured in a secure way. And while that may seem like a no brainer, a lot of people fail to secure and configure their NGINX web server adequately for their WordPress installation. Below you will find the steps and configs that you should use in order to create a secure NGINX config for WordPress.

If copying the code below then please make sure to change any instances of yourdomain.tld as well as your@email.tld appropriately. Also ensure that the PHP version you’re using is correct, the configuration below will specify php7.2-fpm. I’d recommend that you generate your own configuration using https://nginxconfig.io/ which I’ve written about before. This is the same site that I used to generate the config below.

user www-data;
pid /run/nginx.pid;
worker_processes auto;
worker_rlimit_nofile 65535;

events {
  multi_accept on;
  worker_connections 65535;
}

http {
  charset utf-8;
  sendfile on;
  tcp_nopush on;
  tcp_nodelay on;
  server_tokens off;
  log_not_found off;
  types_hash_max_size 2048;
  client_max_body_size 16M;

  # MIME
  include mime.types;
  default_type application/octet-stream;

  # logging
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log warn;

  # SSL
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_session_tickets off;

  # Diffie-Hellman parameter for DHE ciphersuites
  ssl_dhparam /etc/nginx/dhparam.pem;

  # intermediate configuration
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS;
  ssl_prefer_server_ciphers on;

  # OCSP Stapling
  ssl_stapling on;
  ssl_stapling_verify on;
  resolver 1.1.1.1 1.0.0.1 8.8.8.8 8.8.4.4 208.67.222.222 208.67.220.220 valid=60s;
  resolver_timeout 2s;

  # load configs
  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name www.yourdomain.tld;
  set $base /var/www/yourdomain.tld;
  root $base/public;

  # SSL
  ssl_certificate /etc/letsencrypt/live/yourdomain.tld/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.tld/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.tld/fullchain.pem;

  # index.php
  index index.php;

  # index.php fallback
  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  # handle .php
  location ~ \.php$ {
    include nginxconfig.io/php_fastcgi.conf;
  }

  include nginxconfig.io/general.conf;
  include nginxconfig.io/wordpress.conf;
}

# non-www, subdomains redirect
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name .yourdomain.tld;

  # SSL
  ssl_certificate /etc/letsencrypt/live/yourdomain.tld/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.tld/privkey.pem;
  ssl_trusted_certificate /etc/letsencrypt/live/yourdomain.tld/fullchain.pem;

  return 301 https://www.yourdomain.tld$request_uri;
}

# HTTP redirect
server {
  listen 80;
  listen [::]:80;

  server_name .yourdomain.tld;

  include nginxconfig.io/letsencrypt.conf;

  location / {
    return 301 https://www.yourdomain.tld$request_uri;
  }
}
# ACME-challenge
location ^~ /.well-known/acme-challenge/ {
  root /var/www/_letsencrypt;
}
# security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src * data: 'unsafe-eval' 'unsafe-inline'" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

# . files
location ~ /\. {
  deny all;
}

# assets, media
location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ {
  expires 7d;
  access_log off;
}

# svg, fonts
location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ {
  add_header Access-Control-Allow-Origin "*";
  expires 7d;
  access_log off;
}

# gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
try_files $uri =404;

# fastcgi
fastcgi_pass				unix:/var/run/php/php7.2-fpm.sock;
fastcgi_index				index.php;
fastcgi_split_path_info		^(.+\.php)(/.+)$;
fastcgi_param				SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param				PHP_ADMIN_VALUE open_basedir=$base/:/usr/lib/php/:/tmp/;
fastcgi_intercept_errors	off;

fastcgi_buffer_size				128k;
fastcgi_buffers					256 16k;
fastcgi_busy_buffers_size		256k;
fastcgi_temp_file_write_size	256k;

# default fastcgi_params
include fastcgi_params;
# WordPress: allow TinyMCE
location = /wp-includes/js/tinymce/wp-tinymce.php {
  include nginxconfig.io/php_fastcgi.conf;
}

# WordPress: deny wp-content, wp-includes php files
location ~* ^/(?:wp-content|wp-includes)/.*\.php$ {
  deny all;
}

# WordPress: deny wp-content/uploads nasty stuff
location ~* ^/wp-content/uploads/.*\.(?:s?html?|php|js|swf)$ {
  deny all;
}

# WordPress: deny wp-content/plugins (except earlier rules)
location ~ ^/wp-content/plugins {
  deny all;
}

# WordPress: deny scripts and styles concat
location ~* \/wp-admin\/load-(?:scripts|styles)\.php {
  deny all;
}

# WordPress: deny general stuff
location ~* ^/(?:xmlrpc\.php|wp-links-opml\.php|wp-config\.php|wp-config-sample\.php|wp-comments-post\.php|readme\.html|license\.txt)$ {
  deny all;
}

Once all the configuration files are in place and you’re ready to switch over to the secure NGINX config for WordPress, run the following commands:

# Virtual host: create symbolic link
ln -s /etc/nginx/sites-available/yourdomain.tld.conf /etc/nginx/sites-enabled/yourdomain.tld.conf

# HTTPS: create Diffie-Hellman keys
openssl dhparam -dsaparam -out /etc/nginx/dhparam.pem 2048

# HTTPS: create ACME-challenge common directory
sudo -u www-data sh -c "mkdir -p /var/www/_letsencrypt"

# HTTPS: certbot (obtain certificates)
# disable before first run: ssl_certificate, ssl_certificate_key, ssl_trusted_certificate
certbot certonly --webroot -d yourdomain.tld -d www.yourdomain.tld --email your@email.tld -w /var/www/_letsencrypt -n --agree-tos --force-renewal

Now you’re ready to use your site without the headache of configuring all this remotely.