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:
- Cache poisoning — Attackers trick Varnish into caching malicious or incorrect responses served to other users.
- Authentication bypass — Cached responses meant for one user get served to another, leaking private data.
- Backend exposure — Direct access to backend ports bypasses Varnish protections entirely.
- Resource exhaustion — Unbounded cache sizes or unthrottled requests lead to denial of service.
- Information leakage — Server headers, debug responses, or internal IPs leak through the cache.
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
- cache_hit and cache_miss — Sudden drops in hit rate may indicate cache poisoning or configuration errors.
- client_req — Spikes indicate possible flooding attacks.
- backend_conn — Unexpected increases suggest cache bypass attempts.
- sess_queued and sess_dropped — Indicate resource exhaustion.
- n_lru_nuked — High values indicate the cache is too small, which can be exploited for cache denial of service.
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
- Always use a custom VCL — Never rely on the default configuration in production.
- Validate and normalize all inputs — Strip untrusted headers, normalize URLs, and reject malformed requests early.
- Never cache authenticated responses — Bypass the cache for any request with Authorization headers or session cookies.
- Use ACLs for privileged operations — Restrict purges, bans, and administrative access to trusted networks.
- Include all response-affecting inputs in the cache hash — Prevent cache poisoning by hashing on Host, URL, and relevant Vary headers.
- Remove identifying headers — Strip Server, X-Varnish, Via, and X-Powered-By from responses.
- Add security headers — Set HSTS, X-Frame-Options, X-Content-Type-Options, CSP, and Referrer-Policy.
- Implement rate limiting — Use vsthrottle to protect against brute force and flooding.
- Isolate backends — Backends should only accept connections from Varnish, never from the public internet.
- Terminate TLS separately — Use Hitch or NGINX for TLS, keeping Varnish focused on caching.
- Monitor and log — Track cache hit rates, request volumes, and backend connections. Integrate with Fail2Ban for automated response.
- Run as non-root with inline C disabled — Minimize the blast radius of any compromise.
- Cap cache TTLs — Prevent stale or poisoned content from persisting indefinitely.
- Test VCL before deployment — Use
varnishd -C -f default.vclto compile-check VCL before loading it. - Keep Varnish updated — Apply security patches promptly, as Varnish has had CVEs affecting request smuggling and denial of service.
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.