Skip to content

Instantly share code, notes, and snippets.

@nfsarmento
Last active March 7, 2024 13:38
Show Gist options
  • Star 89 You must be signed in to star a gist
  • Fork 44 You must be signed in to fork a gist
  • Save nfsarmento/57db5abba08b315b67f174cd178bea88 to your computer and use it in GitHub Desktop.
Save nfsarmento/57db5abba08b315b67f174cd178bea88 to your computer and use it in GitHub Desktop.
Harden wordpress security nginx
############ WordPress ####################
# Disable logging for favicon and robots.txt
location = /favicon.ico {
try_files /favicon.ico @empty;
access_log off;
log_not_found off;
expires max;
}
location @empty {
empty_gif;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
try_files $uri /index.php?$args;
}
# Limit access to avoid brute force attack
# if you getting error please add this (limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;) to your /etc/nginx/nginx.conf
#location = /wp-login.php {
# limit_req zone=one burst=1 nodelay;
# include fastcgi_params;
# fastcgi_pass 127.0.0.1:9000;
#}
#Deny access to wp-content folders for suspicious files
location ~* ^/(wp-content)/(.*?)\.(zip|gz|tar|bzip2|7z)\$ {
deny all;
}
location ~ ^/wp-content/uploads/sucuri {
deny all;
}
location ~ ^/wp-content/updraft {
deny all;
}
#Disable execution of scripts other than PHP from your document root
location ~* .(pl|cgi|py|sh|lua|asp)$ {
return 444;
}
#Disable access to your configuration files and other files that you don’t want to users are able to see
location ~* /(wp-config.php|readme.html|license.txt|nginx.conf) {
deny all;
}
# Disable wp-config.txt
location = /wp-config.txt {
deny all;
access_log off;
log_not_found off;
}
# 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;
}
# webp rewrite rules
location ~ \.(png|jpe?g)$ {
add_header Vary "Accept-Encoding";
add_header "Access-Control-Allow-Origin" "*";
add_header Cache-Control "public, no-transform";
access_log off;
log_not_found off;
expires max;
try_files $uri $uri =404;
}
}
# nginx block xmlrpc.php requests
location /xmlrpc.php {
deny all;
access_log off;
log_not_found off;
return 444;
}
# nginx block wpscann on plugins folder
location ~* ^/wp-content/plugins/.+\.(txt|log|md)$ {
deny all;
error_page 403 =404 / ;
}
# block access to install.php and upgrade.php
location ^~ /wp-admin/install.php {
deny all;
error_page 403 =404 / ;
}
location ^~ /wp-admin/upgrade.php {
deny all;
error_page 403 =404 / ;
}
# Deny access to any files with a .php extension in the uploads directory
# Works in sub-directory installs and also in multisite network
# Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban)
location ~* /(?:uploads|files)/.*\.php$ {
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 / ;
}
#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.
#We should disable direct access to any PHP files by adding the following rules:
location ~* /(?:uploads|files|wp-content|wp-includes|akismet)/.*.php$ {
deny all;
access_log off;
log_not_found off;
}
#Dotfiles
#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 access to uploads that aren’t images, videos, music, etc.
location ~* ^/wp-content/uploads/.*.(html|htm|shtml|php|js|swf)$ {
deny all;
}
# 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 wp-content/uploads nasty stuff
location ~* ^/wp-content/uploads/.*\.(?:s?html?|php|js|swf)$ {
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;
}
# NGINX RESTRICTIONS
# 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 max;
}
# Web fonts send expires headers
location ~* \.(?:eot|otf|ttf|woff|woff2)$ {
expires max;
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 max;
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 1M;
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 1y;
access_log off;
add_header Cache-Control "public";
}
# HTML send expires headers.
location ~* \.(html)$ {
expires 7d;
access_log off;
add_header Cache-Control "public";
}
# Security settings for better privacy
# Deny hidden files
# Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac).
location ~ /\. {
deny all;
}
# Use the directory /var/www/html to valide acme-challenge
# just create the sub-directories .well-known/acme-challenge and set www-data as owner
# #
# chown -R www-data:www-data /var/www/html && sudo -u www-data mkdir -p /var/www/html/.well-known/acme-challenge
# #
location /.well-known/acme-challenge/ {
alias /var/www/html/.well-known/acme-challenge/;
}
# 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;
}
# common nginx configuration to block sql injection and other attacks
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;
}
@HyperAndreG
Copy link

Thanks!

@TCNOco
Copy link

TCNOco commented Aug 20, 2022

For those not using wordpress at all and just want to block annoying scraper bots:

location ~* /wp- {
    deny all;
}

or better yet:

location ~* /wp- {
    return 302 https://www.youtube.com/watch?v=dQw4w9WgXcQ;
}

@mksoft-systems
Copy link

Hi I used your code but it seems that I have a issue accessing wordpress admin
Can you advice please.

@ustoopia
Copy link

ustoopia commented Mar 10, 2023

return 302 https://www.youtube.com/watch?v=dQw4w9WgXcQ

lmao
I'm going to use it! :-)

@alignwebs
Copy link

alignwebs commented May 12, 2023

It works great!

The only issue I have encountered is with the /wp-content/ajax-handler.php 403 forbidden error. How can we add an exception for it?

@cybersholt
Copy link

It works great!

The only issue I have encountered is with the /wp-content/ajax-handler.php 403 forbidden error. How can we add an exception for it?

Check out line 180.

@boutzamat
Copy link

For those not using wordpress at all and just want to block annoying scraper bots:

location ~* /wp- {
    deny all;
}

or better yet:

location ~* /wp- {
    return 302 https://www.youtube.com/watch?v=dQw4w9WgXcQ;
}

I didn't even have to click the link to know what video that is.

@nfsarmento
Copy link
Author

Hi!

thanks for the tips, one question. Why I see so many

  access_log off;

if someone is trying to access wouldn't be nice to know about it?

Thanks

For that you use the access log of your nginx vhost :)

@nfsarmento
Copy link
Author

@nfsarmento thanks was looking for something quick like this.

Glad I was able to help :) . Thank you

@nfsarmento
Copy link
Author

It works great!
The only issue I have encountered is with the /wp-content/ajax-handler.php 403 forbidden error. How can we add an exception for it?

Check out line 180.

Thank you again :)

@nfsarmento
Copy link
Author

Thanks!

Glad I was able to help :) . Thank you

@rso42
Copy link

rso42 commented Feb 7, 2024

Please be careful with simply copying and pasting this configuration.
For example, there are several contradictory/duplicate location definitions.
In my opinion, this is more of a "best of" collection.

@nfsarmento
Copy link
Author

nfsarmento commented Feb 11, 2024

@rso42 I used in several servers without any issue - It all depends on your site and server configuration. If you think is there any "contradictory/duplicate location definitions" fell free to amended them or even better just tell us which ones :)

@nfsarmento
Copy link
Author

@rso42 - 43 forks and 83 starts on this gist it make me wonder if we are all wrong or just blind that we can't see what you are able to see it :)

@rso42
Copy link

rso42 commented Feb 11, 2024

This config is a total mess.
To be clear:
I warn everyone against simply copying and pasting this config.

Just one exmpale - the config is trying to block .php files 6 times:
location /wp-content/uploads/ { location ~ \.php$ {
location ~* /(?:uploads|files)/.*\.php$ {
location ~* /(?:uploads|files|wp-content|wp-includes|akismet)/.*.php$ {
location ~* ^/wp-content/uploads/.*.(html|htm|shtml|php|js|swf)$ {
location ~* ^/(?:wp-content|wp-includes)/.*\.php$ {
location ~* ^/wp-content/uploads/.*\.(?:s?html?|php|js|swf)$ {
The two 'location ~* /(?:uploads|files)' from above will apply to legitimate php files in any folder with the legitimate name 'files'.
That could break sites/plugins.
It is not a good idea to include several overlapping location rules in a security-relevant configuration.
That makes them susceptible to errors during further processing.

And there are many other issues.
It looks as if many code snippets from different sources have been strung together in this configuration.

@nfsarmento
Copy link
Author

@rso42 uploads folder in WordPress should not hold php at all, due to the uploads folder permissions. Common security enhancement is to prevent PHP execution in the uploads folder - please read WordPress codex for best practise. Can you show me where on the NGINX docs saying that "overlapping" rules are likely to generate any sort of issues ? Note: there is no duplicate rules on file :)

@nfsarmento
Copy link
Author

@rso42 as I said before if you think that something is wrong just fork the gist and improve it. It is public for everyone. Surely if you end up finding this gist is because you were looking for it lolol

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