Add Security to NGINX

These configurations help restrict hackers on either Debian, Linux2023 or anywhere else. Nginx should be a minimum of v1.27

Always have a backup of nginx.conf before these changes.

If you have your own static IP4 address and no one else needs to access specific areas of the site, these configs are also shown where you will see the IP address as XXX.XXX.XXX.XXX.

These configs are thanks to others in forums and articles on the Internet.

If you install Wordfence, or W3 Total Cache, these will place Nginx configuration files into the website root directory and prompt you to restart Nginx when changes are made that impact cache or security changes.

Where you see the memcached.conf file, put in that section of nginx.conf, the file gzip.conf. This file does not have all possible settings, but is pretty good.

The other .conf files are included in the port 443 stanza. e.g. before the error section or around the part that says robots.txt.

You need the semicolon at the end of each include file.

For instance: include /etc/nginx/add.conf;

You must include these files again, in any other domain.conf files in order to work there too.

When finished, restart nginx and php8.2-fpm (or whatever version) and check the website works okay after cache is cleared and any WordPress content cache as well.

Then check performance on https://tools.pingdom.com/ Pingdom Tools

On a complex home page with the Avada WordPress Theme, I get a 91% A rating.

Note: As we are using  nginx v1.27 or higher, we cannot install the ngx_http_headers_module, which would satisfy Pingdom Tools.
If you install the default niginx, it is currently is 1.22. The nginx.conf line http2 on; would need to be removed and then change listen [::]:443 ssl; to read listen [::]:443 ssl http2; (I think!) The nginx.conf lines may have some other differences and behaviours.

gzip.conf

Add after the memcached.conf line, e.g. anywhere before the port 80 stanza.
This does not have all possible values.

cd /etc/nginx

vi gzip.conf

gzip_disable "msie6";

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types
  application/atom+xml
  application/geo+json
  application/javascript
  application/x-javascript
  application/json
  application/ld+json
  application/manifest+json
  application/rdf+xml
  application/rss+xml
  application/xhtml+xml
  application/xml
  application/octet-stream
  font/eot
  font/otf
  font/ttf
  font/woff2
  font/woff
  font/svg
  image/svg+xml
  image/png
  image/jpg
  image/jpeg
  image/webp
  text/css
  text/javascript
  text/plain
  text/xml;

[save and exit]

Add in th port 443 stanza:
include /etc/nginx/gzip.conf;

add.conf

cd /etc/nginx

vi add.conf

# We can block any IP address excaeet your own (or allow several as needed). It is commented out here in case you forget to comment it out.
# location ~ ^/(wp-admin|wp-login\.php)$ {
#          allow XXX.XXX.XXX.XXX;
#                 deny all;
#         }

# nginx  https://gist.github.com/nfsarmento/57db5abba08b315b67f174cd178bea88
# Disable logging for favicon

location = /favicon.ico {
  try_files /favicon.ico @empty;
  access_log off;
  log_not_found off;
  expires max;
}

location @empty {
  empty_gif;
}

# Enable Rewrite Rules for Yoast SEO SiteMap
rewrite ^/sitemap_index\.xml$ /index.php?sitemap=1 last;
rewrite ^/([^/]+?)-sitemap([0-9]+)?\.xml$ /index.php?sitemap=$1&sitemap_n=$2 last;

location ~* .(sh)$ {
   return 444;
}

# Again, I have commented it out in case you don't want it
# location ~* /(wp-config.php|readme.html|license.txt|nginx.conf) {
#   allow XXX.XXX.XXX.XXX;
#   deny all;
#}

# Disallow php in upload folder and add webp rewrite
location /wp-content/uploads/ {
    location ~ \.php$ {
    #Prevent Direct Access Of PHP Files From Web Browsers
        deny all;
    }
    }
# nginx block xmlrpc.php requests
location /xmlrpc.php {
  deny all;
  access_log off;
  log_not_found off;
  return 444;
}

# These two are really annoying, but do work. I don't use them.
# block access to install.php and upgrade.php
   # location ^~ /wp-admin/install.php {
     # deny all;
     # allow XXX.XXX.XXX.XXX;
     # error_page 403 =404 / ;
   # }

   # location ^~ /wp-admin/upgrade.php {
     # deny all;
     # allow XXX.XXX.XXX.XXX;
     # error_page 403 =404 / ;
   # }

#Deny access to wp-content folders for suspicious files
location ~* ^/(wp-content)/(.*?)\.(zip|gz|tar|bzip2|7z)\$ {
  deny all;
}

# Stop scann for the follow files on plugins folder
location ~* ^/wp-content/plugins/.+\.(txt|log|md)$ {
      deny all;
      error_page 403 =404 / ;
}

# Stop scann for the follow files on themes folder
location ~* ^/wp-content/themes/.+\.(txt|log|md)$ {
      deny all;
      error_page 403 =404 / ;
}
# Deny access to uploads that aren’t images, videos, music, etc. (js is still needed in Avada)
 location ~* ^/wp-content/uploads/.*.(html|htm|shtml|php|swf)$ {
     deny all;
 }

#This module will allow us to pattern match certain key files and inject random text in the files that
# is non-destructive / non-invasive and will most importantly alter the md5sum calculated on such files. All transparent to WPScan.
location ~* ^/(license.txt|wp-includes/(.*)/.+\.(js|css)|wp-admin/(.*)/.+\.(js|css))$ {
    sub_filter_types text/css text/javascript text/plain;
    sub_filter_once on;
    sub_filter ';' '; /* $msec */ ';
}

#Direct PHP File Access
#If somehow, a hacker successfully sneaks in a PHP file onto your site,
#they’ll be able to run this file by loading file which effectively becomes a backdoor to infiltrate your site.
location ~* /(?:uploads|wp-content|wp-includes)/.*.php$ {
    deny all;
    access_log off;
    log_not_found off;
}
# Similar to PHP file, a dotfile like .htaccess, .user.ini, and .git may contain sensitive information.
# To be on the safer side, it’s better to disable direct access to these files.
location ~ /\.(svn|git)/* {
    deny all;
    access_log off;
    log_not_found off;
}
location ~ /\.ht {
    deny all;
    access_log off;
    log_not_found off;
}
location ~ /\.user.ini {
    deny all;
    access_log off;
    log_not_found off;
}

# Deny backup extensions & log files
location ~* ^.+\.(bak|log|old|orig|original|php#|php~|php_bak|save|swo|swp|sql)$ {
  deny all;
  access_log off;
  log_not_found off;
}

#WordFence
location ~ \.user\.ini$ {
 deny all;
}
# WordPress: deny wp-content, wp-includes php files
location ~* ^/(?:wp-content|wp-includes)/.*\.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;
}

# Directives to send expires headers and turn off 404 error logging.
location ~* ^.+\.(curl|heic|swf|tiff|rss|atom|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
    access_log off;
    log_not_found off;
    expires 30d;
}

# Web fonts send expires headers
location ~* \.(?:eot|otf|ttf|woff|woff2)$ {
  expires 30d;
  access_log off;
  add_header Cache-Control "public";
}

# SVGs & MP4 WEBM send expires headers - this rule is set specific to ns site
location ~* \.(?:svg|svgz|mp4|webm)$ {
  expires 30d;
  access_log off;
  add_header Cache-Control "public";
}
# Media: images, icons, video, audio send expires headers.
location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|aac|m4a|mp3|ogg|ogv|webp)$ {
  expires 30d;
  access_log off;
  add_header Cache-Control "public";
}

#  Cache css & js files
 location ~* \.(?:css(\.map)?|js(\.map)?)$ {
     add_header "Access-Control-Allow-Origin" "*";
     access_log off;
     log_not_found off;
     expires 30d;
 }

# CSS and Javascript send expires headers.
 location ~* \.(?:css|js)$ {
   expires 30d;
   access_log off;
   add_header Cache-Control "public";
 }

# HTML send expires headers.
 location ~* \.(html)$ {
   expires 7d;
   access_log off;
   add_header Cache-Control "public";
 }

# Return 403 forbidden for readme.(txt|html) or license.(txt|html) or example.(txt|html) or other common git repository files
location ~*  "/(^$|readme|license|example|README|LEGALNOTICE|INSTALLATION|CHANGELOG)\.(txt|html|md)" {
    deny all;
}

# Deny backup extensions & log files and return 403 forbidden
location ~* "\.(old|orig|original|php#|php~|php_bak|save|swo|aspx?|tpl|sh|bash|bak?|cfg|cgi|dll|exe|git|hg|ini|jsp|log|mdb|out|sql|svn|swp|tar|rdf)$" {
    deny all;
}

#WordFence
location ~ \.user\.ini$ {
 deny all;
}
# WordPress: deny wp-content, wp-includes php files
location ~* ^/(?:wp-content|wp-includes)/.*\.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;
}

location = /wp-config-sample.php {
        deny all;
        return 404;
    }

[save and exit]

If you have issues with weird things like not being able to log out of WordPress, it is likely it comes back to the add_headers configs in nginx.conf (or apache's configs) or these additional seucrity files.

db.conf

This focuses on the database.

cd /etc/nginx

vi db.conf

# common nginx configuration to block sql injection and other attacks
# DISABLE THIS INCLUDES FILE FOR UPDATING PHPMYADMIN CONTENT
location ~* "(eval\()" {
    deny all;
}
location ~* "(127\.0\.0\.1)" {
    deny all;
}
location ~* "([a-z0-9]{2000})" {
    deny all;
}
location ~* "(javascript\:)(.*)(\;)" {
    deny all;
}

location ~* "(base64_encode)(.*)(\()" {
    deny all;
}
location ~* "(GLOBALS|REQUEST)(=|\[|%)" {
    deny all;
}
location ~* "(<|%3C).*script.*(>|%3)" {
    deny all;
}
location ~ "(\\|\.\.\.|\.\./|~|`|<|>|\|)" {
    deny all;
}
location ~* "(boot\.ini|etc/passwd|self/environ)" {
    deny all;
}
location ~* "(thumbs?(_editor|open)?|tim(thumb)?)\.php" {
    deny all;
}
location ~* "(\'|\")(.*)(drop|insert|md5|select|union)" {
    deny all;
}
location ~* "(https?|ftp|php):/" {
    deny all;
}
location ~* "(=\\\'|=\\%27|/\\\'/?)\." {
    deny all;
}
location ~ "(\{0\}|\(/\(|\.\.\.|\+\+\+|\\\"\\\")" {
    deny all;
}
location ~ "(~|`|<|>|:|;|%|\\|\s|\{|\}|\[|\]|\|)" {
    deny all;
}
location ~* "/(=|\$&|_mm|(wp-)?config\.|cgi-|etc/passwd|muieblack)" {
    deny all;
}

location ~* "(&pws=0|_vti_|\(null\)|\{\$itemURL\}|echo(.*)kae|etc/passwd|eval\(|self/environ)" {
    deny all;
}
location ~* "/(^$|mobiquo|phpinfo|shell|sqlpatch|thumb|thumb_editor|thumbopen|timthumb|webshell|config|settings|configuration)\.php" {
    deny all;
}

[save and exit]

nginx.conf with additional content

You would need to compare with your own nginx.conf file.

Always remember to have a previous backup copy of nginx.conf

In this nginx.conf example, I show:

(1) How to stop people hacking around who use the website IP address instead of the hsotname
(2) How to block hackers or snoops from downloading the webpage using site, feed, and rss commands.
(3) Examples of unique names you can have in your website pages/posts that only your IP address can see (e.g. "documents", "private" etc.)

NOTES:
This examle uses /run/php/php8.2-fpm.sock. The www.conf file will show the correct socket.
Where you see Stanza port 443 additions, you can do the same with other domain.conf files in their stanzas.
You can use iptables to block IP addresses or ranges, which is my preference to using deny commands. ip2location software works on apache2 or httpd, but not on the default nginx installation.

user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;
    sendfile           on;
    tcp_nopush         on;
    keepalive_timeout  65;

    gzip  on;

# these 4 lines are problably optional:
 proxy_buffer_size 16K;
 proxy_buffers 4 16K;
 proxy_max_temp_file_size 0;
 client_body_buffer_size 2480K;

include /etc/nginx/conf.d/*.conf;

# lws configurations
    server_names_hash_bucket_size 64;
    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
    ssl_session_timeout 10m;
    add_header Content-Security-Policy "object-src 'none'; base-uri 'none'; frame-ancestors 'self'; form-action 'self';";

## Not sure on this. I don't use it... STOPS NINJA forms  add_header Content-Security-Policy "script-src 'nonce-{RANDOM}' 'strict-dynamic';object-src 'none'; base-uri 'none';";
## refer: https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
## usual settings are no good as they prevent page formatting

 add_header X-Xss-Protection "1; mode=block" always;
 add_header X-Frame-Options "SAMEORIGIN" always;
 add_header X-Content-Type-Options "nosniff" always;
 add_header Permissions-Policy "autoplay=(), encrypted-media=(), fullscreen=(), geolocation=(), microphone=(), midi=()";
 add_header Clear-Site-Data "*";

 client_max_body_size 50M;

# I used to use this but I am not sure now. Too many warnings in logs.    
# upstream _php {
  # server unix:/run/live/php8.2-fpm.sock;
  # }

include /etc/nginx/memcached.conf;
include /etc/nginx/gzip.conf;

# Include your oother domains or subdomains here:
# include /etc/nginx/SUBDOMAIN.COM.conf;

    server {
        listen       80;
        listen       [::]:80;
        server_name  domain.com www.domain.com;
        return 301 https://domain.com.net$request_uri;
        root         /var/www/html;
        index index.php index.html index.htm;

set $test 0;
    if ( $host != "domain.com" ){
        set $test 1;
    }
    if ( $host != "www.domain.com" ){
        set $test 1$test;
    }
    if ( $test = 11 ){
        return 444; #CONNECTION CLOSED WITHOUT RESPONSE
    }

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

        location ~ \.php$ {
        # SECURITY : Zero day Exploit Protection
        try_files $uri =404;
        # ENABLE : Enable PHP, listen fpm sock
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        }

        location = /favicon.ico {
        log_not_found off;
        access_log off;
        }
        location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
        }

         error_page 404 /404.html;
         location = /404.html {
         }

        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
        }

# end port 80
}

# start 443

    server {
        listen       443 ssl;
        listen       [::]:443 ssl;
        http2 on;
        server_name  domain.com;
        root         /var/www/html;

# ssl location depends on paid or certbot/letsencrypt and if Debian or Linux2023 etc.

        ssl_certificate "/etc/ssl/certs/domain_com.crt";
        ssl_certificate_key "/etc/ssl/private/domain_com.key";
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers EECDH+CHACHA20:EECDH+AES;
        ssl_ecdh_curve X25519:prime256v1:secp521r1:secp384r1;
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:1m;
        ssl_session_timeout  10m;

if ( $host != "domain.com" ){
        return 444; #CONNECTION CLOSED WITHOUT RESPONSE
    }

location ~* .(?:ico|css|js|gif|jpe?g|png|jpg|woff2|eot|ttf|svg|woff)$ {
    expires 30d;
    add_header Pragma "public";
    add_header Cache-Control "public";
}

        location / {
                 set $memcached_key "$uri?$args";
                 set $memc_key $arg_key;
                 default_type text/html;
                 set $memc_key $arg_key;
                 set $memc_flags $arg_flags; # defaults to 0
                 set $memc_exptime $arg_exptime; # defaults to 0
                 error_page     404 = @remote;
        index index.php index.html index.htm;
        try_files $uri $uri/ /index.php?$args;
        }
          location @remote {
         internal;
         access_log /var/log/nginx/server.fallback.access.log;
         proxy_pass http://remote;
         proxy_set_header Connection "";
         }

        location ~ \.php$ {
        # SECURITY : Zero day Exploit Protection
        try_files $uri =404;
        # ENABLE : Enable PHP, listen fpm sock
        fastcgi_split_path_info ^(.+\.php)(/.+)$;
        fastcgi_pass unix:/run/php/php8.2-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
        }

        location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
        try_files $uri /index.php?$args;
        }

# W3 Total Cache or Wordfence will add their own files so we do not have to.

       include /etc/nginx/add.conf;
       include /etc/nginx/db.conf;

# Examples of blocking all access except your IP address:

             location /myweb-private {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }
             location /admin {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }
             location /documents {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

location ~ ^(.*)(.*)(private)(.*)$ {
              allow XXX.XXX.XXX.XXX;
              deny all;
              try_files $uri $uri/ /index.php?$query_string;
              }

location ~ ^(.*)(.*)(xmlrpc.php)(.*)$ {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

location ~ ^(.*)(.*)(feed)(.*)$ {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

location ~ ^(.*)(.*)(rss)(.*)$ {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

location ~ ^(.*)(.*)(domain.com)(.*)$ {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

location ~ ^(.*)(.*)(sites)(.*)$ {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

location ~ ^(.*)(.*)(.git)(.*)$ {
             allow XXX.XXX.XXX.XXX;
             deny all;
             try_files $uri $uri/ /index.php?$query_string;
             }

# It is difficult to get archived website working. For example, you may have an archive under /data/archive/ready.com and a soft link to it in /var/www/html.
# As I've not yet had good success I won't add a configuration here.

             error_page 404 /404.html;
             location = /404.html {
             }
             error_page 500 502 503 504 /50x.html;
             location = /50x.html {
             }


# end port 443
}


# end nginx.conf
}

Start typing and press Enter to search