├── .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 |
23 |
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 | 
83 |
84 | ## Plecost
85 |
86 | 
87 |
88 | ## Nmap
89 |
90 | 
91 |
--------------------------------------------------------------------------------