Fixing TLS Certificate Errors at Work

You have sorted the proxy out. Traffic is reaching it. But some developer tools still fail when they make HTTPS requests.

Different tools report certificate problems in different ways. You might see errors such as:

SSL certificate problem: self-signed certificate in certificate chain

unable to get local issuer certificate

SSL: CERTIFICATE_VERIFY_FAILED

x509: certificate signed by unknown authority

PKIX path building failed: unable to find valid certification path

You would not normally see all of these together.

They are representative errors from different places. curl, Git, npm, or pip might print one in a terminal. Gradle or Maven might report another in your IDE's build output. A CI job might use different wording again.

The messages differ because the tools use different TLS libraries and certificate stores. The underlying problem is often the same: the program received a certificate but could not establish that it was signed by a certificate authority it trusts.

This article explains what that means, why corporate networks can cause it, and why fixing the certificate for one tool does not necessarily fix every other tool on the same machine.

Why this happens

When a tool connects to a website over HTTPS, it uses a security protocol called TLS, or Transport Layer Security.

TLS encrypts the connection, but it also helps the client check that it is talking to the service it intended to reach.

Suppose npm wants to connect to:

https://registry.npmjs.org

In this example, npm is the client making the request, and registry.npmjs.org is the hostname it wants to reach.

If npm connects to registry.npmjs.org, npm is effectively asking, “How do I know that the system answering me really represents registry.npmjs.org?”

That is one of the jobs TLS certificates perform.

Before npm downloads any package data, it starts a TLS connection. The initial setup of that connection is called the TLS handshake.

During the handshake, the server presents a TLS certificate.

A TLS certificate is a signed document that helps identify the service at the other end of the connection. Among other information, it contains:

npm then checks the certificate before trusting the connection.

One of those checks is whether the certificate is valid for registry.npmjs.org.

The permitted DNS names are normally listed in a certificate field called the Subject Alternative Name, or SAN.

If registry.npmjs.org is listed, that check passes.

If npm requested registry.npmjs.org but received a certificate that was only valid for an unrelated hostname, npm would reject the connection.

The next question is who signed the certificate.

The certificate chain

So far, npm has received a certificate from the server answering for registry.npmjs.org.

The certificate says, in effect, “I am valid for registry.npmjs.org.”

But npm still needs a reason to believe it.

That is what the digital signature on the certificate is for.

The certificate has been signed by a certificate authority, usually shortened to CA. A certificate authority is an organisation or system that signs certificates so clients can verify where those certificates came from.

npm can therefore ask another question:

Who signed this certificate, and do I trust them?

For most public websites, there is more than one certificate involved.

The certificate for registry.npmjs.org is usually signed by an intermediate certificate authority. That intermediate certificate is then signed by another certificate authority higher up the chain, eventually leading to a root certificate authority.

It looks roughly like this:

Server presents:

Certificate for registry.npmjs.org
        |
        | signed by
        v
Intermediate certificate authority
        |
        | signed by
        v
Root certificate authority

The important point is that npm does not need to already know the certificate for every website it might connect to.

Instead, it starts with a smaller set of root certificate authorities that it already trusts.

But how does it know which roots to trust?

It does not work that out from the certificate chain.

A root certificate sits at the top of the chain, so there is no higher certificate that npm can use to prove that the root itself should be trusted.

Instead, trusted root certificates are placed into a trust store in advance.

A trust store is simply a collection of root certificates that an operating system or application has been told it can trust.

For example, an operating system vendor may include root certificates from established public certificate authorities as part of the operating system. Applications can then use that trust store when they verify HTTPS connections.

Some applications and runtimes use their own trust stores instead, which becomes important later in this article.

Once npm has a set of trusted roots, it can verify a website certificate by following the signatures through the chain:

The server gave me a certificate for registry.npmjs.org.

That certificate was signed by this intermediate CA.

That intermediate CA was signed by this root CA.

This root CA is already in the trust store I use.

Therefore I can trust the chain back to the certificate
for registry.npmjs.org.

Technically, npm verifies the digital signatures at each step using the public keys contained in the certificates.

The root certificate is not deciding whether to trust registry.npmjs.org.

npm is making that decision.

The root certificate is the trust anchor npm starts from when it verifies the chain.

This is what makes the system practical. Your computer does not need to know every website certificate or independently trust every intermediate certificate in advance. A relatively small set of trusted root certificates can be used to verify certificate chains for a very large number of websites.

If npm can verify the chain back to a root it trusts, and the certificate is valid for registry.npmjs.org, the certificate check succeeds.

If the chain leads to a root that npm does not trust, npm rejects the connection.

That is what errors such as these are getting at:

certificate signed by unknown authority

unable to get local issuer certificate

The wording varies between tools, but the problem is essentially:

I received a certificate for the service I am connecting to,
but I cannot trace its signature back to a certificate
authority I trust.

What changes on a corporate network

Normally, npm connects to:

https://registry.npmjs.org

Because the URL uses HTTPS, the connection is encrypted. The protocol used to set up and secure that encrypted connection is called TLS, or Transport Layer Security.

Under normal circumstances, the connection looks like this:

npm
 |
 | encrypted HTTPS connection using TLS
 v
registry.npmjs.org

As part of setting up that connection, the npm registry presents its certificate to npm.

As we saw earlier, npm checks that the certificate is valid for registry.npmjs.org. It also follows the certificate chain back to a root certificate authority it trusts.

If those checks succeed, npm and the registry can communicate over the encrypted connection.

Some organisations require their security systems to inspect web traffic entering and leaving company devices. For example, they may scan downloads for malware or check whether sensitive company data is being sent somewhere it should not be.

HTTPS makes that inspection difficult because the contents of the connection between npm and registry.npmjs.org are encrypted.

If a corporate proxy simply passed that encrypted connection between npm and the registry, it would not be able to read the contents.

So, if the organisation wants the proxy to inspect that traffic, the proxy has to take part in the encrypted connection rather than simply pass it along.

Instead of one encrypted connection between npm and the registry, there are now two:

npm
 |
 | encrypted HTTPS connection using TLS
 v
Corporate proxy
 |
 | separate encrypted HTTPS connection using TLS
 v
registry.npmjs.org

The first connection is between npm and the corporate proxy.

The second connection is between the corporate proxy and the real npm registry.

Because npm's encrypted connection ends at the proxy, the proxy can read what npm sends and inspect it. The proxy then sends that traffic onwards through its separate encrypted connection to registry.npmjs.org.

But this raises an important question about certificates.

Normally, the real npm registry presents its certificate to npm. Why can the proxy not simply take that certificate and pass it on?

The proxy can see and copy the certificate from registry.npmjs.org, because a TLS certificate is not secret. But having the certificate is not enough.

A TLS certificate contains a public key. The real npm registry holds the matching private key.

The private key is secret and stays with the system that owns the certificate.

During the TLS handshake, the server does more than present its certificate. It also proves that it possesses the private key that belongs to that certificate.

The corporate proxy does not have the npm registry's private key.

So if the proxy simply copied the real certificate and presented it to npm, it would not be able to prove that it had the matching private key. The TLS handshake would fail.

Instead, the proxy creates its own replacement certificate for registry.npmjs.org. The proxy also controls the private key that belongs to this replacement certificate, so it can complete its own TLS handshake with npm.

The two connections therefore use different certificates:

npm
 |
 | receives a replacement certificate for registry.npmjs.org
 | proxy has the matching private key
 v
Corporate proxy
 |
 | receives the real certificate for registry.npmjs.org
 | registry has the matching private key
 v
registry.npmjs.org

The replacement certificate still says that it is valid for registry.npmjs.org, because that is the hostname npm asked for.

The difference is who signed it.

The real certificate from registry.npmjs.org is signed through a public certificate authority.

The replacement certificate presented by the proxy is signed through a certificate authority controlled by your organisation.

This arrangement is called TLS inspection or TLS interception.

The important point is that npm never sees the original certificate presented by the npm registry. It sees the replacement certificate created by the corporate proxy.

npm then performs the same certificate checks it normally would.

The hostname check can succeed because the replacement certificate is valid for registry.npmjs.org.

But the certificate chain is now different. Instead of leading back to a public root certificate authority, it leads back to your organisation's root certificate authority.

For npm to accept the replacement certificate, npm must trust that corporate root.

If the corporate root certificate is in a trust store npm uses, npm can verify the replacement certificate's chain and the connection succeeds.

If the corporate root is not in a trust store npm uses, npm cannot establish that it trusts whoever signed the replacement certificate. It rejects the connection and reports a certificate error.

Why your browser may work while another tool fails

On a managed work laptop, IT will often install the company's root certificate into the operating system trust store automatically.

This may happen through Group Policy on Windows, mobile device management on macOS, endpoint-management software, or another part of the organisation's standard device configuration.

In practical terms, IT may have already installed the certificate before you received the laptop, or pushed it later through a managed configuration update.

Your browser may therefore work immediately.

It receives the replacement certificate from the corporate proxy, follows the chain back to the company's root certificate authority, finds that root in the operating system trust store, and accepts it.

A developer tool can still fail because not every program gets its trusted certificate authorities from the operating system.

You can therefore end up with this:

Browser -> works

npm -> certificate error

Gradle -> certificate error

All three are getting far enough to begin TLS.

The difference is which certificate authorities each program trusts.

Some applications use the operating system trust store.

Some runtimes maintain their own trust store.

Some tools ship their own bundle of public certificate authorities.

Some can be pointed at an additional CA certificate through configuration or an environment variable.

Containers have their own filesystem and certificate configuration again.

This is the main point to remember when debugging these errors:

Which program is making the failing HTTPS request, and where does that program get its trusted certificate authorities?

Once you know that, you can fix the certificate configuration used by that process.

Get the certificate from the right place

Before adding a certificate anywhere, make sure you have the correct corporate root certificate.

The best source is your organisation's documented internal process.

The certificate may already be installed in the operating system trust store by IT. If so, you may be able to export the existing copy.

Otherwise, your security or networking team may provide it through internal documentation, a managed software portal, a company file share, or another approved location.

Avoid installing a .crt or .pem file simply because somebody sent it to you in chat.

They may have sent the correct certificate, but you have no reliable way to know whether it is current or whether it came from the expected source.

Old corporate certificates also have a habit of remaining in shared folders and chat histories long after they have been replaced.

If your organisation publishes the certificate's SHA-256 fingerprint, compare it with the certificate you received.

A fingerprint is a cryptographic identifier calculated from the certificate. Matching the fingerprint lets you confirm that you have the exact certificate your organisation expects you to trust.

Also check the expiry date.

Internal root and intermediate certificates are occasionally rotated. When that happens, similar certificate errors can suddenly appear across several tools or several members of a team.

Check what certificate you are receiving

You can often inspect the certificate from a browser that successfully reaches the site.

Look at the certificate issuer.

The issuer is the certificate authority that signed the certificate you are currently being shown.

If you visit a public service such as registry.npmjs.org on an unmanaged connection, you would normally expect the certificate chain to lead through a public certificate authority.

On a corporate network using TLS inspection, you may instead see an issuer with your organisation's name or the name of its security product.

For example:

Issuer: Example Corp TLS Inspection CA

That means the certificate you are seeing was signed by the organisation's internal certificate authority rather than through the site's normal public certificate chain.

It is strong evidence that TLS inspection is taking place.

You can also inspect the certificates presented to a command-line TLS client with OpenSSL:

openssl s_client \
  -showcerts \
  -connect registry.npmjs.org:443 \
  -servername registry.npmjs.org \
  </dev/null

The purpose of this command is to open a TLS connection to registry.npmjs.org and print the certificates presented during the handshake.

If your network requires an explicit forward proxy, this command may need to use that proxy as well. OpenSSL supports a -proxy option, for example:

openssl s_client \
  -proxy proxy.example.internal:8080 \
  -showcerts \
  -connect registry.npmjs.org:443 \
  -servername registry.npmjs.org \
  </dev/null

Otherwise, you may be inspecting a different network path from the application you are troubleshooting.

Look at the issuer information in the returned certificates.

If the certificate for a public site has been issued by your organisation's internal CA, the connection is probably being inspected.

Do not treat the command as proof that every application on your laptop follows exactly the same network path. A browser, IDE, command-line tool, and explicitly configured proxy client may use different proxy settings.

Use it as a diagnostic tool alongside the behaviour of the application that is actually failing.

Find the trust store used by the failing tool

Once you have confirmed that the corporate certificate is legitimate, identify the process that is failing before changing any configuration.

Start with the operating system.

On a managed company machine, the root certificate may already be installed there.

If it is missing from a device managed by your organisation, it is usually worth asking IT whether the device policy has applied correctly rather than immediately installing it yourself.

Then look at the failing program.

A trust store is simply the place a program gets its list of trusted certificate authorities from.

Common possibilities include:

You do not need to memorise the exact mechanism used by every language and tool.

You need to recognise that the mechanism can differ.

If curl works but a Gradle build fails, that tells you something useful. The two processes may not be using the same trust store.

If the IDE works but the same build fails in CI, the CI runner may not have the corporate certificate installed.

If the host machine works but a Docker build fails while downloading dependencies, the container may not trust the corporate CA.

Always troubleshoot the process that is actually making the failing HTTPS request.

Java is a common example

Java is worth looking at separately because developers often have several Java installations on one machine.

Java commonly uses a Java keystore called cacerts to store trusted certificate authorities.

A typical import command looks like this:

keytool -importcert \
  -trustcacerts \
  -cacerts \
  -alias corp-root \
  -file corp-root.crt

The purpose of the command is to add the corporate root certificate to the cacerts trust store used by that Java installation.

On a managed work machine, only modify this keystore if that is the approach your organisation expects you to use.

The important part is "that Java installation".

Your terminal, Gradle build, IDE, and CI environment may not all use the same JDK.

For example:

./gradlew -version

shows information about the JVM Gradle is actually using.

That is more useful than assuming Gradle uses whichever java command happens to run in your terminal.

An IDE can also be configured to use a particular JDK for builds, and some IDEs include their own runtime.

You can therefore install the correct certificate into one Java trust store and still see the same error because the failing process is using another JDK.

Before importing the certificate again, check which Java runtime is actually running the failing build.

Other tools may use different certificate settings

The same problem appears outside Java.

Node-based tools, Python applications, Git, package managers, API clients, IDEs, and build systems can all get their trusted certificate authorities from different places.

The exact behaviour can also change between operating systems, tool versions, and how the tool was installed.

For example, one tool may use the operating system trust store while another uses a bundled CA file.

Another may let you add a corporate certificate through an environment variable.

A desktop application may expose its own certificate setting.

The useful troubleshooting process is the same regardless of language:

  1. Identify which process is making the HTTPS request.

  2. Find out where that process gets its trusted certificate authorities.

  3. Add the approved corporate certificate using the supported configuration for that tool.

  4. Restart the process if it only reads its certificate configuration when it starts.

Do not assume that having the correct .crt or .pem file somewhere on your laptop means every application can use it.

The certificate must be available through the trust mechanism the failing process actually reads.

Containers have their own trust configuration

Containers make this separation particularly obvious.

A container has its own filesystem.

Installing the corporate certificate on your laptop does not automatically install it inside a Docker image.

Suppose your Docker build runs:

npm install

inside the image.

It is the Node process inside that container that makes the HTTPS request.

That process sees the container's certificate configuration, not the trust store on your host machine.

If the container does not trust the corporate certificate authority, npm install can fail even though npm works perfectly on your laptop outside Docker.

The image therefore needs the approved corporate CA installed or configured using the method appropriate for that image and runtime.

There is also a separate case when Docker itself connects to a container registry.

That request is made by Docker rather than by your application inside the container, so it may use a different certificate configuration again.

Keep returning to the same question:

Which process is making this connection, and which trust store does that process use?

Do not fix this by turning certificate verification off

Search for almost any certificate error and you will quickly find commands or configuration settings that disable TLS certificate verification.

For example:

curl -k https://example.com

or:

strict-ssl=false

These make the certificate error disappear because they stop performing the check that failed.

They do not make the certificate trusted.

TLS encryption protects the contents of the connection.

Certificate verification checks that the system presenting the certificate can prove an identity your client trusts.

If you disable verification, the traffic may still be encrypted, but the client is no longer properly checking who is at the other end of that encrypted connection.

That is particularly risky with developer tooling.

Package managers download code that may later execute.

Git clients download source code.

Build systems and CI jobs retrieve dependencies, plugins, images, and other executable content.

Disabling certificate verification removes one of the checks that helps ensure those files came through the connection you intended to establish.

There is also a practical maintenance problem.

A temporary workaround can easily end up in a shell profile, repository configuration, Dockerfile, CI job, or internal setup guide.

Months later, nobody remembers why it was added.

It can also hide genuine certificate failures, such as an expired corporate certificate or a badly configured internal service.

If you temporarily disable verification as a diagnostic test and your organisation permits it, treat the result only as evidence that certificate verification is the failing part.

Turn verification back on and fix the trust configuration properly.

How much this varies

Corporate environments differ.

Some organisations push the internal root certificate only to the operating system. Others also configure common developer runtimes as part of the standard laptop setup.

Some organisations do not inspect TLS at all. Others inspect most HTTPS traffic but exclude particular services through policy.

TLS inspection may also be performed by endpoint security software rather than through an obvious proxy configuration.

So do not assume every certificate error at work has the same cause.

If one service fails while others work, inspect the certificate that service is presenting and confirm which trust store the failing tool uses before changing anything.

Take away actions

Next in the series: why your package manager can still fail once the proxy and the certificate are both sorted.