Safeguard
Application Security

Implementing SSL/TLS certificate pinning in Node.js

HTTP Public Key Pinning died in Chrome 67 back in 2018, yet Node.js apps still need pinning for mobile backends and server-to-server calls — here's how to do it without bricking your own API.

Safeguard Research Team
Research
6 min read

Node.js has no built-in pin option anywhere in its TLS API — pinning is something you build yourself on top of options.checkServerIdentity(), a callback documented in the core tls module that fires only after a certificate has already passed normal chain and hostname validation. That single hook is how every Node pinning implementation works, whether it's a mobile app's backend verifying a client-issued cert or a service mesh enforcing which exact key a downstream API is allowed to present. The stakes for getting it wrong are well established: HTTP Public Key Pinning, the browser-native version of this idea standardized in RFC 7469, was deprecated in Chrome 67 in May 2018 and stripped out of Chrome 72 and Firefox 72 shortly after, largely because site operators pinned themselves into outages they couldn't recover from. That history didn't kill the need for pinning — it just moved it out of the browser and into application code, where mobile SDKs and custom Node HTTP clients now carry the responsibility that HPKP headers used to. This guide covers the two use cases where Node pinning still earns its keep — mobile-backend APIs and server-to-server calls — plus the operational mistakes that turn a security control into a self-inflicted denial of service.

What is the actual Node.js API for certificate pinning?

The actual API is checkServerIdentity, an option you pass to tls.connect() or https.request() that receives the hostname and the peer's certificate object after Node's default verification already ran. Node's own documentation demonstrates the canonical pattern: hash the certificate's public key with SHA-256 and compare it against a hardcoded value, returning an Error from the callback if it doesn't match (returning undefined accepts the connection). A minimal version looks like this — const tls = require('tls'); const crypto = require('crypto'); const PINNED_SHA256 = 'AB:CD:...'; https.request({ hostname, checkServerIdentity: (host, cert) => { const fingerprint = crypto.createHash('sha256').update(cert.pubkey).digest('hex'); if (fingerprint !== PINNED_SHA256) return new Error('Certificate pin mismatch'); } }) — note that cert.pubkey gives you the raw public key buffer, which is what you should be hashing rather than the whole DER-encoded certificate, for reasons covered below. This callback approach works identically for outbound HTTPS clients and for tls.Socket connections used in custom server-to-server protocols.

Should you pin the leaf certificate or the public key?

You should pin the public key (SPKI), not the leaf certificate, because leaf pinning breaks on every routine certificate renewal while key pinning survives it. A leaf-certificate pin is a hash of the entire certificate — issuer, serial number, validity dates and all — so the pin goes stale the moment the certificate authority reissues a fresh cert with the same key pair, which happens on a normal 90-day Let's Encrypt or annual CA renewal cycle. Pinning the Subject Public Key Info instead means the hash only changes when the underlying key pair changes, which is a much rarer, deliberate event. This is the same distinction OWASP's Certificate and Public Key Pinning guidance draws for mobile apps: pin the SPKI hash of the leaf or, more resiliently, of an intermediate CA key your provider is contractually likely to keep reusing across reissuance. Pinning an intermediate is the more common choice for mobile-backend pinning specifically, because it tolerates the backend team rotating leaf certs without requiring an app-store release to update the pin.

How does pinning work for a mobile-backend API versus a server-to-server call?

Pinning for a mobile backend and pinning for server-to-server traffic solve different threat models even though they use the same Node API. For a mobile backend, the pin lives in the mobile app binary (iOS/Android), and the Node.js server side of the equation is usually just an ordinary TLS server — the pinning logic runs client-side, checking the server's cert against a value baked into the app to defeat on-path attackers with a rogue CA, including corporate MITM proxies. For server-to-server calls, the Node service is the pinning client: a Node process calling a partner API or an internal microservice sets checkServerIdentity to enforce that the peer presents a specific key, which is useful when you don't fully trust the ambient CA trust store the host inherited, or when you want defense-in-depth against a compromised or coerced CA. In both cases the actual comparison logic in Node is identical — the difference is only which side of the connection is doing the pinning and why.

What pitfalls turn certificate pinning into a self-inflicted outage?

The most common pitfall is pinning a single key with no backup, so that a routine or emergency certificate rotation locks every pinned client out simultaneously — this is precisely the failure mode that made HPKP too dangerous for browsers to keep shipping. The standard mitigation, carried over from HPKP-era guidance and still recommended in OWASP's pinning cheat sheet, is to pin at least two keys: the currently active one and a backup key generated and stored offline, so you can rotate to the backup and reissue certs without an app update or emergency patch. A second pitfall is pinning the leaf certificate instead of the SPKI hash, discussed above, which turns every routine renewal into an incident. A third is treating pinning as sufficient on its own: on rooted or jailbroken mobile devices, tools like Frida-based SSL-unpinning scripts can hook into the app process and neutralize client-side pinning checks entirely, a widely documented limitation rather than a specific CVE — pinning raises the bar against passive and network-level attackers, but it is not a substitute for server-side authentication and rate limiting.

Is certificate pinning still worth implementing in 2026?

Yes, for the two narrow cases this guide covers, even though the general trend across the industry has been away from broad, browser-level pinning. HPKP's removal from every major browser by 2020 reflected a specific judgment that forcing pin distribution through HTTP response headers, with no safe rollback path, was too operationally risky for the web at large. That judgment doesn't apply to a mobile app team that controls both ends of a connection and can ship app updates, or to a platform team pinning a small number of known internal service identities for server-to-server calls — both are bounded, deliberately-managed trust relationships rather than an open-ended promise to every visitor on the public web. The practical takeaway is to treat pinning as a targeted control for high-value, narrow-scope connections, back every pin with a second offline key, pin SPKI rather than leaf certificates, and keep a documented rotation runbook — the same operational discipline HPKP lacked is exactly what makes pinning safe to run today.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.