Working Behind Zscaler · Part 1
The Machine in the Middle
Here is the fact the rest of this series hangs on, stated as baldly as it deserves: your corporate laptop trusts a man-in-the-middle on purpose. Not through a bug, not through an attack — somebody installed that trust deliberately, with your employer’s blessing, and it is working exactly as designed. The browser is the proof. Every HTTPS site loads, padlock and all, no warnings, no friction. Then you run npm install and it throws a certificate error on the very first package — same machine, same network, same second the browser was fine. Both of those things are true at once, and the contradiction is the whole subject. One of them is lying to you about what “works” means, and the surprising part, the part this post is here to fix, is that the browser is the liar. The tool that looks broken is the only one telling you the truth about what’s on the wire.
So start with what trust means to a machine, because the whole mess unpacks from there. When you connect to a site over HTTPS, the site hands your laptop a certificate — a signed document that amounts to I am registry.npmjs.org, and here is the proof. Your laptop doesn’t take that on faith. It checks who signed the certificate, and follows the signatures upward — this cert signed by that one, that one signed by another — until the chain reaches a root certificate authority: one of a small set of blue-chip organisations whose signatures the whole internet has agreed to treat as final. But “the whole internet has agreed” has to live somewhere concrete on your actual computer, and it does. It’s a file — or a system database — called the trust store: the explicit list of which root CAs this machine is willing to believe. A certificate checks out if, and only if, its chain ends at a root already sitting in that store. No matching root in the store, no trust, no connection. Hold onto that list, because everything in this series turns on it: the trust store is your machine’s private, editable answer to the single question whose signatures count?
Now the trap springs, and it springs on the word editable. Your employer doesn’t want traffic it can’t see leaving the building, so it routes every connection through Zscaler — and Zscaler does something that would be a felony coming from anyone else. It terminates your HTTPS. When you reach for registry.npmjs.org you don’t receive npm’s real certificate; you receive one Zscaler minted on the spot, claiming to be registry.npmjs.org, signed by a Zscaler root CA. Zscaler decrypts what you sent, reads it, scans it, then re-encrypts it down a second tunnel it’s holding open to the real npm, and passes your bytes along. That is, mechanically and without exaggeration, a textbook man-in-the-middle: two separate encrypted tunnels with an inspector sitting in the seam reading everything in the clear. The industry’s polite name for it is TLS inspection (or SSL inspection — same thing). The only reason your laptop doesn’t scream bloody murder is the detail I told you to hold onto: that Zscaler root was slipped into your trust store ahead of time, pushed to the machine by IT through the same MDM/Intune channel that installed your VPN and your antivirus, back before you ever opened the lid. The forged certificate chains up to a root your computer was pre-instructed to believe, so it checks out, so the padlock closes and the browser smiles. That is the sentence I opened with, no longer a metaphor: your laptop trusts a man-in-the-middle because someone with admin rights added the middleman to the guest list, and your machine is only being loyal to the list.
So why does the browser sail through while npm install face-plants, if they’re on the same machine with the same Zscaler root in the same store? Because they are not reading the same store. Here is the part nobody tells you, and the hinge the whole series swings on: the browser reads your operating system’s trust store — the one IT pushed the root into — and almost none of your development tools do. Node ships its own list of root CAs, baked into the binary. Python’s requests ships its own, in a bundle called certifi. Git, Docker, the JVM, Go, the .NET runtime on Linux — each carries a private, hard-coded idea of whose signatures count, compiled in at build time by people who had never heard of your employer or its proxy. To the browser, Zscaler’s forged certificate is signed by a trusted root and all is well. To npm, that same certificate is signed by a stranger — a root that appears nowhere in Node’s little bundled list — and it does the correct, principled thing a security tool should do when a stranger signs the mail: it refuses. SELF_SIGNED_CERT_IN_CHAIN. Same cert, same second, opposite verdict, and the only difference is which list each program thought to consult. That is the whole series compressed to a sentence: every tool ships its own idea of who to trust, and your corporate laptop has exactly one blessed trust store — the one the browser reads — that almost none of them bother with.
And this is not a museum problem, the kind crusty old tools have and shiny new ones outgrew. The first time it drew blood for me was npm install, in 2017. The most recent was a few months ago, installing GitHub Copilot into JetBrains Rider — an AI tool, the newest thing in my stack — which simply, silently refused to work behind the proxy. Seven years apart, an entire turn of the industry in between, and underneath it the identical handshake failing in the identical place, because nobody ever handed either tool the memo that the Zscaler root is legitimate. Strip the magic off Copilot-in-Rider and it is a Node process wearing an AI costume: a background agent making https.get() calls, reading Node’s bundled CA list, tripping over the very same Zscaler root that stopped npm in 2017. The AI didn’t change the problem; it just gave it a more embarrassing place to show up. The tools keep changing. The memo keeps not arriving.
It’s worth stepping off the wire for a moment to say plainly what this thing in the middle actually is, because most people meet Zscaler as “the VPN thing IT made me install,” and that label is precisely backwards. A VPN extends your company’s network out to your laptop: once you connect, you’re inside, on the trusted side of the moat, and the network largely waves you through on the strength of where you’re sitting. Zscaler is the opposite shape. It doesn’t put you inside anything — it plants itself permanently in the path between you and the whole internet, a cloud forward proxy (the vendor’s term is Secure Web Gateway) that every request has to pass through and be inspected by, every time, trusting nothing on the basis of where you happen to be. That’s what “zero trust” actually means underneath the marketing: not you’re on the network, so you’re fine, but prove yourself per request, and I’ll be reading the mail regardless. And — to be fair to it, because this is not a screed — that inspection buys the company real things: it catches malware in downloads, it stops someone paste-uploading the customer database (data-loss prevention), it lets auditors attest that traffic is watched. Zscaler is doing precisely the job it was installed to do.
Which is exactly why the reflex, when it breaks, is the wrong one. Ask me in the moment what’s wrong and I’d have said “it’s Zscaler” — I’ve said it, out loud, at my own desk. Everyone does; the proxy has a name, the name is right there on the IT ticket, so the name takes the blame. But that reflex is the misdirection this whole series exists to correct. The proxy isn’t malfunctioning — it’s inspecting your traffic on purpose, the one thing it was bought to do. IT isn’t malfunctioning either; they even pushed the root into your OS store so the browser would be content. The only party never told the inspection was legitimate is the heap of tools on your side of the glass, each reading its own private list that nobody thought to update. “It’s Zscaler” is where you start. “It’s that nobody told my tools” is where you have to end up — because only one of those two sentences points at something you can actually go and fix.
So go and look. The first move — the one that turns a two-hour panic into a ten-second diagnosis — is to stop theorising and put your eyes on the certificate itself. You don’t need Zscaler’s console or a favour from IT; the tool that’s lying to you will show you the lie if you ask it directly. One command, anywhere openssl exists:
openssl s_client -connect registry.npmjs.org:443 -showcerts </dev/null
Read the chain it prints back, and specifically the issuer — the i: line, the who signed this — of each certificate. On an unmanaged machine that chain terminates in a public authority: run it against npmjs.org from a home connection today and the root is Google Trust Services, npm’s actual CA. On your corporate laptop the same command against the same host will instead hand you a chain that terminates in something with Zscaler in its name. There’s the whole mystery in plain text: the certificate your machine was handed for registry.npmjs.org was minted by your employer’s proxy, not by whoever npm actually pays. Every cert error in this series, every NODE_EXTRA_CA_CERTS in the next post, every failed Docker build in the one after — all of it is downstream of that single line of output. Don’t memorise which CA is “right”; the right one drifts, and a post that hardcodes DigiCert ages badly the day npm moves to Google. Memorise the move instead: look at the cert first, and see whose name is on it.
So that’s the machine in the middle, and the model you needed before any fix could make sense: a proxy reading your HTTPS with your own machine’s blessing; a forged certificate that delights the browser and offends every tool that consults its own trust store instead of the operating system’s; and a reflex — look at the cert first — that collapses a whole confusing genre of failure into one recognisable shape. What the model pointedly does not do is fix anything. Knowing why npm refuses doesn’t put the Zscaler root into Node’s bundle, or Python’s certifi, or Git’s, or the JVM’s — and each of them insists on being told in its own dialect, which is the unglamorous and genuinely useful labour of the next post. There’s a shortcut waiting for you there, too: a single environment variable that switches the checking off and makes the error vanish on the spot. We are going to walk straight past it, because taking the lock off the door to stop it rattling is not the same as owning the key. Next: teaching every tool on the machine to read the memo — one repeating pattern, worked across the whole toolchain.
This series has no talk or book under it the way the last one did — it’s the memo I never got, reconstructed the slow way across a decade of the same certificate breaking the same handshake in a new tool each time. The scars are first-hand: npm install in 2017, Copilot-in-Rider in 2026. The machinery underneath — TLS inspection, trust stores, a corporate root your OS believes on purpose — is the ordinary, well-documented plumbing of any Secure Web Gateway, Zscaler’s or anyone else’s; nothing here is specific to one employer’s tenant, and every command was run against public hosts.
◐│◑ Aranmula Kaṇṇāṭi — the front-surface metal mirror of Kerala, cast from a family-secret copper-and-tin alloy and polished for days so the image forms on the metal’s own face, with no glass between you and the reflection.