Squid SSL-Bump
splice. In many jurisdictions you must tell users, allow opt-out, and keep a hard no-bump list for banking, healthcare and government. If any of the traffic crosses a border, have your legal team sign off on the deployment before you flip ssl_bump from peek to bump.
CONNECT (when all you have is the SNI) and after peek (when you have the server's certificate). Decide at the earliest phase whether to splice or bump. Any site the business cannot afford to break goes in a no_bump list evaluated at the first phase.
What CONNECT does
When a browser configured to use a proxy requests an https:// URL, it does not perform the TLS handshake directly. It sends the proxy an HTTP CONNECT verb:
CONNECT www.example.com:443 HTTP/1.1
Host: www.example.com:443
User-Agent: Mozilla/5.0 ...
A "dumb" proxy (no SSL-Bump) replies 200 Connection established and blindly forwards the subsequent TLS bytes between client and server. It has no visibility into URL, method, or body.
SSL-Bump intercepts that CONNECT. Squid decides — based on its ACLs and the SNI field in the client hello — whether to:
- act as a dumb tunnel (splice), or
- become a TLS server to the client while it's also a TLS client to the origin (bump), re-encrypting in the middle.
ssl_bump modes
| Mode | What Squid does | Typical use |
|---|---|---|
splice |
Transparent passthrough — no decrypt. Squid sees only SNI. | Default for banks, government, personal email. The safe choice. |
peek |
Read the client hello (get the SNI), optionally read the server hello (get cert CN/SAN). Does not decrypt the application traffic unless a later step bumps. | Diagnostic step before a bump/splice decision. |
stare |
Like peek but takes control of the TLS handshake with the server. Cannot be followed by splice — you must then bump or terminate. |
Rarely needed. Use if you need full server cert validation choices upstream. |
bump |
Full MITM: Squid terminates TLS from the client with a cert it signed, and opens its own TLS to the server. | DLP, URL filtering by path, content scanning. |
terminate |
Close the connection. | Deny policy (e.g. "no HTTPS to category X"). |
stare forecloses the option to splice, which is rarely what you want.
Decision flow
There are up to four steps in a single SSL-Bump handshake. You can attach ACLs to each with ssl_bump <action> [acl] [at_step N]:
- Step 1 — Client hello seen. You know the SNI, client IP, user (via authenticated proxy), TCP outbound interface. Best chance to splice for
no_bumpdomains. - Step 2 — Server hello seen. You know the origin's cert chain, SAN, issuer. Useful for trust-level-based policy ("don't bump if cert is EV").
- Step 3 — Client certificate processed. Rare in practice.
- Step 4 — Server certificate processed. Last chance to change your mind before the app-data phase.
# Conceptual flow in squid.conf
acl step1 at_step SslBump1
acl step2 at_step SslBump2
acl step3 at_step SslBump3
acl no_bump ssl::server_name_regex "/etc/squid/no_bump.regex"
acl bump_all src 10.0.0.0/8
ssl_bump peek step1
ssl_bump splice step2 no_bump
ssl_bump bump step2 bump_all
ssl_bump terminate step2 all
Read it aloud: "At step 1, peek. At step 2, if this SNI matches no_bump, splice. Otherwise, if the client is on our network, bump. Otherwise terminate."
Generating the Squid CA
mkdir -p /etc/squid/ssl
cd /etc/squid/ssl
# 10-year CA — acceptable internally; externally signed CAs are not allowed to
# issue unconstrained intermediates, so you must self-sign.
openssl req -new -newkey rsa:4096 -days 3650 -nodes -x509 \
-keyout squidCA.key -out squidCA.crt \
-subj "/C=US/ST=NY/O=Example Co/OU=ITSec/CN=Example Squid SSL-Bump CA"
# Combine for Squid's cert generator:
cat squidCA.crt squidCA.key > squidCA.pem
chown -R squid:squid /etc/squid/ssl
chmod 600 squidCA.key squidCA.pem
# Prime the ssl_db directory for sslcrtd_program:
/usr/lib64/squid/security_file_certgen -c -s /var/lib/squid/ssl_db -M 16MB
chown -R squid:squid /var/lib/squid/ssl_db
Squid's on-the-fly cert generator (security_file_certgen on modern Squid, formerly ssl_crtd) caches the certs it synthesizes for each origin so it doesn't resign from scratch on every CONNECT. 16MB is enough for thousands of cached origin certs.
Distributing trust to clients
Every client that will traverse the proxy must trust squidCA.crt. Otherwise every bumped connection is a certificate-error nightmare.
| Platform | How |
|---|---|
| Windows (domain) | GPO → Computer Configuration → Windows Settings → Security Settings → Public Key Policies → Trusted Root Certification Authorities → Import. |
| Windows (standalone) | certutil -addstore -enterprise -f Root squidCA.crt |
| macOS | security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain squidCA.crt |
| RHEL-family Linux | cp squidCA.crt /etc/pki/ca-trust/source/anchors/ && update-ca-trust |
| Debian/Ubuntu | cp squidCA.crt /usr/local/share/ca-certificates/squidCA.crt && update-ca-certificates |
| Firefox (separate trust store) | Either security.enterprise_roots.enabled = true in about:config (Firefox reads the OS store), or import per-profile. |
squidCA.key to endpoints. The private key stays on the Squid host only. If it ever leaks, every TLS session ever bumped with it is retroactively readable by whoever has the key — generate a new CA, re-deploy, revoke the old one.
Never-bump lists
Regulatory and user-trust reasons to always splice certain destinations:
- Banking, tax, healthcare portals.
- OS update servers (cert pinning breaks Windows Update, macOS Software Update).
- Enterprise apps that use mutual TLS — Squid cannot present the client cert.
- Anything using HPKP / Certificate Transparency enforcement.
# /etc/squid/no_bump.regex — regex match against SNI
\.bank\.(com|co\.uk)$
^.*\.chase\.com$
^.*\.irs\.gov$
^.*\.login\.gov$
^.*\.nhs\.uk$
^.*\.icloud\.com$
^.*\.apple\.com$
^.*\.microsoft\.com$
^.*\.windowsupdate\.com$
^.*\.mozilla\.org$
Regex match is expensive per-connection — if the list grows past a couple hundred entries, switch to domain match with acl no_bump ssl::server_name "/etc/squid/no_bump.dstdomain" and a literal-FQDN list.
Performance impact
Bumping doubles TLS handshake CPU and adds a per-origin cert-generation step on the first hit. Rough rules of thumb on modern hardware:
- Splice: negligible overhead above a plain HTTP proxy.
- Bump, cached cert: +30-50% CPU vs splice, latency essentially unchanged.
- Bump, first-time cert synthesis: +100-150 ms first connection to a new origin.
- Bump of modern TLS 1.3 with ECDSA certs: about half the overhead of RSA bumping. If you can, generate an ECDSA CA.
Sizing: a single Squid host with 4 cores and 8 GB RAM handles ~500 concurrent authenticated users with full bump if you keep the cert DB warm and block HTTP/3 (QUIC) at the firewall so traffic falls back to TCP.
Minimal working squid.conf
http_port 3128 ssl-bump \
cert=/etc/squid/ssl/squidCA.pem \
generate-host-certificates=on dynamic_cert_mem_cache_size=16MB \
tls-dh=prime256v1:/etc/squid/ssl/dhparam.pem
sslcrtd_program /usr/lib64/squid/security_file_certgen -s /var/lib/squid/ssl_db -M 16MB
sslcrtd_children 8 startup=1 idle=1
acl localnet src 10.0.0.0/8
acl Safe_ports port 80 443
acl CONNECT method CONNECT
acl step1 at_step SslBump1
acl step2 at_step SslBump2
acl no_bump ssl::server_name_regex "/etc/squid/no_bump.regex"
acl bump_users src 10.0.0.0/8
# Peek first to get SNI; then splice or bump.
ssl_bump peek step1
ssl_bump splice step2 no_bump
ssl_bump bump step2 bump_users
ssl_bump terminate all
http_access deny !Safe_ports
http_access deny CONNECT !Safe_ports
http_access allow localnet
http_access allow localhost
http_access deny all
# Logging — drop from access.log the SNI when we spliced, for privacy.
logformat squidmime %ts.%03tu %6tr %>a %Ss/%03>Hs %<st %rm %ru %[un %Sh/%<a %mt
access_log /var/log/squid/access.log squidmime
cache_mem 512 MB
maximum_object_size_in_memory 64 KB
coredump_dir /var/spool/squid
Generate the DH params once:
openssl dhparam -out /etc/squid/ssl/dhparam.pem 2048
chown squid:squid /etc/squid/ssl/dhparam.pem
Start:
squid -k parse # syntax check
systemctl enable --now squid
journalctl -u squid -f
Testing with curl
Without Squid's CA in the local trust store, a bumped fetch fails cert verify — that's the smoke test:
# Smoke test: should fail verify because curl doesn't trust squidCA yet
curl -v -x http://proxy.example.com:3128 https://httpbin.org/get
# -> curl: (60) SSL certificate problem: unable to get local issuer certificate
# With the CA explicitly trusted:
curl -v --cacert /etc/squid/ssl/squidCA.crt \
-x http://proxy.example.com:3128 https://httpbin.org/get
# -> 200, body returned
# Confirm splice path — a no_bump destination returns the real origin cert:
curl -v --cacert /etc/ssl/certs/ca-bundle.crt \
-x http://proxy.example.com:3128 https://www.chase.com/ -o /dev/null
# Squid access.log should show "splice" in the result code column.
Troubleshooting
| Symptom / log | Root cause | Fix |
|---|---|---|
SECURITY ALERT: Host header forgery detected |
Client connects to an IP that doesn't match the SNI DNS resolution Squid does for itself. | Ensure the Squid host has working DNS. Add host_verify_strict off only as a last resort — it weakens a useful check. |
| All bumped sites show browser "Your connection is not private" | Squid CA not trusted on the client. | Deploy squidCA.crt; see Distributing trust. |
| Banking sites break with cert-pinning error | Bump is applied to a site that must stay end-to-end. | Add to no_bump.regex, reload Squid. Verify with access.log. |
sslcrtd_program crashes / zombie children |
SSL DB directory permissions wrong, or disk full. | chown -R squid:squid /var/lib/squid/ssl_db; security_file_certgen -c -s ... to re-init. |
| Windows Update / macOS Software Update fails through the proxy | Bumped. Update endpoints validate their own chain, not yours. | Always splice: add *.windowsupdate.com, *.swscan.apple.com, *.mzstatic.com to no-bump. |
| Mobile apps refuse to work (Slack, Teams, Zoom) | Certificate pinning or mutual TLS. | Splice those domains. The vendor publishes their domain lists. |
| TLS handshake latency visibly high on first hit to a site | Cert DB cold, expected for first visit. | Raise dynamic_cert_mem_cache_size and -M on security_file_certgen. Pre-warm with a daily script hitting the top 500 destinations. |
| HTTP/3 (QUIC) traffic appears to "escape" the proxy | QUIC is UDP/443; Squid is TCP only. | Block UDP/443 outbound at the firewall to force fallback to TCP/443 via the proxy. |