Score:0

apache behind varnish; htaccess deny rules ignored

de flag

So, I have a wordpress website running behind nginx -> varnish -> httpd

The htaccess rule for wp-login.php is set as:

<Files wp-login.php>
allow from client ip
deny from all
</Files>

This used to work fine without using varnish, but when I put varnish in the middle between nginx and httpd, this caching issue or IP forwarding issue started happening.

Since I can easily change the webserver config, I can disable/enable varnish for a site easily. So, as a test, I changed the htaccess rule to:

<Files wp-login.php>
allow from Server Public IP
deny from all
</Files>

This made wp-login accessible to everyone. Then I disabled varnish and kept the Server public IP in htaccess, and now nobody can access the page (which is what is supposed to happen).

So, the culprit is varnish.

I have mod_cloudflare setup on apache. I've also tested by switching it to mod_remoteip to no avail.

Here's my nginx:443, varnish:82 and apache:8181 vhost templates (This IP 108.148.54.124 is an example for the server public IP):

server {
    listen 108.148.54.124:443 ssl http2;
    server_name %domain_idn% %alias_idn%;
    
    access_log /usr/local/apache/domlogs/%domain%.bytes bytes;
    access_log /usr/local/apache/domlogs/%domain%.log full;
    error_log /usr/local/apache/domlogs/%domain%.error.log error;

    ssl_certificate      %ssl_cert_path%/%domain%.bundle;
    ssl_certificate_key  %ssl_key_path%/%domain%.key;
    ssl_protocols TLSv1.3;
    ssl_ciphers EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA!RC4:EECDH:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS;
    ssl_prefer_server_ciphers   on;

    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 60m;

    location / {
        location ~.*\.(3gp|gif|jpg|jpeg|png|ico|wmv|avi|asf|asx|mpg|mpeg|mp4|pls|mp3|mid|wav|swf|flv|html|htm|txt|js|css|exe|zip|tar|rar|gz|tgz|bz2|uha|7z|doc|docx|xls|xlsx|pdf|iso|woff|ttf|svg|eot|sh|webp)$ {
            root %docroot%;
            expires max;
            try_files $uri $uri/ @backend;
        }
        
        error_page 405 = @backend;
        error_page 500 = @custom;
        add_header X-Cache "HIT from Backend";
        add_header Strict-Transport-Security "max-age=31536000";
        add_header X-XSS-Protection "1; mode=block" always;
        add_header X-Content-Type-Options "nosniff" always;
        proxy_pass %proxy_protocol%://108.148.54.124:82;
        include proxy.inc;
    }

    location @backend {
        internal;
        proxy_pass %proxy_protocol%://108.148.54.124:82;
        include proxy.inc;
    }

    location @custom {
        internal;
        proxy_pass %proxy_protocol%://108.148.54.124:82;
        include proxy.inc;
    }

    location ~ .*\.(php|jsp|cgi|pl|py)?$ {
        proxy_pass %proxy_protocol%://108.148.54.124:82;
        include proxy.inc;
    }

    location ~ /\.ht    {deny all;}
    location ~ /\.svn/  {deny all;}
    location ~ /\.git/  {deny all;}
    location ~ /\.hg/   {deny all;}
    location ~ /\.bzr/  {deny all;}
    location ~\.(ini|log|conf)$ {deny all;error_page 403 =404 / ;}

    disable_symlinks if_not_owner from=%docroot%;

    location /.well-known/acme-challenge {
        default_type "text/plain";
        alias /usr/local/apache/autossl_tmp/.well-known/acme-challenge;
    }

    location /.well-known/pki-validation {
        default_type "text/plain";
        alias /usr/local/apache/autossl_tmp/.well-known/acme-challenge;
    }
}
.....
backend %backend_domain% {
    .host = "108.148.54.124";
    .port = "8181";
}

sub vcl_recv {
    if (req.http.host ~ "%domain%") {
        set req.backend_hint = %backend_domain%;

        # Always cache the following file types for all users.
        if (req.url ~ "(?i)\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm)(\?[a-z0-9]+)?$") {
            unset req.http.Cookie;
        }

        # Remove any Google Analytics based cookies
        set req.http.Cookie = regsuball(req.http.Cookie, "has_js=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "__utm.=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "_ga=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "utmctr=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "utmcmd.=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "utmccn.=[^;]+(; )?", "");

        # Do not cache AJAX requests.
        if (req.http.X-Requested-With == "XMLHttpRequest") {
            return(pass);
        }

        # Post requests will not be cached
        if (req.http.Authorization || req.method == "POST") {
            return (pass);
        }
        if (req.method != "GET" && req.method != "HEAD") {
            return (pass);
        }

        # Do not cache Authorized requests.
        if (req.http.Authorization) {
            return(pass);
        }

        # LetsEncrypt Certbot passthrough
        if (req.url ~ "^/\.well-known/acme-challenge/") {
            return (pass);
        }

        if (req.url ~ "^/\.well-known/pki-validation/") {
            return (pass);
        }

        # Forward client's IP to the backend
        if (req.restarts == 0) {
            if (req.http.X-Real-IP) {
                set req.http.X-Forwarded-For = req.http.X-Real-IP;
            } else if (req.http.X-Forwarded-For) {
                set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
            } else {
                set req.http.X-Forwarded-For = client.ip;
            }
        }

        ### Wordpress ###
        if (req.url ~ "(wp-admin|post\.php|edit\.php|wp-login)") {
            return(pass);
        }
        if (req.url ~ "/wp-cron.php" || req.url ~ "preview=true") {
            return (pass);
        }

        # WP-Affiliate
        if ( req.url ~ "\?ref=" ) {
            return (pass);
        }

        set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-1=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "wp-settings-time-1=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "wordpress_test_cookie=[^;]+(; )?", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "PHPSESSID=[^;]+(; )?", "");

        return (hash);
    }
}
<VirtualHost 108.148.54.124:8443>
    ServerName %domain_idn%
    %domain_aliases%
    ServerAdmin webmaster@%domain%
    DocumentRoot %docroot%
    UseCanonicalName Off
    ScriptAlias /cgi-bin/ %docroot%/cgi-bin/

    CustomLog /usr/local/apache/domlogs/%domain%.bytes bytes
    CustomLog /usr/local/apache/domlogs/%domain%.log combined
    ErrorLog /usr/local/apache/domlogs/%domain%.error.log

    ## Custom settings are loaded below this line (if any exist)
    # IncludeOptional "/usr/local/apache/conf/userdata/%user%/%domain%/*.conf"

    SSLEngine on
    SSLCertificateFile %ssl_cert_path%/%domain%.cert
    SSLCertificateKeyFile %ssl_key_path%/%domain%.key
    SSLCertificateChainFile %ssl_cert_path%/%domain%.bundle
    SetEnvIf User-Agent ".*MSIE.*" nokeepalive ssl-unclean-shutdown

    <IfModule mod_userdir.c>
        UserDir disabled
        UserDir enabled %user%
    </IfModule>

    <IfModule mod_suexec.c>
        SuexecUserGroup %user% %group%
    </IfModule>

    <IfModule mod_suphp.c>
        suPHP_UserGroup %user% %group%
        suPHP_ConfigPath %home%/%user%
    </IfModule>

    <IfModule mod_ruid2.c>
        RMode config
        RUidGid %user% %group%
    </IfModule>

    <IfModule itk.c>
        AssignUserID %user% %group%
    </IfModule>

    <Directory "%docroot%">
        AllowOverride All
        SSLRequireSSL
        Require all granted
    </Directory>

    <IfModule proxy_fcgi_module>
        <FilesMatch \.php$>
            SetHandler "proxy:%backend_fcgi%|fcgi://localhost"
        </FilesMatch>
    </IfModule>

</VirtualHost>

and this is the main nginx conf file:

user nobody;
worker_processes auto;
#worker_rlimit_nofile    65535;
error_log               /var/log/nginx/error.log crit;
pid                     /var/run/nginx.pid;

events {
    worker_connections  1024;
    use                 epoll;
    multi_accept        on;

}
http {
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    client_header_timeout 3m;
    client_body_timeout 3m;
    client_max_body_size 256m;
    client_header_buffer_size 4k;
    client_body_buffer_size 256k;
    large_client_header_buffers 4 32k;
    send_timeout 3m;
    keepalive_timeout 60 60;
    reset_timedout_connection       on;
    server_names_hash_max_size 1024;
    server_names_hash_bucket_size 1024;
    ignore_invalid_headers on;
    connection_pool_size 256;
    request_pool_size 4k;
    output_buffers 4 32k;
    postpone_output 1460;

    include mime.types;
    default_type application/octet-stream;

    # Compression gzip
    gzip on;
    gzip_vary on;
    gzip_disable "MSIE [1-6]\.";
    gzip_proxied any;
    gzip_min_length 512;
    gzip_comp_level 6;
    gzip_buffers 8 64k;
    gzip_types text/plain text/xml text/css text/js application/x-javascript application/xml image/png image/x-icon image/gif image/jpeg image/svg+xml application/xml+rss text/javascript application/atom+xml application/javascript application/json application/x-font-ttf font/opentype;

    # Proxy settings
    proxy_redirect      off;
    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_pass_header   Set-Cookie;
    proxy_connect_timeout   300;
    proxy_send_timeout  300;
    proxy_read_timeout  300;
    proxy_buffers       32 4k;
    proxy_cache_path /var/cache/nginx levels=2 keys_zone=cache:10m inactive=60m max_size=512m;
    proxy_cache_key "$host$request_uri $cookie_user";
    proxy_temp_path  /var/cache/nginx/temp;
    proxy_ignore_headers Expires Cache-Control;
    proxy_cache_use_stale error timeout invalid_header http_502;
    proxy_cache_valid any 1d;

    open_file_cache_valid 120s;
    open_file_cache_min_uses 2;
    open_file_cache_errors off;
    open_file_cache max=5000 inactive=30s;
    open_log_file_cache max=1024 inactive=30s min_uses=2;

    # SSL Settings
    ssl_session_cache   shared:SSL:10m;
    ssl_protocols       TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers        "EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM:EECDH+ECDSA+SHA384:EECDH+ECDSA+SHA256:EECDH+aRSA+SHA384:EECDH+aRSA+SHA256:EECDH+aRSA!RC4:EECDH:!RC4:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!SRP:!DSS";

    # Logs
    log_format  main    '$remote_addr - $remote_user [$time_local] $request '
                        '"$status" $body_bytes_sent "$http_referer" '
                        '"$http_user_agent" "$http_x_forwarded_for"';
    log_format  full '[$time_local] $remote_addr $remote_user - "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for"';
    log_format  bytes   '$body_bytes_sent';
    #access_log          /var/log/nginx/access.log main;
    access_log off;

    # Cache bypass
    map $http_cookie $no_cache {
        default 0;
        ~SESS 1;
        ~wordpress_logged_in 1;
    }

    # Include additional configuration
    include /etc/nginx/cloudflare.inc;
    include /etc/nginx/conf.d/*.conf;
}
iraqiboy90 avatar
de flag
The server-status for apache shows only Server IP as client whether varnish is enabled or not.
Score:0
de flag

I just found a solution, which is to use this instead

<Files wp-login.php>
SetEnvIf X-Forwarded-For %Client_IP% allow_me
Allow from env=allow_me
deny from all
</Files>

I can also put the SetEnvIf outside, i.e. at the top of the htaccess file, and just change the ip on this line every time my IP changes.

SetEnvIf X-Forwarded-For %Client_IP% allow_me
<Files wp-login.php>
Allow from env=allow_me
deny from all
</Files>
Score:0
in flag

The X-Forward-For header should contain the client IP address. This header will be set by Nginx and will also be used by Varnish.

It is possible that the X-Forward-For header looks like this:

X-Forwarded-For: 1.2.3.4, 5.6.7.8

It's a matter of extracting the first value and matching that in your .htaccess file.

This article provides an easy way to perform allow from calls based on the value of the X-Forwarded-For header: Apache, use X-Forwarded-For for allow

This could translate into the following configuration:

<Files wp-login.php>
    SetEnvIf X-Forwarded-For ^1\.2\.3\.4 proxy_env
    Order allow,deny
    Satisfy Any
    Allow from env=proxy_env
    Deny from all
</Files>

You can also restrict access to wp-login.php in your Nginx configuration or in your Varnish VCL.

iraqiboy90 avatar
de flag
Thanks, but this almost repeats the same answer I wrote above... The only thing I didn't mention in the question is why I was trying to "allow from SERVER IP" in the first place. It was because mod_security was blocking some requests made by the server, so I whitelisted the server, which caused that everyone was whitelisted from mod_security, which led me into the path that whitelisting while having varnish infront of apache will be trouble.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.