TLS inspection: when to deploy it, how it works, and what it breaks
For developers and platform engineers deciding whether to put a TLS-inspecting proxy in the path. This guide explains where TLS inspection sits, how the handshake is split in practice, what errors and breakages you should expect, and the decision criteria for saying yes or no.
TL;DR — TLS inspection is a man-in-the-middle proxy you operate on purpose: it terminates the client TLS session, inspects traffic, then opens a second TLS session upstream. It is worth it when you have a hard requirement to inspect or enforce policy on encrypted egress; otherwise it usually adds latency, certificate-management pain, and breaks certificate pinning, mTLS, QUIC, and some privacy-sensitive or regulated flows. Reading time: ~7 min
What it is and where it sits
TLS inspection sits inline on the network path between a client and the real destination. In a corporate or segmented environment, that usually means one of these placements:
- outbound web proxy for developer laptops or CI runners
- egress gateway for a private subnet or Kubernetes cluster
- firewall/proxy between a restricted segment and the internet
- less commonly, inbound reverse proxy doing TLS termination plus request inspection before your app
For this article, focus on the common case: outbound TLS inspection for HTTPS egress.
Without inspection, the client negotiates TLS directly with api.example.com, validates that server's certificate chain, and sends encrypted HTTP inside that tunnel. With inspection, the client is configured to trust your organization's internal CA. The inspection proxy generates a leaf certificate for api.example.com on the fly, signed by that internal CA, terminates the client's TLS, reads the HTTP, applies policy, then creates a second TLS connection to the real api.example.com.
What it replaces in the flow:
- direct client-to-origin TLS
- or a plain TCP pass-through firewall rule that cannot see HTTP method, path, headers, or response status
What talks to it:
- browsers,
curl, package managers, SDKs, build agents, containers - sometimes only traffic explicitly configured to use a proxy via
HTTP_PROXY/HTTPS_PROXY - sometimes transparently redirected traffic, though that is harder to reason about and easier to break
View as text
[Client] ---- 1. TLS ClientHello for api.example.com ----> [TLS Inspect Proxy] [Client] <---- 2. Forged cert for api.example.com -------- [TLS Inspect Proxy] [Client] ---- 3. Encrypted HTTP over client-proxy TLS ----> [TLS Inspect Proxy] [TLS Inspect Proxy] ---- 4. New TLS to real origin -------> [Origin Server] [TLS Inspect Proxy] <---- 5. Real origin cert + response -- [Origin Server] [Client] <---- 6. Policy-allowed response ---------------- [TLS Inspect Proxy]
Architecture context matters because the operational burden lands where the proxy sits:
- if it is an explicit proxy, clients need proxy settings and usually trust-store changes
- if it is transparent, routing and SNI-based interception need to be correct, and debugging gets worse
- if HTTP/3 is in use, many deployments disable QUIC/UDP 443 so traffic falls back to TLS over TCP where inspection is possible
How it actually works
A realistic end-to-end example: a CI runner in a restricted subnet calls https://registry.npmjs.org/left-pad through an outbound TLS-inspecting proxy.
Step-by-step flow
- The CI runner is configured with a proxy:
export HTTPS_PROXY=http://proxy.corp.local:3128
export HTTP_PROXY=http://proxy.corp.local:3128
export NO_PROXY=localhost,127.0.0.1,.svc.cluster.local
- The runner also trusts the corporate root CA. On Debian/Ubuntu that is usually:
sudo cp corp-root-ca.crt /usr/local/share/ca-certificates/corp-root-ca.crt
sudo update-ca-certificates
-
npmorcurlsends an HTTPCONNECT registry.npmjs.org:443to the proxy. -
The proxy allows the destination by policy, replies
200 Connection established, and then waits for the TLS handshake from the client through that tunnel.
A curl -v shape looks like this:
$ curl -v --proxy http://proxy.corp.local:3128 https://registry.npmjs.org/
* Uses proxy env variable HTTPS_PROXY == 'http://proxy.corp.local:3128'
* Trying 10.20.30.40:3128...
* Connected to proxy.corp.local (10.20.30.40) port 3128
> CONNECT registry.npmjs.org:443 HTTP/1.1
> Host: registry.npmjs.org:443
> User-Agent: curl/8.5.0
> Proxy-Connection: Keep-Alive
< HTTP/1.1 200 Connection established
* CONNECT phase completed
* CONNECT tunnel established, response 200
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* Server certificate:
* subject: CN=registry.npmjs.org
* issuer: CN=Corp TLS Inspection Root CA
* SSL certificate verify ok.
-
The proxy sees SNI
registry.npmjs.org, generates or retrieves a cached leaf certificate for that hostname signed byCorp TLS Inspection Root CA, and presents it to the client. -
Separately, the proxy opens its own TLS connection to the real
registry.npmjs.org, validates the public certificate chain using its own trust store, negotiates ALPN, and fetches the content. -
The proxy can now enforce policy on decrypted HTTP: block methods, domains, MIME types, uploads, malware signatures, DLP patterns, or log request metadata.
-
If allowed, it re-encrypts the response on the client-facing TLS session and returns it.
What failure looks like
If the client does not trust the corporate CA, you get the classic unknown issuer error:
$ curl -I --proxy http://proxy.corp.local:3128 https://example.com
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html
If the proxy blocks the destination before TLS is established, you often see an HTTP proxy error on the CONNECT:
$ curl -v --proxy http://proxy.corp.local:3128 https://example.com
> CONNECT example.com:443 HTTP/1.1
< HTTP/1.1 403 Forbidden
< Content-Type: text/html
< X-Block-Reason: policy-deny
* CONNECT tunnel failed, response 403
curl: (56) CONNECT tunnel failed, response 403
If the app uses certificate pinning, trust-store installation is irrelevant; the app compares the cert or public key against a pinned value and rejects the forged cert:
curl: (60) SSL: public key does not match pinned public key
If the app expects mTLS end-to-end, the proxy may terminate the client side and the origin never sees the original client certificate. Some products support re-origination of mTLS with a different client cert, but that is not the same security property.
When to use it (and when not to)
Use TLS inspection because you have a specific control objective, not because "encrypted traffic is scary." In segmented environments, the strongest reasons are usually egress control, DLP, malware scanning, or regulatory logging requirements on outbound web traffic.
| Scenario | Recommendation |
|---|---|
| You must inspect outbound HTTP payloads for DLP, malware, or policy in a restricted segment | Use TLS inspection on egress, but scope it narrowly by subnet, user group, or destination class |
| You only need domain-level allow/deny for outbound traffic | Do not inspect TLS first. Start with DNS policy, SNI-aware filtering, or explicit egress allowlists |
| You run internal service-to-service traffic with mTLS | Avoid TLS inspection. It weakens or complicates the trust model and usually breaks identity propagation |
| Your apps use certificate pinning, mobile SDKs, or security-sensitive APIs | Avoid, or carve out bypass rules for those destinations |
| You need HTTP visibility for debugging a single app in dev/test | Use app-level logging, a local debugging proxy, or service-mesh telemetry before deploying network-wide inspection |
| You rely heavily on HTTP/3/QUIC | Expect to disable or bypass QUIC for inspected flows |
| You are trying to secure inbound traffic to your own app | Usually use a reverse proxy/WAF/TLS terminator you control, not generic outbound TLS inspection |
You probably don't need this if...
- your real requirement is just "only these hosts may be reached"
- you can enforce egress with firewall rules, DNS policy, or private package mirrors
- you own both ends of the connection and can add application-layer audit logs instead
- your environment is mostly service-to-service mTLS where identity is the point
- your security team cannot operate an internal CA cleanly
Trade-offs
Every benefit comes with a bill.
-
Benefit: inspect HTTP methods, paths, headers, and bodies on encrypted traffic
Cost: you become a CA operator. You must distribute a root cert to every trust store: OS, browser, Java, Node, containers, mobile devices, CI images. -
Benefit: block uploads, malware, or data exfiltration in real time
Cost: added latency and CPU. Every connection is terminated twice, often with content scanning in the middle. -
Benefit: centralized egress logging and policy enforcement
Cost: privacy and compliance risk. You are now handling decrypted user traffic, which may include credentials, tokens, personal data, or regulated content. -
Benefit: one choke point for segmented networks
Cost: a new critical dependency. Proxy outage can become internet outage for developers, builds, and workloads. -
Benefit: visibility into otherwise opaque HTTPS traffic
Cost: breakage for certificate pinning, some mTLS designs, some desktop/mobile apps, and protocols that assume end-to-end TLS semantics. -
Benefit: enforce policy without changing every app
Cost: protocol downgrade pressure. Many deployments block UDP/443 so QUIC falls back to TCP; some ALPN or HTTP/2 behaviors change through proxies.
The lock-in angle is operational rather than branding: once many clients trust your inspection CA and depend on your proxy PAC files, bypassing or replacing it is a migration project.
In practice
Example 1: verify whether a proxy is doing TLS inspection
openssl s_client -proxy proxy.corp.local:3128 -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates -fingerprint -sha256
This connects through an explicit proxy, performs the TLS handshake, and prints the presented leaf cert details. If the issuer is your internal CA instead of a public CA, inspection is happening. Gotcha: some proxies bypass inspection for allowlisted domains, so test both a known-bypassed and known-inspected host.
Typical output shape:
subject=CN = example.com
issuer=CN = Corp TLS Inspection Root CA
notBefore=Oct 30 09:10:11 2026 GMT
notAfter=Nov 29 09:10:11 2026 GMT
sha256 Fingerprint=8A:2F:...:91
Example 2: install a corporate root CA into a Debian/Ubuntu container image
⚠️ Changing a base image trust store affects every TLS client in that image. If the CA is wrong or rotated without rebuilding, package downloads and API calls will fail at runtime.
FROM debian:12-slim
COPY corp-root-ca.crt /usr/local/share/ca-certificates/corp-root-ca.crt
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& update-ca-certificates \
&& rm -rf /var/lib/apt/lists/*
This bakes the inspection CA into the image so curl, apt, and many OpenSSL-linked tools trust the proxy-generated certs. Gotcha: Java, Node, and some language runtimes may use their own trust stores or bundled CAs; the OS store alone may not fix your app.
Example 3: explicit proxy environment settings with a bypass list
export HTTP_PROXY=http://proxy.corp.local:3128
export HTTPS_PROXY=http://proxy.corp.local:3128
export NO_PROXY=localhost,127.0.0.1,::1,.internal.example.com,.svc,.cluster.local
curl -I https://example.com
curl -I https://service.internal.example.com
This sends internet-bound traffic through the proxy while bypassing internal service names. Gotcha: NO_PROXY matching rules differ across tools; some expect suffixes with a leading dot, some do not, and CIDR support is inconsistent.
A useful diagnostic pattern is comparing direct vs proxied headers:
curl -I https://example.com
curl -I --proxy http://proxy.corp.local:3128 https://example.com
If the proxy injects headers or blocks redirects, the output often diverges. Example shape for a redirect that still works:
HTTP/2 301
location: https://www.example.com/
server: envoy
Example shape when the proxy itself denies on CONNECT before any origin response exists:
HTTP/1.1 403 Forbidden
content-type: text/html
x-block-reason: category=uncategorized
Further reading
- RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 9110: HTTP Semantics
curldocumentation: SSL Certificates- OpenSSL
s_clientmanual page - The "Proxy servers and tunneling" sections of the MDN HTTP docs
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI