← Back to DevBytes

Varnish Cache Security Hardening and Best Practices

Introduction to Varnish Cache Security Hardening

Varnish Cache is a high-performance HTTP accelerator that sits between your users and your backend servers. While it dramatically improves response times and reduces backend load, exposing a caching layer to the public internet introduces a unique attack surface. Security hardening Varnish means configuring it so that attackers cannot abuse it to bypass authentication, poison the cache, exhaust resources, or reach protected backends directly.

This tutorial walks through practical hardening techniques using Varnish Configuration Language (VCL), covering access control, input validation, sensitive header handling, rate limiting, and operational best practices. The examples target Varnish 6.x and 7.x.

Why Varnish Security Hardening Matters

A misconfigured Varnish instance can become a liability in several ways:

Hardening Varnish is therefore not optional — it is a required step before exposing it to production traffic.

Securing the Default VCL Template

The default VCL that ships with Varnish is intentionally permissive. A hardened deployment should replace it with a custom configuration that explicitly handles every stage of the request and response lifecycle. Below is a hardened starting template.

vcl 4.1;

import std;
import vsthrottle;

# Define who can access the admin backend
acl admin_network {
    "localhost";
    "10.0.0.0"/8;
    "192.168.1.0"/24;
}

# Define allowed purge sources
acl purge_network {
    "localhost";
    "10.0.0.10";
    "10.0.0.11";
}

backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 2s;
    .first_byte_timeout = 30s;
    .between_bytes_timeout = 5s;
}

sub vcl_recv {
    # Normalize the Host header to prevent cache key collisions
    if (req.http.host) {
        set req.http.host = std.tolower(regsub(req.http.host, ":[0-9]+$", ""));
    }

    # Reject requests with invalid or missing Host headers
    if (!req.http.host || req.http.host == "") {
        return (synth(400, "Bad Request"));
    }

    # Block HTTP TRACE and TRACK methods
    if (req.method == "TRACE" || req.method == "TRACK") {
        return (synth(405, "Method Not Allowed"));
    }

    # Restrict allowed methods
    if (req.method != "GET" &&
        req.method != "HEAD" &&
        req.method != "PUT" &&
        req.method != "POST" &&
        req.method != "DELETE" &&
        req.method != "OPTIONS" &&
        req.method != "PURGE") {
        return (synth(405, "Method Not Allowed"));
    }

    # Handle cache purges securely
    if (req.method == "PURGE") {
        if (!client.ip ~ purge_network) {
            return (synth(403, "Forbidden"));
        }
        return (purge);
    }

    # Never cache authenticated or session-bearing requests
    if (req.http.Authorization || req.http.Cookie) {
        return (pass);
    }

    # Strip query string parameters that do not affect content
    if (req.url ~ "\?.*utm_") {
        set req.url = regsuball(req.url, "(utm_[^&]+&?)*", "");
        set req.url = regsub(req.url, "\?&*", "?");
        set req.url = regsub(req.url, "[?&]+$", "");
    }

    return (hash);
}

sub vcl_backend_response {
    # Never cache responses to authenticated requests
    if (bereq.http.Authorization || bereq.http.Cookie) {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
    }

    # Do not cache private or no-store responses
    if (beresp.http.Cache-Control ~ "private|no-store|no-cache") {
        set beresp.uncacheable = true;
        set beresp.ttl = 0s;
    }

    # Cap TTL to a safe maximum
    if (beresp.ttl > 1h) {
        set beresp.ttl = 1h;
    }

    return (deliver);
}

sub vcl_deliver {
    # Remove identifying headers from responses
    unset resp.http.Server;
    unset resp.http.X-Varnish;
    unset resp.http.Via;
    unset resp.http.X-Powered-By;

    # Add security headers
    set resp.http.X-Content-Type-Options = "nosniff";
    set resp.http.X-Frame-Options = "SAMEORIGIN";
    set resp.http.Referrer-Policy = "no-referrer";
    set resp.http.Strict-Transport-Security = "max-age=31536000; includeSubDomains";
    set resp.http.Content-Security-Policy = "default-src 'self'";

    return (deliver);
}

Access Control with ACLs

Varnish supports Access Control Lists (ACLs) to restrict who can perform privileged operations such as cache purging, ban operations, or accessing administrative endpoints. Always define ACLs explicitly and apply them at the earliest point in vcl_recv.

Restricting Administrative Endpoints

vcl 4.1;

acl admin_network {
    "localhost";
    "10.0.0.0"/8;
}

sub vcl_recv {
    if (req.url ~ "^/admin" || req.url ~ "^/internal/") {
        if (!client.ip ~ admin_network) {
            return (synth(403, "Forbidden"));
        }
    }

    return (hash);
}

This pattern ensures that administrative paths are unreachable from public IPs, even if the backend itself would otherwise serve them.

Preventing Cache Poisoning

Cache poisoning occurs when an attacker manipulates unkeyed inputs — such as headers that do not form part of the cache hash — to make Varnish store a malicious response. The defense is to ensure that all inputs which affect the response are included in the cache key, and to reject or normalize suspicious inputs.

Hashing on Critical Headers

vcl 4.1;

sub vcl_hash {
    hash_data(req.url);

    if (req.http.host) {
        hash_data(req.http.host);
    }

    # Include Accept-Encoding to prevent content-type confusion
    if (req.http.Accept-Encoding) {
        if (req.http.Accept-Encoding ~ "gzip") {
            hash_data("gzip");
        } else if (req.http.Accept-Encoding ~ "br") {
            hash_data("br");
        } else {
            hash_data("none");
        }
    }

    # Include Vary headers explicitly if your backend uses them
    if (req.http.X-Forwarded-Host) {
        hash_data(req.http.X-Forwarded-Host);
    }

    return (lookup);
}

Rejecting Suspicious Headers

Never trust client-supplied X-Forwarded-* headers. Strip and replace them with values Varnish controls.

vcl 4.1;

sub vcl_recv {
    # Remove client-supplied forwarding headers
    unset req.http.X-Forwarded-For;
    unset req.http.X-Forwarded-Host;
    unset req.http.X-Forwarded-Proto;
    unset req.http.X-Real-IP;

    # Set trusted values
    set req.http.X-Forwarded-For = client.ip;
    set req.http.X-Forwarded-Proto = "https";
    set req.http.X-Real-IP = client.ip;

    return (hash);
}

Handling Sensitive Headers and Cookies

Cookies and authorization headers are the most common source of cache leakage. Any request bearing credentials must bypass the cache, and any response containing sensitive data must never be stored.

Stripping Tracking Cookies

Some analytics cookies do not affect content but cause every request to miss the cache. Strip them carefully while preserving session cookies.

vcl 4.1;

sub vcl_recv {
    if (req.http.Cookie) {
        # Remove known tracking cookies
        set req.http.Cookie = regsuball(req.http.Cookie, "_ga=[^;]+;?\s*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "_gid=[^;]+;?\s*", "");
        set req.http.Cookie = regsuball(req.http.Cookie, "_fbp=[^;]+;?\s*", "");

        # Clean up empty cookie header
        if (req.http.Cookie ~ "^\s*$") {
            unset req.http.Cookie;
        }
    }

    # If a session cookie remains, do not cache
    if (req.http.Cookie ~ "sessionid|PHPSESSID|JSESSIONID") {
        return (pass);
    }

    return (hash);
}

Removing Sensitive Response Headers

vcl 4.1;

sub vcl_backend_response {
    # Never leak internal headers to clients
    unset beresp.http.X-Backend-Server;
    unset beresp.http.X-Internal-Debug;
    unset beresp.http.Set-Cookie;

    return (deliver);
}

Be cautious with Set-Cookie removal — only strip it when you are certain the response should not set a cookie. For login endpoints, pass the request through instead.

Rate Limiting and Abuse Prevention

Varnish itself does not include built-in rate limiting, but the vsthrottle VMOD (bundled with many Varnish packages) provides a robust solution. Rate limiting protects both Varnish and your backends from brute force and flooding attacks.

Basic Rate Limiting with vsthrottle

vcl 4.1;

import vsthrottle;

sub vcl_recv {
    # Limit each client to 60 requests per minute
    if (!vsthrottle.is_denied(client.ip, 60, 60s)) {
        return (hash);
    }

    # Exceeded limit
    return (synth(429, "Too Many Requests"));
}

sub vcl_deliver {
    if (resp.status == 429) {
        set resp.http.Retry-After = "60";
    }

    return (deliver);
}

Differentiated Limits for Sensitive Endpoints

vcl 4.1;

import vsthrottle;

sub vcl_recv {
    # Stricter limits on login and password reset endpoints
    if (req.url ~ "^/login" || req.url ~ "^/password-reset") {
        if (vsthrottle.is_denied(client.ip, 10, 60s)) {
            return (synth(429, "Too Many Requests"));
        }
    }

    # General traffic limit
    if (vsthrottle.is_denied(client.ip, 120, 60s)) {
        return (synth(429, "Too Many Requests"));
    }

    return (hash);
}

Protecting the Backend

Varnish should be the only service exposed to the public internet. Backend servers must listen only on localhost or private network interfaces. Configure your backend firewall to accept connections only from the Varnish host.

Backend Connection Hardening

vcl 4.1;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
    .connect_timeout = 2s;
    .first_byte_timeout = 30s;
    .between_bytes_timeout = 5s;
    .max_connections = 200;
    .proxy_header = 2;
}

sub vcl_backend_fetch {
    # Set a maximum backend request timeout
    set bereq.http.X-Forwarded-Proto = "https";

    # Limit request body size for POST/PUT (10MB)
    if (bereq.method == "POST" || bereq.method == "PUT") {
        if (std.integer(req.http.Content-Length, 0) > 10485760) {
            return (abandon);
        }
    }

    return (fetch);
}

Using .proxy_header = 2 enables the PROXY protocol version 2, which securely passes the original client IP to the backend. Ensure your backend supports PROXY protocol before enabling this.

SSL/TLS Termination

Varnish does not natively terminate TLS in versions prior to 6.0, and even in newer versions the recommended approach is to place a TLS-terminating proxy such as Hitch or NGINX in front of Varnish. This separation of concerns keeps Varnish focused on caching while the proxy handles cryptographic operations.

Typical Architecture

Client (HTTPS)
    |
    v
Hitch (TLS termination, port 443)
    |
    v
Varnish (HTTP, port 6081)
    |
    v
Backend (HTTP, port 8080)

Forcing HTTPS Redirects in Varnish

vcl 4.1;

sub vcl_recv {
    # If Varnish receives plain HTTP, redirect to HTTPS
    if (req.http.X-Forwarded-Proto != "https") {
        return (synth(301, "Moved Permanently"));
    }

    return (hash);
}

sub vcl_synth {
    if (resp.status == 301) {
        set resp.http.Location = "https://" + req.http.host + req.url;
        set resp.http.Content-Length = "0";
        return (deliver);
    }

    return (deliver);
}

Input Validation and Request Sanitization

Reject malformed requests early. Varnish should never forward obviously malicious or broken requests to the backend.

Blocking Common Attack Patterns

vcl 4.1;

sub vcl_recv {
    # Block path traversal attempts
    if (req.url ~ "\.\./" || req.url ~ "\.\.\\") {
        return (synth(400, "Bad Request"));
    }

    # Block null bytes
    if (req.url ~ "%00" || req.http.host ~ "%00") {
        return (synth(400, "Bad Request"));
    }

    # Block excessively long URLs
    if (req.url.len > 2048) {
        return (synth(414, "URI Too Long"));
    }

    # Block requests with no User-Agent (common bot behavior)
    if (!req.http.User-Agent) {
        return (synth(403, "Forbidden"));
    }

    # Block known malicious User-Agents
    if (req.http.User-Agent ~ "(?i)(sqlmap|nikto|nmap|masscan|dirbuster)") {
        return (synth(403, "Forbidden"));
    }

    return (hash);
}

Logging, Monitoring, and Incident Response

Security hardening is incomplete without observability. Varnish logs every transaction through its shared memory log, accessible via varnishlog, varnishncsa, and varnishstat.

Custom NCSA Logging Format

Configure varnishncsa to log security-relevant fields:

# /etc/varnish/varnishncsa-format

%{X-Forwarded-For}i %l %u %t "%r" %s %b "%{Referer}i" "%{User-agent}i" "%{Cookie}i" %D

Start varnishncsa with this format:

varnishncsa -F "$(cat /etc/varnish/varnishncsa-format)" -a -w /var/log/varnish/access.log

Key Metrics to Monitor

Integrating with Fail2Ban

You can use Varnish logs with Fail2Ban to automatically block abusive IPs. Create a custom filter:

# /etc/fail2ban/filter.d/varnish-429.conf

[Definition]
failregex = ^<HOST> .* ".*" 429 .*$
ignoreregex =
# /etc/fail2ban/jail.d/varnish.conf

[varnish-429]
enabled = true
filter = varnish-429
logpath = /var/log/varnish/access.log
maxretry = 10
findtime = 60
bantime = 3600

Operational Hardening

Run Varnish as a Non-Root User

Varnish drops privileges by default, but verify the configuration:

# /etc/default/varnish

DAEMON_OPTS="-a :6081 \
             -f /etc/varnish/default.vcl \
             -s malloc,2g \
             -u varnish -g varnish \
             -p vcc_allow_inline_c=off \
             -p http_max_hdr=64 \
             -p http_req_hdr_len=8192 \
             -p http_req_size=32768 \
             -p cli_buffer=8192"

Disable Inline C

The vcc_allow_inline_c=off parameter prevents VCL files from embedding arbitrary C code, which is a significant security risk if an attacker can modify VCL files.

Limit Resource Consumption

# Limit maximum headers and request size to prevent memory exhaustion
-p http_max_hdr=64
-p http_req_hdr_len=8192
-p http_req_size=32768
-p cli_buffer=8192
-p connect_timeout=2s
-p first_byte_timeout=30s
-p between_bytes_timeout=5s

Secure the CLI Port

Varnish exposes a management CLI on port 6082 by default. This port must never be accessible from the public internet. Bind it to localhost only:

# /etc/default/varnish

DAEMON_OPTS="${DAEMON_OPTS} -T 127.0.0.1:6082"

If you need remote management, use SSH tunneling rather than exposing the CLI port directly.

Best Practices Summary

Conclusion

Varnish Cache is a powerful tool for accelerating web applications, but its position at the edge of your infrastructure makes it a critical security checkpoint. By implementing the hardening techniques in this tutorial — custom VCL with strict input validation, ACLs for privileged operations, careful handling of cookies and authorization headers, rate limiting with vsthrottle, backend isolation, TLS termination via a dedicated proxy, and comprehensive logging — you transform Varnish from a simple cache into a first line of defense. Security is an ongoing process: review your VCL regularly, monitor metrics for anomalies, keep Varnish and its VMODs patched, and test configuration changes in staging before deploying to production. A hardened Varnish deployment not only improves performance but also significantly reduces the attack surface of your entire application stack.

— Ad —

Google AdSense will appear here after approval

← Back to all articles