├── .dockerignore ├── .github └── FUNDING.yml ├── error_pages ├── 401.html ├── 403.html ├── 404.html ├── 405.html ├── 406.html ├── 412.html └── 50x.html ├── proxy.conf ├── screenshots ├── nmap.jpg ├── plecost.jpg └── wp-scan.jpg ├── default.conf ├── docker-compose.yml ├── Dockerfile ├── custom-errors.conf ├── .gitignore ├── restrictions.conf ├── docker-entrypoint.sh ├── nginx.conf ├── wordpress.conf └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | LICENSE 4 | *.md -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cr0hn] 4 | -------------------------------------------------------------------------------- /error_pages/401.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/error_pages/401.html -------------------------------------------------------------------------------- /error_pages/403.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/error_pages/403.html -------------------------------------------------------------------------------- /error_pages/404.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/error_pages/404.html -------------------------------------------------------------------------------- /error_pages/405.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/error_pages/405.html -------------------------------------------------------------------------------- /error_pages/406.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/error_pages/406.html -------------------------------------------------------------------------------- /error_pages/412.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/error_pages/412.html -------------------------------------------------------------------------------- /proxy.conf: -------------------------------------------------------------------------------- 1 | real_ip_header X-Forwarded-For; 2 | set_real_ip_from 172.16.0.0/12; 3 | real_ip_recursive on; 4 | -------------------------------------------------------------------------------- /screenshots/nmap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/screenshots/nmap.jpg -------------------------------------------------------------------------------- /screenshots/plecost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/screenshots/plecost.jpg -------------------------------------------------------------------------------- /screenshots/wp-scan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr0hn/nginx-wordpress-docker-sec/HEAD/screenshots/wp-scan.jpg -------------------------------------------------------------------------------- /default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | server_name _; 4 | root /var/www/html; 5 | 6 | client_max_body_size 64m; 7 | 8 | index index.php; 9 | 10 | include global/restrictions.conf; 11 | include global/wordpress.conf; 12 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | wordpress: 5 | image: hardened-wordpress 6 | depends_on: 7 | - mysql 8 | environment: 9 | WORDPRESS_DB_PASSWORD: my-secret-pw 10 | volumes: 11 | - wordpress:/var/www/html 12 | 13 | nginx: 14 | image: nginx-wordpress 15 | depends_on: 16 | - wordpress 17 | volumes: 18 | - wordpress:/var/www/html/ 19 | ports: 20 | - "8080:80" 21 | environment: 22 | POST_MAX_SIZE: 128m 23 | 24 | mysql: 25 | image: mysql:5.7 26 | environment: 27 | MYSQL_ROOT_PASSWORD: my-secret-pw 28 | MYSQL_DATABASE: wordpress 29 | 30 | 31 | volumes: 32 | wordpress: -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | RUN apt-get update && apt-get upgrade -y 4 | 5 | RUN apt-get install -y libterm-readline-perl-perl && apt-get install -y nginx-common && apt-get upgrade -y nginx && \ 6 | apt-get install -y libnginx-mod-http-headers-more-filter && apt-get clean 7 | 8 | COPY default.conf /etc/nginx/conf.d/default.conf 9 | COPY wordpress.conf /etc/nginx/global/wordpress.conf 10 | COPY restrictions.conf /etc/nginx/global/restrictions.conf 11 | COPY proxy.conf /etc/nginx/global/proxy.conf 12 | COPY docker-entrypoint.sh /entrypoint.sh 13 | COPY nginx.conf /etc/nginx/nginx.conf 14 | COPY custom-errors.conf /etc/nginx/custom-errors.conf 15 | COPY error_pages/* /usr/share/nginx/html/ 16 | 17 | RUN chmod -R 755 /usr/share/nginx/html && \ 18 | chown -R www-data:www-data /usr/share/nginx/html 19 | 20 | ENTRYPOINT ["/entrypoint.sh"] 21 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /custom-errors.conf: -------------------------------------------------------------------------------- 1 | error_page 401 /401.html; 2 | location = /401.html { 3 | root /usr/share/nginx/html; 4 | internal; 5 | } 6 | error_page 403 /403.html; 7 | location = /403.html { 8 | root /usr/share/nginx/html; 9 | internal; 10 | } 11 | error_page 404 /404.html; 12 | location = /404.html { 13 | root /usr/share/nginx/html; 14 | internal; 15 | } 16 | error_page 405 /405.html; 17 | location = /405.html { 18 | root /usr/share/nginx/html; 19 | internal; 20 | } 21 | error_page 406 /406.html; 22 | location = /406.html { 23 | root /usr/share/nginx/html; 24 | internal; 25 | } 26 | error_page 412 /412.html; 27 | location = /412.html { 28 | root /usr/share/nginx/html; 29 | internal; 30 | } 31 | 32 | error_page 500 502 503 504 /50x.html; 33 | location = /50x.html { 34 | root /usr/share/nginx/html; 35 | internal; 36 | } -------------------------------------------------------------------------------- /error_pages/50x.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 500 - Error interno del servidor. 6 | 19 | 20 | 21 | 22 |
23 |
24 |

500 - Error interno del servidor.

25 |

Hay un problema con el recurso que busca y no se puede mostrar.

26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | 6 | # User-specific stuff: 7 | .idea/**/workspace.xml 8 | .idea/**/tasks.xml 9 | .idea/dictionaries 10 | 11 | # Sensitive or high-churn files: 12 | .idea/**/dataSources/ 13 | .idea/**/dataSources.ids 14 | .idea/**/dataSources.xml 15 | .idea/**/dataSources.local.xml 16 | .idea/**/sqlDataSources.xml 17 | .idea/**/dynamic.xml 18 | .idea/**/uiDesigner.xml 19 | 20 | # Gradle: 21 | .idea/**/gradle.xml 22 | .idea/**/libraries 23 | 24 | # CMake 25 | cmake-build-debug/ 26 | cmake-build-release/ 27 | 28 | # Mongo Explorer plugin: 29 | .idea/**/mongoSettings.xml 30 | 31 | ## File-based project format: 32 | *.iws 33 | 34 | ## Plugin-specific files: 35 | 36 | # IntelliJ 37 | out/ 38 | 39 | # mpeltonen/sbt-idea plugin 40 | .idea_modules/ 41 | 42 | # JIRA plugin 43 | atlassian-ide-plugin.xml 44 | 45 | # Cursive Clojure plugin 46 | .idea/replstate.xml 47 | 48 | # Crashlytics plugin (for Android Studio and IntelliJ) 49 | com_crashlytics_export_strings.xml 50 | crashlytics.properties 51 | crashlytics-build.properties 52 | fabric.properties 53 | -------------------------------------------------------------------------------- /restrictions.conf: -------------------------------------------------------------------------------- 1 | # Global restrictions configuration file. 2 | # Designed to be included in any server {} block. 3 | location = /favicon.ico { 4 | log_not_found off; 5 | access_log off; 6 | } 7 | 8 | location = /robots.txt { 9 | allow all; 10 | log_not_found off; 11 | access_log off; 12 | try_files $uri /index.php?$args; 13 | } 14 | 15 | # Deny all attempts to access hidden files such as .htaccess, .htpasswd, .DS_Store (Mac). 16 | # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban) 17 | location ~ /\. { 18 | deny all; 19 | } 20 | 21 | # Deny access to any files with a .php extension in the uploads directory 22 | # Works in sub-directory installs and also in multisite network 23 | # Keep logging the requests to parse later (or to pass to firewall utilities such as fail2ban) 24 | location ~* /(?:uploads|files)/.*\.php$ { 25 | deny all; 26 | } 27 | 28 | # 29 | # These rules was taken from: https://jesus.perezpaz.es/874/wordpress-nginx-configuration/ 30 | # 31 | # Common deny or internal locations, to help prevent access to areas of 32 | # the site that should not be public 33 | location ~* wp-admin/includes { deny all; } 34 | location ~* wp-includes/theme-compat/ { deny all; } 35 | location ~* wp-includes/js/tinymce/langs/.*\.php { deny all; } 36 | location /wp-content/ { internal; } 37 | location /wp-includes/ { internal; } 38 | 39 | # Protects the wp-config.php|readme.html|license.txt files from being 40 | # accessed (uncomment after wordpress installation) 41 | #location ~ /(\.|wp-config.php|readme.html|license.txt|README.txt|README.md|readme.md|Readme.txt|changelog.txt|CHANGELOG.md|changelog.md|error_log|index.php) { deny all; } 42 | location ~ /(\.|wp-config.php|readme.html|license.txt|README.txt|README.md|readme.md|Readme.txt|changelog.txt|CHANGELOG.md|changelog.md|error_log) { deny all; } 43 | 44 | # Prevent access to any files starting with a $ (usually temp files) 45 | location ~ ~$ { access_log off; log_not_found off; deny all; } -------------------------------------------------------------------------------- /docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | common_post_max_size() { 5 | if [[ ! "${POST_MAX_SIZE}" =~ ^([0-9]+)([kKmMgG]?)$ ]]; then 6 | echo >&2 'error: invalid value "'"${POST_MAX_SIZE}"'" for POST_MAX_SIZE environment variable' 7 | exit 1 8 | fi 9 | 10 | local VALUE="${BASH_REMATCH[1]}" 11 | local UNIT="${BASH_REMATCH[2]}" 12 | 13 | # Nginx does not support Gigabyte unit, convert it to Megabytes 14 | if [ "${UNIT}" == "g" ] || [ "${UNIT}" == "G" ]; then 15 | VALUE="$(($VALUE * 1024))" 16 | UNIT="m" 17 | fi 18 | POST_MAX_SIZE="${VALUE}${UNIT}" 19 | } 20 | 21 | escape_sed() { 22 | echo "$1" | sed -e 's/[\/&]/\\&/g' 23 | } 24 | 25 | if [ "$1" == nginx ]; then 26 | : "${POST_MAX_SIZE:=64m}" 27 | : "${BEHIND_PROXY:=$([ -z ${VIRTUAL_HOST} ] && echo "false" || echo "true")}" 28 | : "${REAL_IP_HEADER:=X-Forwarded-For}" 29 | : "${REAL_IP_FROM:=172.17.0.0/16}" 30 | : "${WP_CONTAINER_NAME:=wordpress}" 31 | 32 | common_post_max_size 33 | 34 | sed -i 's/client_max_body_size *[0-9]\+[kKmM]\?/client_max_body_size '"${POST_MAX_SIZE}"'/' /etc/nginx/conf.d/default.conf 35 | sed -i 's/upload_max_filesize *= *[0-9]\+[kKmMgG]\?/upload_max_filesize='"${POST_MAX_SIZE}"'/' /etc/nginx/global/wordpress.conf 36 | sed -i 's/post_max_size *= *[0-9]\+[kKmMgG]\?/post_max_size='"${POST_MAX_SIZE}"'/' /etc/nginx/global/wordpress.conf 37 | sed -i 's/fastcgi_pass .*;/fastcgi_pass '"$(escape_sed "${WP_CONTAINER_NAME}")"':9000;/' /etc/nginx/global/wordpress.conf 38 | 39 | if [ "${BEHIND_PROXY}" == "true" ]; then 40 | sed -i 's/real_ip_header .*;/real_ip_header '"$(escape_sed "${REAL_IP_HEADER}")"';/' /etc/nginx/global/proxy.conf 41 | sed -i 's/set_real_ip_from .*;/set_real_ip_from '"$(escape_sed "${REAL_IP_FROM}")"';/' /etc/nginx/global/proxy.conf 42 | grep -qF 'include global/proxy.conf;' /etc/nginx/conf.d/default.conf || \ 43 | sed -i '/^}/i\ include global/proxy.conf;' /etc/nginx/conf.d/default.conf 44 | else 45 | sed -i '/include global\/proxy.conf;/d' /etc/nginx/conf.d/default.conf 46 | fi 47 | fi 48 | 49 | exec "$@" 50 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | user www-data; 2 | worker_processes auto; 3 | pid /run/nginx.pid; 4 | include /etc/nginx/modules-enabled/*.conf; 5 | 6 | events { 7 | worker_connections 4096; 8 | multi_accept on; 9 | use epoll; 10 | } 11 | 12 | http { 13 | 14 | ## 15 | # Basic Settings 16 | ## 17 | 18 | sendfile on; 19 | tcp_nopush on; 20 | tcp_nodelay on; 21 | keepalive_timeout 65; 22 | types_hash_max_size 2048; 23 | server_tokens off; 24 | server_name_in_redirect off; 25 | more_set_headers 'Server: Microsoft-IIS/8.5'; 26 | more_set_headers 'X-Powered-By: ASP.NET 4.8'; 27 | 28 | client_body_buffer_size 128k; 29 | client_max_body_size 10m; 30 | client_header_buffer_size 1k; 31 | large_client_header_buffers 4 4k; 32 | output_buffers 1 32k; 33 | postpone_output 1460; 34 | 35 | client_header_timeout 3m; 36 | client_body_timeout 3m; 37 | send_timeout 3m; 38 | 39 | # server_names_hash_bucket_size 64; 40 | # server_name_in_redirect off; 41 | 42 | include /etc/nginx/mime.types; 43 | default_type application/octet-stream; 44 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 45 | '$status $body_bytes_sent "$http_referer" ' 46 | '"$http_user_agent" "$http_x_forwarded_for"'; 47 | 48 | ## 49 | # Error pages 50 | ## 51 | error_page 401 /401.html; 52 | error_page 403 /403.html; 53 | error_page 404 /404.html; 54 | error_page 405 /405.html; 55 | error_page 406 /406.html; 56 | error_page 412 /412.html; 57 | error_page 500 502 503 504 /50x.html; 58 | 59 | gzip on; 60 | gzip_disable "MSIE [1-6].(?!.*SV1)"; 61 | 62 | gzip_vary on; 63 | gzip_proxied any; 64 | gzip_comp_level 6; 65 | gzip_buffers 16 8k; 66 | gzip_http_version 1.1; 67 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 68 | 69 | ## 70 | # Virtual Host Configs 71 | ## 72 | include /etc/nginx/conf.d/*.conf; 73 | } -------------------------------------------------------------------------------- /wordpress.conf: -------------------------------------------------------------------------------- 1 | include /etc/nginx/custom-errors.conf; 2 | 3 | # WordPress single site rules. 4 | # Designed to be included in any server {} block. 5 | 6 | # This order might seem weird - this is attempted to match last if rules below fail. 7 | # http://wiki.nginx.org/HttpCoreModule 8 | location / { 9 | try_files $uri $uri/ /index.php?$args; 10 | } 11 | 12 | # Add trailing slash to */wp-admin requests. 13 | rewrite /wp-admin$ $scheme://$host$uri/ permanent; 14 | 15 | # Directives to send expires headers and turn off 404 error logging. 16 | location ~* ^.+\.(eot|otf|woff|woff2|ttf|rss|atom|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf|json)$ { 17 | access_log off; log_not_found off; expires max; 18 | } 19 | 20 | # Media: images, icons, video, audio send expires headers. 21 | location ~* \.(?:jpg|jpeg|gif|png|ico|cur|gz|svg|svgz|mp4|ogg|ogv|webm|json)$ { 22 | expires 1M; 23 | access_log off; 24 | add_header Cache-Control "public"; 25 | } 26 | 27 | # CSS and Javascript send expires headers. 28 | location ~* \.(?:css|js)$ { 29 | expires 1y; 30 | access_log off; 31 | add_header Cache-Control "public"; 32 | } 33 | 34 | # HTML send expires headers. 35 | location ~* \.(html)$ { 36 | expires 7d; 37 | access_log off; 38 | add_header Cache-Control "public"; 39 | } 40 | 41 | # Browser caching of static assets. 42 | location ~* \.(jpg|jpeg|png|gif|ico|css|js|pdf|json)$ { 43 | expires 7d; 44 | add_header Cache-Control "public, no-transform"; 45 | } 46 | 47 | # Enable Gzip compression in NGNIX. 48 | gzip on; 49 | gzip_disable "msie6"; 50 | 51 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml; 52 | 53 | # Pass all .php files onto a php-fpm/php-fcgi server. 54 | location ~ [^/]\.php(/|$) { 55 | fastcgi_split_path_info ^(.+?\.php)(/.*)$; 56 | if (!-f $document_root$fastcgi_script_name) { 57 | return 404; 58 | } 59 | # This is a robust solution for path info security issue and works with "cgi.fix_pathinfo = 1" in /etc/php.ini (default) 60 | 61 | include fastcgi_params; 62 | fastcgi_index index.php; 63 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 64 | fastcgi_param PHP_VALUE "upload_max_filesize=64m 65 | post_max_size=64m"; 66 | fastcgi_pass wordpress:9000; 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Anti-hacking tools deployment of Nginx for WordPress 2 | 3 | This repo only add small rules in the nginx configuration for Wordpress, **without change any internal functionality** of Wordpress. 4 | 5 | **The main goal is to disable hacking tools lik: WP-Scan or Plecost** 6 | 7 | # Support this project 8 | 9 | Support this project (to solve issues, new features...) by applying the Github "Sponsor" button. 10 | 11 | # Security rules 12 | 13 | - Denied access to the Readme, license or changelog files from plugins, to avoid extract plugins versions 14 | - Denied access to .htaccess files 15 | - Denied access to files starting at symbol: "~", usually backup files 16 | - Denied public access to upload.php / file.php 17 | - Denied public access to any .php file from a theme 18 | - Denied public access to wp.config.php file 19 | - Denied public access error_log file 20 | - Limited internal-only access to *wp-content/* and *wp-includes/* 21 | 22 | # Paranoid-rules 23 | 24 | I recognize, I'm a bit paranoid. So, this repo also implement: 25 | 26 | - Nginx will tell to the scanners that it's a "Microsoft-IIS/8.5" instead of nginx 27 | - Nginx will change the typical HTTP header response, *X-Powered-By: PHP/7.5" to *X-Powered-By: ASP.NET 4.8" 28 | 29 | 30 | # Examples 31 | 32 | This docker image must be complemented with the *wordpress-docker-sec* image, that you can find at: https://github.com/cr0hn/wordpress-docker-sec 33 | 34 | To quick test, you can download the *docker-compose.yml* form this repo and launch a complete hardened stack of Wordpress: 35 | 36 | ```yaml 37 | 38 | version: "3" 39 | services: 40 | 41 | wordpress: 42 | image: cr0hn/wordpress-docker-sec 43 | depends_on: 44 | - mysql 45 | environment: 46 | - WORDPRESS_DB_USER=my-user 47 | - WORDPRESS_DB_HOST=mysql 48 | - WORDPRESS_DB_PASSWORD=my-secret-pw 49 | - WORDPRESS_DB_NAME=wordpress 50 | - WORDPRESS_TABLE_PREFIX=mycustomprefix_ 51 | volumes: 52 | - wordpress:/var/www/html 53 | 54 | nginx: 55 | image: cr0hn/nginx-wordpress-docker-sec 56 | depends_on: 57 | - wordpress 58 | volumes: 59 | - wordpress:/var/www/html/ 60 | ports: 61 | - "8080:80" 62 | environment: 63 | POST_MAX_SIZE: 128m 64 | 65 | mysql: 66 | image: mysql:5.7 67 | environment: 68 | MYSQL_ROOT_PASSWORD: my-secret-pw 69 | MYSQL_DATABASE: wordpress 70 | 71 | 72 | volumes: 73 | wordpress: 74 | ``` 75 | 76 | # Screenshots 77 | 78 | If you deploy this version of configuration for Nginx + wordpress-docker-sec (see below) hacking tools will tell you something like: 79 | 80 | ## WP-Scan 81 | 82 | ![WP-SCan](screenshots/wp-scan.jpg) 83 | 84 | ## Plecost 85 | 86 | ![Plecost](screenshots/plecost.jpg) 87 | 88 | ## Nmap 89 | 90 | ![Nmap](screenshots/nmap.jpg) 91 | --------------------------------------------------------------------------------