Software Engineering WikiSE Wiki

Squid

Run and debug a Squid forward proxy: http_access rule order, ACLs, caching, authentication, parent proxies and access.log analysis.

Reviewed MarkdownEdit

On this page

Cheatsheet#

TaskCommand
Check config syntax before reloadsquid -k parse
Apply config without a restartsquid -k reconfigure
Rotate logssquid -k rotate
Create cache directories (first run, new cache_dir)squid -z
Runtime statisticscurl -s http://localhost:3128/squid-internal-mgr/info
Requests in flightcurl -s http://localhost:3128/squid-internal-mgr/active_requests
Test a URL through the proxycurl -x http://localhost:3128 -sI https://example.com
Recent denialsgrep TCP_DENIED /var/log/squid/access.log | tail
Purge one cached objectcurl -s -X PURGE -x http://localhost:3128 http://example.com/file
Trace ACL decisionsdebug_options ALL,1 28,3 then read cache.log

squidclient was removed in Squid 7

The cache manager answers plain HTTP requests at /squid-internal-mgr/<page> on the proxy port. Use curl for every mgr: page older guides fetch with squidclient. The cache_object:// URL scheme stopped working after Squid 6.5. See the cache manager docs.

First checks for a failing proxy#

squid -k parse                                        # config errors, exits non-zero on failure
systemctl status squid                                # running, last start error
journalctl -u squid -n 50 --no-pager                  # startup and reconfigure messages
curl -x http://localhost:3128 -sI https://example.com # end-to-end from the proxy host
tail -n 20 /var/log/squid/access.log                  # what Squid decided for each request

Each access.log line carries a result code such as TCP_MISS/200 or TCP_DENIED/403. A TCP_DENIED means an http_access rule refused the request. A /5xx with a non-denied tag means Squid accepted it but could not reach the origin (DNS, routing, parent proxy). No line at all means the request never reached Squid (client proxy settings, firewall). See systemd if the service itself will not start.

How a request is decided#

Squid evaluates http_access lines top to bottom and stops at the first line whose ACLs all match. The action on that line (allow or deny) is final, so an allow placed after a matching deny never runs. ACL names on one line are ANDed; separate lines are ORed. Prefix an ACL name with ! to negate it.

acl localnet src 10.0.0.0/8
acl SSL_ports port 443
acl Safe_ports port 80 443 21 70 210 1025-65535
acl CONNECT method CONNECT

http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localnet
http_access deny all

If no line matches, Squid applies the opposite of the last line’s action. A list ending in allow localnet therefore denies everything else, while a list ending in a deny allows everything else. Always end with an explicit http_access deny all so the default never depends on the last rule.

To see which rule matched, raise debug section 28 (access control) and read cache.log. Higher levels log more of each ACL check; 9 is the most verbose.

debug_options ALL,1 28,3
squid -k reconfigure
tail -f /var/log/squid/cache.log | grep -E 'ACL|checking'

Revert debug_options afterwards: high levels grow cache.log quickly under load.

ACL types#

TypeMatchesExample
src / dstClient / server IP address or rangeacl office src 192.0.2.0/24
dstdomainDestination host nameacl allowed dstdomain .example.com
dstdom_regexHost name regexacl ads dstdom_regex -i (^|\.)ads?\.
url_regexFull URL regexacl media url_regex -i \.(mp4|iso)$
portDestination portacl SSL_ports port 443
methodHTTP methodacl CONNECT method CONNECT
timeDay and time windowacl work time MTWHF 08:00-18:00
proxy_authAuthenticated user nameacl users proxy_auth REQUIRED
maxconnClient concurrent connections above a limitacl heavy maxconn 20
req_mime_typeRequest Content-Typeacl upload req_mime_type -i ^multipart/form-data

A leading dot in dstdomain matches the domain and all subdomains; without the dot the match is exact. For CONNECT requests Squid only sees the host name and port, so url_regex cannot match paths inside HTTPS. Keep long lists in files, one entry per line:

acl allowed dstdomain "/etc/squid/allowed-domains.txt"

Regex ACLs run on every request and are slower than dstdomain. Prefer dstdomain where a domain list does the job.

Minimal working configuration#

A forward proxy for a private network, with caching and no authentication.

http_port 3128
visible_hostname proxy.example.com

acl localnet src 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16
acl SSL_ports port 443
acl Safe_ports port 80 443
acl CONNECT method CONNECT

http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localnet
http_access deny all

cache_dir ufs /var/spool/squid 10000 16 256   # 10000 MB, 16 first-level, 256 second-level dirs
maximum_object_size 512 MB
cache_mem 512 MB
coredump_dir /var/spool/squid

access_log daemon:/var/log/squid/access.log squid
logfile_rotate 7

forwarded_for delete   # drop X-Forwarded-For so internal client IPs do not leak upstream
via off                # drop the Via header
squid -z                          # create cache_dir structure; run once with Squid stopped
systemctl enable --now squid      # needs root
squid -k parse && squid -k reconfigure   # after every change

squid -k reconfigure re-reads the config in the running process. Existing tunnels normally survive. Changing workers or the cache_dir layout needs a full restart (and squid -z for a new cache_dir).

Caching#

Squid follows origin cache headers. Cache-Control: no-store or private stops storage. A response with no Expires, max-age or Last-Modified gets heuristic freshness from refresh_pattern, which only applies when the origin gave no explicit freshness (unless an override option is set).

# regex                        min(min) percent max(min) options
refresh_pattern -i \.(deb|rpm|whl|tgz)$ 10080  90%  43200  override-expire
refresh_pattern ^ftp:                   1440   20%  10080
refresh_pattern .                          0   20%   4320

override-expire and similar options break HTTP semantics for the matched URLs. Use them only for content you know is immutable, such as versioned package files.

HTTPS through CONNECT is an opaque TCP tunnel. Squid sees the host name and byte counts, caches nothing, and logs TCP_TUNNEL. Caching package mirrors therefore needs plain-HTTP mirrors, or SSL bumping (see below).

Log tagMeaning
TCP_HITServed from disk cache
TCP_MEM_HITServed from memory cache
TCP_REFRESH_UNMODIFIEDRevalidated with the origin, which returned 304
TCP_REFRESH_MODIFIEDRevalidated, origin sent new content
TCP_MISSFetched from origin (or parent)
TCP_DENIEDRefused by http_access or another access list
TCP_TUNNELCONNECT tunnel; contents never cacheable
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'hit ratio|StoreEntries|Storage'
curl -s http://localhost:3128/squid-internal-mgr/storedir | grep -E 'Maximum|Current'

Purging needs an ACL for the PURGE method, placed before deny all. Without it Squid answers 403.

acl PURGE method PURGE
http_access allow localhost PURGE
http_access deny PURGE
curl -s -o /dev/null -w '%{http_code}\n' -X PURGE -x http://localhost:3128 http://example.com/file
200

A 404 means the object was not cached.

Authentication#

auth_param basic program /usr/lib64/squid/basic_ncsa_auth /etc/squid/passwd
auth_param basic children 20
auth_param basic realm Proxy
auth_param basic credentialsttl 2 hours

acl authenticated proxy_auth REQUIRED
http_access allow authenticated

The helper path is distribution-specific: /usr/lib64/squid/ on Fedora and RHEL, /usr/lib/squid/ on Debian and Ubuntu.

htpasswd -c /etc/squid/passwd alice   # -c creates or overwrites the file; omit it to add users

The proxy_auth ACL triggers a 407 Proxy Authentication Required challenge when it is evaluated and no credentials are present. Put it after rules for unauthenticated traffic you want to allow, or those clients get prompted too.

Basic authentication sends the password base64-encoded on every request. Accept it only on a trusted network or with TLS between client and proxy (https_port). For directory accounts use basic_ldap_auth; on domain-joined clients negotiate_kerberos_auth avoids sending passwords at all.

Transparent and reverse modes#

# Intercept: the router or nftables redirects port 80 to 3129; clients have no proxy setting
http_port 3129 intercept

# Reverse proxy (accelerator) in front of one origin
http_port 80 accel defaultsite=app.example.com
cache_peer 192.0.2.5 parent 8080 0 no-query originserver name=app
acl our_sites dstdomain app.example.com
http_access allow our_sites
cache_peer_access app allow our_sites
cache_peer_access app deny all

Keep a separate, non-intercept http_port 3128 for clients that set the proxy explicitly. Intercept ports disable proxy authentication and expect NAT-redirected traffic.

Inspecting HTTPS needs SSL bump: Squid terminates TLS with certificates generated from a CA that clients trust, then opens its own TLS connection to the origin. peek and splice let Squid read the SNI and pass a connection through without decrypting it, which is how you exempt categories.

SSL bump decrypts user traffic

Get written authorisation and legal review before deploying it, and splice (do not bump) banking, health and other sensitive categories. Certificate-pinned applications break when bumped. Configuration details are version-specific: follow the SslPeekAndSplice page for your release. See TLS for how the generated chain is validated.

Upstream proxies#

cache_peer upstream.example.com parent 3128 0 no-query default
acl internal dstdomain .internal.example.com
never_direct deny internal   # internal hosts may go direct
never_direct allow all       # everything else must use a parent

Without never_direct, Squid is free to connect to origins directly, for example when the parent is down. That leaks connections in a network that only permits egress through the parent. always_direct allow <acl> does the reverse and makes matching requests skip all peers. Confirm with the hierarchy field in access.log: FIRSTUP_PARENT/ or DEFAULT_PARENT/ means the parent was used, HIER_DIRECT/ means it was not.

awk '{print $9}' /var/log/squid/access.log | cut -d/ -f1 | sort | uniq -c
  8123 DEFAULT_PARENT
   212 HIER_DIRECT
    40 HIER_NONE

HIER_NONE is normal for denials and cache hits.

Logs and monitoring#

The native squid log format has these fields, which the one-liners below rely on:

FieldContent
$1Timestamp (Unix seconds with milliseconds)
$2Elapsed time in ms
$3Client IP
$4Result code / HTTP status, e.g. TCP_MISS/200
$5Bytes sent to client
$6Method
$7URL (host:port for CONNECT)
$8User name or -
$9Hierarchy code / peer, e.g. HIER_DIRECT/203.0.113.10
$10Content type

A custom logformat changes these positions. To keep the native format and add a second log for another tool, add a second access_log line with a different format name.

curl -s http://localhost:3128/squid-internal-mgr/info            # uptime, hit ratios, FD usage
curl -s http://localhost:3128/squid-internal-mgr/5min | grep -E 'client_http|server_http'
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'file desc'

Alert on the denied-request ratio, file descriptor usage and cache disk fullness. A Prometheus exporter such as squid-exporter can scrape the cache manager; see Prometheus. Running out of file descriptors shows as random connection failures under load.

Scripts#

A health check suitable for a monitoring probe or a systemd ExecStartPost. It exits non-zero when the config is invalid, the process is down, or a canary URL fails through the proxy.

#!/usr/bin/env bash
set -euo pipefail
proxy=${1:-http://localhost:3128}
canary=${2:-https://example.com}

squid -k parse >/dev/null 2>&1 || { echo "config invalid"; exit 1; }
systemctl is-active --quiet squid   || { echo "squid not running"; exit 1; }

code=$(curl -x "$proxy" -o /dev/null -sw '%{http_code}' --max-time 10 "$canary" || true)
[[ $code =~ ^(200|204|301|302)$ ]] || { echo "canary failed: HTTP $code"; exit 1; }

fd=$(curl -s "$proxy/squid-internal-mgr/info" | awk -F'\t' '/Number of file desc.*avail/ {print $2}')
echo "ok: canary $code, fds available $fd"

An access.log summary for the last N minutes: request rate, hit ratio, denial rate and the top offenders. Run it from cron and pipe the output into a chat webhook or a report.

#!/usr/bin/env bash
set -euo pipefail
log=${1:-/var/log/squid/access.log}
mins=${2:-15}
since=$(( $(date +%s) - mins*60 ))

awk -v s="$since" '$1 >= s {
    n++; bytes += $5
    if ($4 ~ /HIT/)    hit++
    if ($4 ~ /DENIED/) deny++
    host=$7; sub(/^[a-z]+:\/\//,"",host); sub(/[:\/].*/,"",host); dst[host]++
    cli[$3]++
  }
  END {
    if (!n) { print "no requests in window"; exit }
    printf "requests: %d (%.1f/s)\n", n, n/(60*'"$mins"')
    printf "hit ratio: %.1f%%   denied: %.1f%%   traffic: %.1f MB\n", 100*hit/n, 100*deny/n, bytes/1048576
    print "top clients:";      for (c in cli) print "  " cli[c], c | "sort -rn | head -5"
    close("sort -rn | head -5")
    print "top destinations:"; for (d in dst) print "  " dst[d], d | "sort -rn | head -5"
  }' "$log"

A safe bulk purge: read a list of URLs and issue PURGE for each, reporting which were cached (200) and which were not (404). Requires the PURGE method ACL shown above.

#!/usr/bin/env bash
set -euo pipefail
proxy=${PROXY:-http://localhost:3128}
while IFS= read -r url; do
  [[ -z $url || $url == \#* ]] && continue
  code=$(curl -s -o /dev/null -w '%{http_code}' -X PURGE -x "$proxy" "$url")
  printf '%s  %s\n' "$code" "$url"
done < "${1:-urls.txt}"

Troubleshooting#

SymptomLikely causeCheck
Everything deniedAn earlier deny matched, or the implicit default deniedTCP_DENIED lines; debug_options 28,3
HTTPS fails, HTTP worksCONNECT denied by SSL_ports / Safe_portsLog shows TCP_DENIED/403 ... CONNECT host:443
Clients prompted for a password unexpectedlyproxy_auth ACL evaluated before an allow ruleOrder of http_access lines
Works by IP, fails by nameSquid’s resolver cannot resolvedns_nameservers; cache.log DNS errors; DNS
Slow or failing on dual-stack hostsBroken IPv6 path on the proxy; dns_v4_first was removed in Squid 5connect_timeout; IPv6 routing on the proxy
Slow first byteDNS, parent proxy or origin latency, not caching$2 elapsed times; HTTP timing
Too many open filesFD limitRaise LimitNOFILE in the unit and max_filedescriptors
Cache never hitsOrigin sends no-store / private, or traffic is all TCP_TUNNELResult codes in access.log
PURGE returns 403No PURGE method ACL allowedSee the purge ACL above
Reconfigure had no effectConfig error, or a directive that needs a restartsquid -k parse; journalctl -u squid
Intercepted traffic loops or 4xxExplicit-proxy clients pointed at the intercept port, or NAT lookup failedSeparate ports; cache.log
Disk cache never fillscache_dir too small, or objects exceed maximum_object_sizestoredir mgr page; raise the size ceilings
High latency only on cache missesSlow origin or parent, or ufs storage stalling the main threadElapsed times on MISS lines; switch to aufs/rock
WARNING: swapfile ... not found in cache.logCache index and disk disagree after an unclean stopStop Squid, squid -z, restart; corruption clears on rebuild
commBind: Cannot bind socket ... Address already in useAnother process on http_port, or a previous Squid still runningss -ltnp sport = :3128; kill the stale process
Client gets the wrong site’s certificateSSL bump generating a cert for the default site, or wrong SNI peekcache.log at debug_options 83,3; check ssl_bump peek order
refresh_pattern ignoredAn earlier pattern matched first, or the origin sent explicit freshnessRules are first-match top to bottom; check origin Cache-Control

Oneliners#

These assume the native squid log format.

# Result codes by count
awk '{print $4}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head

# Top destination hosts
awk '{print $7}' /var/log/squid/access.log | sed -E 's#^[a-z]+://##; s#[:/].*##' | sort | uniq -c | sort -rn | head

# Top clients by request count
awk '{print $3}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head

# Bytes served per client
awk '{b[$3]+=$5} END {for (c in b) printf "%12d %s\n", b[c], c}' /var/log/squid/access.log | sort -rn | head

# Denied requests with client and URL
awk '$4 ~ /DENIED/ {print $3, $7}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head

# Hit ratio from the log
awk '{t++; if ($4 ~ /HIT/) h++} END {printf "%.1f%% of %d\n", 100*h/t, t}' /var/log/squid/access.log

# Requests per minute (strftime needs gawk)
gawk '{print strftime("%H:%M", $1)}' /var/log/squid/access.log | uniq -c | tail -20

# Status and total time for a site through the proxy, from a client
curl -x http://proxy.example.com:3128 -o /dev/null -sw '%{http_code} %{time_total}\n' https://example.com

# Show the running http_access rules (the "config" page is refused unless cachemgr_passwd allows it)
curl -s http://localhost:3128/squid-internal-mgr/config | grep -E '^http_access'

# Validate then reload
squid -k parse && squid -k reconfigure

# Slowest requests: elapsed time, status, URL
sort -k2 -rn /var/log/squid/access.log | awk '{print $2, $4, $7}' | head

# Bandwidth per destination host (bytes sent to client)
awk '{h=$7; sub(/^[a-z]+:\/\//,"",h); sub(/[:\/].*/,"",h); b[h]+=$5} END {for (x in b) printf "%12d %s\n", b[x], x}' /var/log/squid/access.log | sort -rn | head

# CONNECT (HTTPS) targets, which never cache
awk '$6=="CONNECT" {print $7}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head

# Requests per authenticated user
awk '$8!="-" {print $8}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head

# Average and max elapsed time in ms
awk '{s+=$2; if ($2>m) m=$2} END {printf "avg=%.0f max=%d n=%d\n", s/NR, m, NR}' /var/log/squid/access.log

# 5xx responses with the origin and hierarchy code
awk '$4 ~ /\/5[0-9][0-9]$/ {print $4, $9, $7}' /var/log/squid/access.log | sort | uniq -c | sort -rn | head

# Cache hit ratio for a single content type (e.g. images)
awk '$10 ~ /^image/ {t++; if ($4 ~ /HIT/) h++} END {if (t) printf "%.1f%% of %d\n", 100*h/t, t}' /var/log/squid/access.log

# File descriptor usage right now
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'file descriptor|Number of file'

# Live memory and disk cache utilisation
curl -s http://localhost:3128/squid-internal-mgr/info | grep -iE 'Storage (Mem|Swap) size|Maximum'

# Median object size served (needs numeric sort of bytes column)
awk '$5 ~ /^[0-9]+$/ {print $5}' /var/log/squid/access.log | sort -n | awk '{a[NR]=$1} END {print a[int(NR/2)]}'

# Clients exceeding a request-rate threshold in the last 10000 lines
tail -10000 /var/log/squid/access.log | awk '{c[$3]++} END {for (x in c) if (c[x] > 500) print c[x], x}' | sort -rn

# Which parent handled each miss
awk '$4 ~ /MISS/ {print $9}' /var/log/squid/access.log | cut -d/ -f1 | sort | uniq -c | sort -rn

# Open the cache manager menu (lists every mgr page available)
curl -s http://localhost:3128/squid-internal-mgr/menu

# Confirm the running binary and build options
squid -v | head -3

Cache tuning#

Cache effectiveness depends on object size limits, the split between memory and disk, and the replacement policy. The defaults are conservative; raise them deliberately for a caching proxy that fronts package mirrors or large static assets.

cache_mem 2048 MB                  # in-memory hot objects and in-transit data
maximum_object_size_in_memory 1 MB # objects larger than this never sit in cache_mem
maximum_object_size 4 GB           # ceiling for a single cached object on disk
minimum_object_size 0 KB
cache_replacement_policy heap LFUDA  # keep popular objects; better hit ratio than the default LRU for mixed sizes
memory_replacement_policy heap GDSF   # favour keeping many small objects in RAM
cache_swap_low 90                  # start replacement at 90% full
cache_swap_high 95                 # replace aggressively above 95%

heap LFUDA (Least Frequently Used with Dynamic Ageing) maximises byte hit ratio by keeping popular objects regardless of size; heap GDSF (Greedy Dual Size Frequency) maximises object hit ratio by preferring small objects. Use LFUDA on disk where bandwidth saving matters and GDSF in memory where request-count hits matter. Both need Squid built with --enable-removal-policies, which every distribution package enables.

For a large disk cache prefer rock storage or aufs over the classic ufs. ufs performs cache I/O on the main thread and stalls it; aufs uses threads, and rock packs small objects into a database file with far less filesystem overhead.

cache_dir rock /var/spool/squid/rock 20000 max-size=32768   # small objects, one file, low overhead
cache_dir aufs /var/spool/squid/aufs 40000 16 256 min-size=32769   # larger objects, threaded I/O

Run squid -z after adding or resizing any cache_dir, with Squid stopped, or it will not create the directory structure and logs Failed to verify one of the swap directories.

Further reading#