wordpressnginxvarnishvarnish-vclvarnish-4

Varnish Cache Strange Issue showing same images /css/txt files on index page or other pages


I am trying to fix an issue on one of my Varnish Cache + Nginx setup on a Wordpress website. I am running the following server stack

Nginx( HTTPS - 443 ) -> Varnish ( HTTP - 80 ) -> Nginx (HTTP - 8080 )

I am using a vhost method for Varnish cache so that I can host multiple site on he same varnish server. Currently this server is hosting only one website on Wordpress.

The Issue ? The varnish is delivering random images / CSS file or even robot.txt to all pages while accessing the website. I included the following vcl file in default.vcl

mydomain.com.vcl

# Auto Generated Varnish Configuration
# Wordpress Varnish  Optimizations
# Build time : 2024-03-15 14:19:11.575844
# Domain : mydomain.com
import proxy;
sub vcl_recv {
    if (req.http.host=="mydomain.com" || req.http.host=="www.mydomain.com") { 
        # Set the backend 
        set req.backend_hint = ip_3a355ffbe92e19d4c646b0943e689d609954ef7d; 
        
        # Remove empty query string parameters    
        if (req.url ~ "\?$") {
            set req.url = regsub(req.url, "\?$", "");
        }

        # Sorts query string parameters alphabetically for cache normalization purposes
        set req.url = std.querysort(req.url);

        # Remove the proxy header to mitigate the httpoxy vulnerability    
        unset req.http.proxy;

        # Add X-Forwarded-Proto header when using https
        if (!req.http.X-Forwarded-Proto) {
            if(std.port(server.ip) == 443 || proxy.is_ssl()) {
                set req.http.X-Forwarded-Proto = "https";
            } else {
                set req.http.X-Forwarded-Proto = "http";
            }
        }
        # Only handle relevant HTTP request methods
        if (
            req.method != "GET" &&
            req.method != "HEAD" &&
            req.method != "PUT" &&
            req.method != "POST" &&
            req.method != "PATCH" &&
            req.method != "TRACE" &&
            req.method != "OPTIONS" &&
            req.method != "DELETE"
        ) {
            return (pipe);
        }
        # Only cache GET and HEAD requests
        if (req.method != "GET" && req.method != "HEAD") {
            set req.http.X-Cacheable = "NO:REQUEST-METHOD";
            return(pass);
        }
        # Mark static files with the X-Static-File header, and remove any cookies
        # X-Static-File is also used in vcl_backend_response to identify static files
        if (req.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
           set req.http.X-Static-File = "true";
         #   unset req.http.Cookie;
         #   return(hash);
         #   return(pass);     #=> If I pass the image the site works fine
        }
        # No caching of special URLs, logged in users and some plugins
        if (
            req.http.Cookie ~ "wordpress_(?!test_)[a-zA-Z0-9_]+|wp-postpass|comment_author_[a-zA-Z0-9_]+|woocommerce_cart_hash|woocommerce_items_in_cart|wp_woocommerce_session_[a-zA-Z0-9]+|wordpress_logged_in_|comment_author|PHPSESSID" ||
            req.http.Authorization ||
            req.url ~ "add_to_cart" ||
            req.url ~ "edd_action" ||
            req.url ~ "nocache" ||
            req.url ~ "^/addons" ||
            req.url ~ "^/bb-admin" ||
            req.url ~ "^/bb-login.php" ||
            req.url ~ "^/bb-reset-password.php" ||
            req.url ~ "^/cart" ||
            req.url ~ "^/checkout" ||
            req.url ~ "^/control.php" ||
            req.url ~ "^/login" ||
            req.url ~ "^/logout" ||
            req.url ~ "^/lost-password" ||
            req.url ~ "^/my-account" ||
            req.url ~ "^/product" ||
            req.url ~ "^/register" ||
            req.url ~ "^/register.php" ||
            req.url ~ "^/server-status" ||
            req.url ~ "^/signin" ||
            req.url ~ "^/signup" ||
            req.url ~ "^/stats" ||
            req.url ~ "^/wc-api" ||
            req.url ~ "^/wp-admin" ||
            req.url ~ "^/wp-comments-post.php" ||
            req.url ~ "^/wp-cron.php" ||
            req.url ~ "^/wp-login.php" ||
            req.url ~ "^/wp-activate.php" ||
            req.url ~ "^/wp-mail.php" ||
            req.url ~ "^/wp-login.php" ||
            req.url ~ "^\?add-to-cart=" ||
            req.url ~ "^\?wc-api=" ||
            req.url ~ "^/preview=" ||
            req.url ~ "^/\.well-known/acme-challenge/"
        ) {
            set req.http.X-Cacheable = "NO:Logged in/Got Sessions";
            if(req.http.X-Requested-With == "XMLHttpRequest") {
                set req.http.X-Cacheable = "NO:Ajax";
            }
            return(pass);
        }
        # Enable GZIP compression
        if (req.http.Accept-Encoding) {
            if (req.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
                unset req.http.Accept-Encoding;
            } elsif (req.http.Accept-Encoding ~ "gzip") {
                set req.http.Accept-Encoding = "gzip";
            } elsif (req.http.Accept-Encoding ~ "deflate" &&
                req.http.user-agent !~ "MSIE") {
                set req.http.Accept-Encoding = "deflate";
            } else {
                unset req.http.Accept-Encoding;
            }
        }
        
        # Remove any cookies left
        unset req.http.Cookie;
        return(hash);
    }   
}
    
sub vcl_hash {
    if (req.http.host=="mydomain.com" || req.http.host=="www.mydomain.com") { 
        if(req.http.X-Forwarded-Proto) {
            # Create cache variations depending on the request protocol       
            hash_data(req.http.X-Forwarded-Proto);
        }
        return (lookup);
    }
}

sub vcl_backend_response {
    if (bereq.http.host=="mydomain.com" || bereq.http.host=="www.mydomain.com") {
    # Do not cache 404 error
    if ( beresp.status >= 301 ) {
      set beresp.ttl = 30s;
      set beresp.uncacheable = true;
      return (deliver);
    }
   
        # Inject URL & Host header into the object for asynchronous banning purposes
        set beresp.http.x-url = bereq.url;
        set beresp.http.x-host = bereq.http.host;

        # If we dont get a Cache-Control header from the backend
        # we default to 1h cache for all objects
        if (!beresp.http.Cache-Control) {
            set beresp.ttl = 1h;
            set beresp.http.X-Cacheable = "YES:Forced";
        }

        # If the file is marked as static we cache it for 1 day
        if (bereq.http.X-Static-File == "true") {
            unset beresp.http.Set-Cookie;
            set beresp.http.X-Cacheable = "YES:Forced";
            set beresp.ttl = 1d;
        }

        # Remove the Set-Cookie header when a specific Wordfence cookie is set
        if (beresp.http.Set-Cookie ~ "wfvt_|wordfence_verifiedHuman") {
            unset beresp.http.Set-Cookie;
            }
        
        if (beresp.http.Set-Cookie) {
            set beresp.http.X-Cacheable = "NO:Got Cookies";
        } elseif(beresp.http.Cache-Control ~ "private") {
            set beresp.http.X-Cacheable = "NO:Cache-Control=private";
        
        } elseif (beresp.http.Cache-Control ~ "must-revalidate") {
            set beresp.grace = 0s;
        }

        # Removice cookies on static files
        if (bereq.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
            unset beresp.http.cookie;
        }


        # Gzip compression
        if (beresp.http.url ~ "^[^?]*\.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(\?.*)?$") {
             set beresp.do_gzip = false;
        }
        else {
            set beresp.do_gzip = true;
            set beresp.http.X-Cache = "ZIP";
        }
        # Do not cache  301, 302, 404, 403, 502 .etc.  error
    
        return (deliver);
    }   
}

sub vcl_deliver {
    if (req.http.host=="mydomain.com" || req.http.host=="www.mydomain.com") {
        # You can do accounting or modifying the final object here.
        if (obj.hits) { # Add debug header to see if it's a HIT/MISS and the number of hits, disable when not needed
            set resp.http.X-Cache = "HIT";
        } else {
            set resp.http.X-Cache = "MISS";
        }
        # Cleanup of headers
        unset resp.http.x-url;
        unset resp.http.x-host;   
        unset resp.http.x-powered-by;        
        unset resp.http.X-Powered-By;        
        return (deliver);
    }
}
# Bypass webmail , eenos etc,.
# Domain : mail.mydomain.com
sub vcl_recv {
    # Set the backend 
    if (req.http.host=="mail.mydomain.com" || req.http.host=="www.mail.mydomain.com" || req.http.host=="eenos.mydomain.com"){
        set req.backend_hint = ip_3a355ffbe92e19d4c646b0943e689d609954ef7d; 
        return (pass);
    }
}

As you can see this is the same official varnish Wordpress settings provided from their website with some minor changes.

The following is my Varnish SSL termination on Nginx

#.......... HTTPS VHOST ..................................
# Varnish ssl termination 
server {
    listen 10.0.0.79:443 ssl ;    
    http2 on;    
    server_name  mydomain.com  www.mydomain.com;   
    ssl_certificate /etc/letsencrypt/live/mydomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mydomain.com/privkey.pem;
    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-SHA;
    # Browser control ssl ciphers
    ssl_prefer_server_ciphers off;
    ssl_session_cache   shared:SSL:20m;
    ssl_session_timeout 60m;        
    # All access logs will be handled by proxy
    access_log off;
     # Disable direct access to .ht files and folders
    location ~ /\.(ht|ini|log|conf)$ {
          deny all;
    }
    keepalive_requests 100;
    keepalive_timeout 60s;
    # Directory listing disabled
    autoindex off;
# External Alias
    location / {
        proxy_pass http://10.0.0.79:80;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Host $http_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 https;
        proxy_set_header X-Forwarded-Port 443;
        proxy_set_header HTTPS "on";
        proxy_buffering on;
        proxy_send_timeout 600s;
        proxy_read_timeout 600s;
        proxy_buffer_size 256k;
        proxy_buffers 128 256k;
        proxy_busy_buffers_size 256k;
        proxy_temp_file_write_size 256k;
        proxy_connect_timeout 300s;
        proxy_http_version 1.1;
          
    }
}

The Nginx Wordpress Vhost

server {
    listen 10.0.0.79:8080;    
    server_name  mydomain.com  www.mydomain.com;   
    # lets encrypt auto ssl acme validation
    location ^~ /.well-known/acme-challenge/ {
        default_type "text/plain";                
        root /var/www/html;
    }
    root /home/site/public_html;
    index index.php  index.perl index.pl  index.cgi  index.phtml index.shtml index.xhtml index.html index.htm index.wml Default.html Default.htm default.html default.htm home.html home.htm eenos.html;
    location = /favicon.ico { log_not_found off; }
    # All access logs   
    access_log /var/log/domlogs/mydomain.com main;
    access_log /var/log/domlogs/mydomain.com-bytes_log bytes_log;   

    referer_hash_bucket_size 512;    
    # Run static file directly from nginx 
    location ~* ^.+.(jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|iso|doc|xls|exe|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|mp3|ogv|ogg|flv|swf|mpeg|mpg|mpeg4|mp4|avi|wmv|js|css|3gp|sis|sisx|nth)$ {
        expires 30d;    
    }

    # Disable direct access to .ht files and folders
    location ~ /\.(ht|ini|log|conf)$ {
          deny all;
    }
    keepalive_requests 100;
    keepalive_timeout 60s;
    # Directory listing disabled
    autoindex off;
#phpfiles
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass 127.0.0.1:9000;
        #fastcgi_pass unix:/var/run/3446b18857ef807e294d091515955c3666797768.sock;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include /etc/nginx/fastcgi_params;
    }
    # Disable direct access to .php files in the following on a wordpress site
    location ~* /(?:uploads|files)/.*\.php$ {
        deny all;
    }
    location / { 
      client_max_body_size 2000m;
        client_body_buffer_size 512k;
        try_files $uri $uri/ /index.php?$args;
    }
}

if I add return(pass); in the vcl file X-Static-File section , the website works fine, but the varnish don't cache the static files . Does anyone ever faced the issue. I am running Varnish 7.4


Solution

  • Just remove return (lookup); from vcl_hash and your problem should be fixed.

    By performing a return, you're basically bypassing the standard behavior that Varnish has for this VCL subroutine.

    In the case of vcl_hash it's important to have req.url & req.http.Host as the values to identify an object in the cache.

    For mydomain.com & www.mydomain.com you're using the X-Forwarded-Proto header value to identify the object in the cache, but by performing return(lookup), the URL & Host header value are lost.

    Long story short: remove return(lookup) from vcl_hash.