• Resolved Paul Vogel

    (@pavog)


    Hello,

    I would like to report a misbehaviour of the plugin when using a reverse proxy.

    The problem
    The plugin does not recognise the real IP of the user / the request, but only the internal IP / the IP of my reverse proxy. Although I have switched on the setting “My site is behind a reverse proxy” and my reverse proxy (NGINX) is configured to send the required headers along.
    This causes all IP addresses to be blocked when only one IP address should be blocked.

    Let me explain the problem with an example…
    The following situation:
    I switch the option ” Immediately block IP when attempting to log in with a non-existing username” to on. Then I use two devices with different IPs to log in. Let’s say my PC and my smartphone (via LTE -> different IP address).
    On the smartphone, I go to my login URL and try to log in with a fake username + password. With my PC, I also go to my login URL and try to log in with a correct username + password.

    Expected behaviour
    On the smartphone, I cannot log in. The combination of username + password is wrong. Because I have switched on the option “Immediately block IP when attempting to log in with a non-existing username”, I am also blocked for the time being and cannot log in at all for the next few minutes.
    This should not be a problem on the PC because I use a different IP address there. If I want to log in (with a proper username + password), I should be able to log in without any problems.

    Actual behaviour
    I cannot log in on the smartphone. I am blocked immediately.
    But on the PC I can’t log in either because all logins / all IP addresses are blocked.

    This is probably because the plugin does not use the real IP address, but that of the reverse proxy. I also see this in the logs under Dashboard, Activity and Lockouts.

    My setup

    • Latest version of WordPress (5.8)
    • Latest version of WP Cerber Security (8.9)
    • WordPress Plugins: WooCommerce, WooCommerce Germanized, Borlabs Cookie Consent Management and a few more WordPress plugins that you need for a shop. So a few extensions for WooCommerce and such. Nothing extraordinary or weird. Most of the plugins are very popular and widely used.
    • WordPress is run on a VM via Docker. The official Docker images for WordPress (from hub.docker.com/_/wordpress) are used and Docker-Compose is used to run WordPress and a MariaDB.
    • We use a NGINX reverse proxy that is switched in front of the Docker container of WordPress. The NGINX reverse proxy takes care of the accesses and the SSL certificate. The communication between the NGINX and WordPress works without SSL.
    • Settings in WP Cerber Security:
      – “Load security engine”: Normal
      – “Site connection / My site is behind a reverse proxy”: Checked
      All other settings are left at default values. At least I think so…

    This is my NGINX config for the reverse proxy

    user nginx;
    
    worker_processes auto;
    
    error_log /var/log/nginx/error.log warn;
    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;
    
      keepalive_timeout 60;
      gzip on;
      gzip_proxied any;
      server_tokens off;
    
      client_max_body_size 256m;
    
      # ssl config
      # c.f. https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
      # c.f. https://mozilla.github.io/server-side-tls/ssl-config-generator/
      ssl_session_cache shared:SSL:50m;
      ssl_session_timeout 60m;
    
      # modern
      ssl_protocols TLSv1.2;
      ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    
      ssl_prefer_server_ciphers on;
      ssl_session_tickets off;
    
      # security
      # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
      add_header Strict-Transport-Security max-age=15768000;
    
      # generic proxy settings
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    
      # pages may need longer to produce output - this should somewhat correspond to the proxied server timeout
      proxy_read_timeout 400s;
    
      # redirect everything to https
      server
      {
        listen 80;
        server_name _;
    
        # document root for letsencrypt certbot challenges
        location /.well-known/acme-challenge
        {
          root /proxy/conf/ssl/letsencrypt;
        }
    
        # everything else redirected to https
        location /
        {
          return 302 https://$host$request_uri;
        }
      }
    
      server
      {
        listen 443 ssl http2;
        server_name mysite.url;
    
        resolver 127.0.0.11 valid=10s;
    
        # ssl
        ssl_certificate /etc/letsencrypt/live/mysite.url/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/mysite.url/privkey.pem;
    
        location /
        {
          proxy_set_header X-Forwarded-Proto $scheme;
          proxy_set_header Host $host;
    
          http2_push_preload on;
          proxy_intercept_errors on;
          set $upstream https://mysite.url-container-name:80;
          proxy_pass $upstream$request_uri;
        }
      }
    }
    • This topic was modified 3 years, 4 months ago by Paul Vogel.
Viewing 1 replies (of 1 total)
  • Thread Starter Paul Vogel

    (@pavog)

    I have found a solution to the problem.
    It was not caused by the plugin, but by an incorrect configuration in our infrastructure.

    TLDR
    The problem was that the real IP address of the client was not passed on to the WordPress container and thus also to the plugin.

    The first problem was that we used the headers in the NGINX config in two different places, namely at the top as a generic part and again within server or location config section. Unfortunately this does not work, NGINX then ignores the first (generic) part.
    For this to work, we had to specify all proxy-specific headers in the generic part of the config. So we moved the following entries to the top:

    • proxy_set_header Host $host;
    • proxy_set_header X-Real-IP $remote_addr;
    • proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    • proxy_set_header X-Forwarded-Proto $scheme;

    The new full NGINX config is below (attached).

    Only with this configuration did the real client IP address reach the WordPress Docker container. Unfortunately, the header X-Forwarded-For was still not displayed. I tried this with a call to phpinfo().

    Here is a detailed explanation
    To begin with, an explanation of how the WordPress Docker Container works:
    We use the image for WordPress-PHP7.4-Apache from the official repository on hub.docker.com.
    In the Dockerfile for this, it is written into the Apache-Config that the header X-Forwarded-For should be used to set the remote IP. See: Dockerfile on GitHub
    Unfortunately, this does not take over the real header X-Forwarded-For, which was set by the NGINX reverse proxy.
    This means that the header X-Forwarded-For never arrives at WordPress or the plugin. Therefore, the code that would use this header can never work when WordPress is run using this Docker image.

    But the plugin needs the X-Forwarded-For header, at least that’s what I thought. Here is the code from cerber-common.php:

    /**
     * Detect and return remote client IP address
     *
     * @return string Valid IP address
     * @since 6.0
     */
    function cerber_get_remote_ip() {
    	static $remote_ip;
    
    	if ( isset( $remote_ip ) ) {
    		return $remote_ip;
    	}
    
    	if ( defined( 'CERBER_IP_KEY' ) ) {
    		$remote_ip = filter_var( $_SERVER[ CERBER_IP_KEY ], FILTER_VALIDATE_IP );
    	}
    	elseif ( crb_get_settings( 'proxy' ) && isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
    		$list = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
    		foreach ( $list as $maybe_ip ) {
    			$remote_ip = filter_var( trim( $maybe_ip ), FILTER_VALIDATE_IP );
    			if ( $remote_ip ) {
    				break;
    			}
    		}
    		if ( ! $remote_ip && isset( $_SERVER['HTTP_X_REAL_IP'] ) ) {
    			$remote_ip = filter_var( $_SERVER['HTTP_X_REAL_IP'], FILTER_VALIDATE_IP );
    		}
    	}
    	else {
    		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
    			$remote_ip = $_SERVER['REMOTE_ADDR'];
    		}
    		elseif ( ! empty( $_SERVER['HTTP_X_REAL_IP'] ) ) {
    			$remote_ip = $_SERVER['HTTP_X_REAL_IP'];
    		}
    		elseif ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
    			$remote_ip = $_SERVER['HTTP_CLIENT_IP'];
    		}
    		$remote_ip = filter_var( $remote_ip, FILTER_VALIDATE_IP );
    	}
    
    	if ( ! $remote_ip ) { // including WP-CLI, other way is: if defined('WP_CLI')
    		$remote_ip = CERBER_NO_REMOTE_IP;
    	}
    
    	if ( cerber_is_ipv6( $remote_ip ) ) {
    		$remote_ip = cerber_ipv6_short( $remote_ip );
    	}
    
    	return $remote_ip;
    }

    Fortunately, this code still has a fallback:
    It can also use HTTP_X_REAL_IP, REMOTE_ADDR or HTTP_CIENT_IP.

    That’s why it works now. Yay.
    Thank you ??

    Here is my (new) NGINX config:

    user nginx;
    
    worker_processes auto;
    
    error_log /var/log/nginx/error.log warn;
    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;
    
      keepalive_timeout 60;
      gzip on;
      gzip_proxied any;
      server_tokens off;
    
      client_max_body_size 256m;
    
      # ssl config
      # c.f. https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html
      # c.f. https://mozilla.github.io/server-side-tls/ssl-config-generator/
      ssl_session_cache shared:SSL:50m;
      ssl_session_timeout 60m;
    
      # modern
      ssl_protocols TLSv1.2;
      ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
    
      ssl_prefer_server_ciphers on;
      ssl_session_tickets off;
    
      # security
      # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
      add_header Strict-Transport-Security max-age=15768000;
    
      # generic proxy settings
      proxy_set_header Host $host;
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header X-Forwarded-Proto $scheme;
    
      # pages may need longer to produce output - this should somewhat correspond to the proxied server timeout
      proxy_read_timeout 400s;
    
      # redirect everything to https
      server
      {
        listen 80;
        server_name _;
    
        # document root for letsencrypt certbot challenges
        location /.well-known/acme-challenge
        {
          root /proxy/conf/ssl/letsencrypt;
        }
    
        # everything else redirected to https
        location /
        {
          return 302 https://$host$request_uri;
        }
      }
      
      server
      {
        listen 443 ssl http2;
        server_name mysite.url;
    
        resolver 127.0.0.11 valid=10s;
    
        # ssl
        ssl_certificate /etc/letsencrypt/live/mysite.url/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/mysite.url/privkey.pem;
        
        location /
        {
          http2_push_preload on;
          proxy_intercept_errors on;
          set $upstream https://mysite.url-container-name:80;
          proxy_pass $upstream$request_uri;
        }
      }
    }
Viewing 1 replies (of 1 total)
  • The topic ‘Plugin uses the IP of the reverse proxy and not the real IP of the user’ is closed to new replies.