LINUXOR.SK ... open source notes ...

Proxy 04 - Squid with SSL interception

category: solutionz/proxy · date: 2019-12-31 · updated: 2026-10-03 · author: LALA

Proxy Solution · Previous: firewalld rich rules · Next: Certificate renewal

The firewall decides who may talk to port 3128; Squid decides what happens to the connection after that. This part is the proxy itself: the default RHEL 7 configuration that was kept, the listening port with its ssl-bump options, the certificate cache, the four SSL-bump actions and their order, the two exclusion lists, and the little that is known of the production configuration. The Squid part of my notes is the lab build; of the two production proxies only the port lines survive.

What Squid does here

Squid 3.5 (the RHEL 7.5 package, squid-3.5.20-12.el7; the notes print no version) runs as a forward proxy: a server in the client ranges sets http_proxy and https_proxy to the proxy's internal address and port 3128, sends plain HTTP requests to it and, for HTTPS, a CONNECT request for a tunnel to the origin server on port 443. Without interception the proxy sees only the name and port in that CONNECT. With ssl-bump it takes the TLS handshake apart, decides per connection whether to leave it alone or to step into it, and when it steps in it presents the client a certificate for the origin server that it generated and signed with its own CA certificate. The whole configuration is a Config document: squid.conf of the lab. Why interception was wanted is in Overview and design; the notes do not say.

The default configuration, kept

The squid package of RHEL 7.5 installs a squid.conf that already does most of what a forward proxy for internal servers needs, and the lab kept it whole. The localnet ACL lists the three RFC 1918 ranges, the same ranges the firewalld rich rules of firewalld rich rules accept port 3128 from, plus the IPv6 unique-local and link-local ranges. Safe_ports lists the ports a plain request may ask for, SSL_ports holds only 443, and CONNECT matches the tunnel method. The http_access lines are read in order and the first match decides.

OrderLineEffect
1http_access deny !Safe_portsNo request to a port outside the safe list
2http_access deny CONNECT !SSL_portsTunnels to port 443 only
3http_access allow localhost managerThe cache manager from the proxy itself
4http_access deny managerand from nowhere else
5http_access allow localnetEverything from the client ranges
6http_access allow localhostand from the proxy itself
7http_access deny allNothing else

The default http_port 3128 is commented out and the disk cache, cache_dir, stays commented out as the package ships it; the notes hold no cache or access log settings, so I cannot say what the production proxies logged. One thing I did not see in 2018: localnet has fc00::/7 and fe80::/10 but not the organisation's 2001:db8::/32, which the production firewall accepts on port 3128. If the production file kept this default, an IPv6 client got through the firewall and was refused by line 7. The production file is not in the notes, so this is a question, not a finding.

The listening port

Below the commented default the lab added one line, marked # LOCAL -> like every addition.

ini
# LOCAL -> Squid listen Port with enable SSL-bump
http_port 10.90.114.114:3128 ssl-bump generate-host-certificates=on dynamic_cert_mem_cache_size=4MB key=/etc/pki/tls/private/proxy.lab.example.net.key cert=/etc/pki/tls/certs/proxy.lab.example.net.crt
OptionWhat it does
10.90.114.114:3128Listen on the internal address only; the external interface has no listener
ssl-bumpTurn SSL interception on for this port
generate-host-certificates=onGenerate a certificate for each origin server that is bumped, instead of serving the proxy's own certificate for every site
dynamic_cert_mem_cache_size=4MBThe in-memory cache of generated certificates
key=, cert=The lab's private key and self-signed certificate, which sign the generated certificates and thereby act as a CA

The meaning of the options is my reading of the Squid 3.5 documentation; the notes hold the line. The certificate, a 2048-bit RSA key and a self-signed certificate for proxy.lab.example.net valid 3652 days, was made with openssl before Squid was installed: CA certificate and Squid installation in the lab.

The certificate cache

Generated certificates are made by a helper, ssl_crtd, that Squid starts and that keeps its database on disk. The database has to exist before Squid starts, so after yum install squid I created it and gave it to the squid user, as root on the lab host.

bash
$ /usr/lib64/squid/ssl_crtd -c -s /var/lib/ssl_db
$ chown -R squid:squid /var/lib/ssl_db/*

The chown has a glob: it changes the owner of everything inside /var/lib/ssl_db and leaves the directory itself to root; that the directory stayed root's is my inference, and nothing in the notes shows it mattered. The lab note records no successful request through Squid: the pip test fails, the export that follows is written down as the solution, and no second run is kept, so whether it worked is implied by the step title and nothing else. The same two commands close every renewal, because the cache is deleted when the CA changes: Certificate renewal has the why. The lab squid.conf sets no sslcrtd_program, so it names neither the helper nor the path; the error message on dc2-a-vcprx001, when SELinux stopped the helper, names /var/lib/ssl_db, the path the cache was created at, and the production squid.conf that may or may not have set it is not in the notes. That is described in SELinux and client trust.

Peek, splice, stare and bump

The SSL-bump block is six rules and three step ACLs.

ini
# LOCAL -> SSL-bump configuration
acl step1 at_step SslBump1
acl step2 at_step SslBump2
acl step3 at_step SslBump3

acl ssl_exclude_domains ssl::server_name "/etc/squid/ssl_exclude_domains.conf"
acl ssl_exclude_ips     dst              "/etc/squid/ssl_exclude_ips.conf"

ssl_bump splice localhost
ssl_bump peek step1 all
ssl_bump splice ssl_exclude_domains
ssl_bump splice ssl_exclude_ips
ssl_bump stare step2 all
ssl_bump bump all

What follows is general knowledge from the Squid wiki page SslPeekAndSplice, my reference at the time; the notes hold the rules and no explanation. Squid evaluates the ssl_bump rules once per step, up to three times per connection, and the first rule that matches at a step decides. At step 1 Squid has only the CONNECT request: destination and port. At step 2 it has read the client's TLS handshake and knows the server name the client sent (SNI). At step 3 it has also talked to the origin server and holds its certificate. Four actions are used. splice makes a plain tunnel: nothing is decrypted, the client sees the origin's own certificate. bump steps in: the client gets a generated certificate, Squid opens its own TLS session to the origin. peek and stare are the two ways to look before deciding. peek at step 1, as here, reads the client's hello only and keeps both splice and bump open for step 2; peeking again at step 2 would forward the hello to the origin unchanged and, as the wiki says, usually rule bump out. stare at step 2, which is what this file does, opens Squid's own session to the origin to read its certificate, which keeps bump possible and rules splice out.

mermaid
flowchart TB
  c["Client sends CONNECT to port 3128"]
  lh{"Client is localhost?"}
  s0["splice, plain tunnel"]
  p1["Step 1, peek: read the client handshake"]
  ex{"Server name in ssl_exclude_domains or destination in ssl_exclude_ips?"}
  s1["Step 2, splice: plain tunnel, nothing decrypted"]
  st["Step 2, stare: fetch the origin server certificate"]
  b["Step 3, bump: generated certificate to the client"]
  c --> lh
  lh -- "yes" --> s0
  lh -- "no" --> p1
  p1 --> ex
  ex -- "yes" --> s1
  ex -- "no" --> st
  st --> b

That is why the order is what it is. Peek comes before splice because at step 1 the server name is not known yet: a CONNECT may carry a bare address, and the name the exclusion list is matched against is in the client's handshake, which peek reads without committing. Stare comes before bump because a bump at step 2 would have to invent the certificate from the client's side alone; after stare Squid holds the origin's real certificate at step 3 and generates one that mimics it. splice localhost stands first so that connections from the proxy itself are never bumped. The step3 ACL is defined and used by no rule; bump all matches at step 3 without it. It is a harmless leftover.

The exclusion lists

Two files, one entry per line, decide what is left alone: server names in ssl_exclude_domains.conf, matched by ssl::server_name, and destination addresses in ssl_exclude_ips.conf, matched by dst. The lab files hold .example.com, .example.org and 8.8.8.8; they look like example entries put there to test the mechanism, and the notes do not say otherwise. What the production proxies excluded is not in the notes. Below the rules one more line was left commented out, #sslproxy_cafile /etc/pki/tls/certs/ca-bundle.crt, the bundle Squid would verify origin certificates against when it bumps.

The certificate chain

Interception only works if the client trusts what the proxy signs. The chain has three links: the proxy's self-signed certificate, which is the CA; the host certificates ssl_crtd generates and signs with it; and the client's trust store, which must hold the CA. In the lab the CA went into the system anchors with cp and update-ca-trust, and pip still needed REQUESTS_CA_BUNDLE; in production the certificate goes to the colleague who runs the certificate deployment, and how it reaches the servers is not in the notes.

mermaid
flowchart LR
  subgraph proxy["proxy.lab.example.net"]
    key["proxy.lab.example.net.key, RSA 2048"]
    ca["proxy.lab.example.net.crt, self-signed, 3652 days"]
    crtd["ssl_crtd, cache /var/lib/ssl_db"]
    host["generated certificate for the origin server"]
  end
  subgraph client["the client's trust store"]
    anchors["/etc/pki/ca-trust/source/anchors/"]
    bundle["/etc/pki/tls/certs/ca-bundle.crt"]
    pip["pip with REQUESTS_CA_BUNDLE"]
  end
  key --> ca
  ca -- "signs" --> host
  crtd --> host
  host -- "presented in the bumped session" --> pip
  ca -- "cp and update-ca-trust" --> anchors
  anchors --> bundle
  bundle --> pip

The left half is in CA certificate and Squid installation in the lab, the right half in SELinux and client trust.

The production port lines

Of the production squid.conf my notes hold two lines per host, printed by grep ssl-bump at the start of each certificate renewal: Listening ports of dc1-a-vcprx001 and Listening ports of dc2-a-vcprx001. Against the lab line they differ in these ways.

LabProductionRemark
http_port 10.90.114.114:3128http_port 3128No address: the production listener binds every address of the host
/etc/pki/tls/private/…key, /etc/pki/tls/certs/…crt/etc/ssl/squid/….private_1, ….crt_1 (datacenter 1); /etc/ssl/squid/…_2.key, …_2.crt (datacenter 2)Own directory, two naming schemes
dynamic_cert_mem_cache_size=4MBnot presentThe default applies
not presentversion=1The TLS version selector of Squid 3.5, 1 being automatic; the notes do not say why it was written out
not presentoptions=NO_SSLv2,NO_SSLv3,SINGLE_DH_USEOpenSSL options; the notes do not say why these
not presenta second line, https_port 3128 … ssl-bump intercept …A TLS listener for NAT-redirected connections, on the same port

The meaning of version=1 and of the options= values is my reading of the Squid 3.5 documentation, as for the lab table. The second line is the odd one. intercept is for connections a firewall redirects to the proxy without the client knowing, and nothing in the firewalld configuration of either host redirects anything. Both lines are printed without a comment sign, so as printed both were active. That two listeners on the same port and the same addresses cannot both bind is my understanding of socket binding, not something the notes or a Squid document state; how Squid ran with both lines the notes do not say. Everything else of the production file is unknown to me today.

Today

Checked against Squid 7.7 in October 2026, four things change this picture most. The certificate generator has been security_file_certgen since Squid 4, its default database lives under the cache directory, and it refuses -s without -M: the cache is created with security_file_certgen -c -s <dir> -M 4MB. version= is gone, replaced by tls-min-version=, NO_SSLv2 is an error, and every sslproxy_* directive went with them: tls_outgoing_options uses the system CAs by default, so the commented sslproxy_cafile would be deleted rather than uncommented. A fresh Squid 6 or later, which RHEL 10 ships as 6.10, no longer allows localnet by default; the line has to be switched on. And Squid 7.7 documents that the signing certificate must be a CA certificate able to sign, which a plain openssl x509 -req -signkey certificate like the lab's, made without CA extensions, is not; the Squid wiki's one-step openssl req -x509 -extensions v3_ca recipe is the documented way. The SSL-bump rules themselves are unchanged; the details are in the "Checked against" sections of the Config documents.

What I would do differently

Three things the notes themselves support. Remove the unused step3 ACL, or use it: ssl_bump bump step3 all says what the last rule means. Write chown -R squid:squid /var/lib/ssl_db without the glob, so the directory changes owner too. And keep a copy of the production squid.conf with the notes, so that its port lines would not have to be explained from a grep seven years later.

← solutionz/proxy