Ahosting Logo
Knowledge Base

How to Fix Certificate Errors in Scripts and API Clients

Why a script rejects a certificate a browser acceptsBrowserScript or API clientTrust listthe operating system list, kept currentoften its own bundled list, possibly oldMissing intermediateoften filled in from cachefails outrightHostname checksstrictstrict, and less forgiving of edge casesThe wrong fixn/adisabling verification, which removesthe protection entirelyIf a script cannot verify a certificate, fix the chain or update its trust bundle. Turning verification off makes theconnection meaningless.

A site loads perfectly in a browser and your script refuses it with a certificate error. Nothing is inconsistent. The two verify differently.

Why they disagree

A browser carries the operating system's trust list, kept reasonably current, and it caches intermediate certificates it has encountered on other sites. So it can often complete a chain the server failed to send.

A script carries its own trust list, frequently a bundle shipped with the language, the package or the container image, and frequently old. It caches nothing. An incomplete chain fails, every time, on every machine.

That single difference explains most of these reports.

Diagnose in one command

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | head -20

Look for the verification result. Verify return code: 0 (ok) means the chain is complete from this machine's point of view.

unable to get local issuer certificate means it is not, either the server is not sending the intermediates, or this machine does not have the root.

Distinguish them by testing from a machine you know is current. If that succeeds and the failing one does not, it is the client's trust list; if both fail, it is the server. Checking a certificate from the command line explains reading the output.

Cause one: the server is not sending intermediates

The most common, and it is your problem to fix if the server is yours.

The server must send its certificate and the intermediates, not the root, which the client already has. Understanding the certificate chain goes into installing the full chain, and why a certificate is trusted walks through why the missing piece matters.

If the server is somebody else's, tell them. It is a real misconfiguration affecting every non-browser client, not just yours.

Cause two: the client's trust list is old

Containers and older systems ship a bundle that stops being updated. When an authority introduces a new root, that client cannot verify certificates under it.

apt-get update && apt-get install --reinstall ca-certificates # Debian/Ubuntu
yum reinstall ca-certificates # RHEL family

In PHP, the bundle used is a configuration setting and is frequently unset or pointing at a file that no longer exists:

php -i | grep -i cafile

Point it at the system bundle rather than shipping your own copy, which becomes the next stale bundle.

Cause three: the server name is not being sent

One address hosts many sites, and the server chooses the certificate from the name requested.

A client that does not send it gets the server's default certificate, which is for a different site, and the mismatch is reported as a certificate error.

Modern clients send it automatically. Very old libraries, and some hand-rolled connections, do not, and the symptom is a certificate error naming a hostname you have never heard of.

What never to do

curl -k https://example.com # do not
CURLOPT_SSL_VERIFYPEER => false # do not

These make the error disappear by removing verification entirely. The connection is still encrypted and there is no longer anything establishing who it is encrypted to, so any intermediary can present their own certificate and read everything.

That matters especially here, because these connections usually carry API keys and credentials, which is exactly what an intermediary would want.

It appears constantly in forum answers because it works instantly. It is the difference between fixing the fault and removing the alarm.

The legitimate exception

A private service using an internal authority, where the correct answer is to add that authority's root to the client's trust list rather than disable verification:

curl --cacert /path/to/internal-ca.crt https://internal.example.com

Verification still happens; you have simply told the client who else to trust. Using client certificates walks through the arrangement where you run your own authority.

When it worked yesterday

Three usual causes. The certificate renewed and the new chain is not installed correctly. An authority changed its intermediates. Or the server's clock is wrong, so a valid certificate appears not yet valid. There is more on that one, and it is the least suspected in why a server clock matters.

Find which bundle the client is actually using

Each language and runtime has its own idea of where trusted roots live, and the failing one is frequently not the system's.

openssl version -d
python3 -c "import ssl; print(ssl.get_default_verify_paths())"
node -p "process.env.NODE_EXTRA_CA_CERTS || 'system default'"
php -r 'echo ini_get("openssl.cafile") ?: "unset", "\n";'

A path that does not exist, or one inside an application directory instead of the system's, is the finding. Applications that ship their own bundle keep whatever was current when they were packaged.

Pointing the client at the system bundle instead of a private copy means it is maintained by the operating system's updates rather than by nobody, which is the difference between a fix and the same problem in two years.

Containers are the common case now

A minimal container image frequently ships without any certificate bundle at all, and every outbound HTTPS request fails with a verification error that reads like a server problem.

ls -la /etc/ssl/certs/ca-certificates.crt 2>/dev/null
apk add --no-cache ca-certificates # Alpine
apt-get install -y ca-certificates # Debian

The signature is distinctive: the same code works on a developer's machine and fails identically for every destination inside the container. That is not a certificate problem with any particular server. It is the absence of the list.

Installing the package is the fix, and it belongs in the image instead of being applied at runtime. For the build, see running Docker on a VPS.

Verify against a known-good destination first

Before investigating a specific server, establish whether the client can verify anything at all.

curl -sI https://example.com >/dev/null && echo "client verification works"
openssl s_client -connect example.com:443 </dev/null 2>/dev/null | grep 'Verify return'

If a well-known site also fails, the problem is the client and no amount of work on the target server helps. If it succeeds, the problem is specific to that destination.

That single check saves the most common wasted effort here: reconfiguring a server whose chain was fine for a client that could not verify anything.

Let it fail loudly rather than quietly

The temptation under time pressure is to catch the verification error and continue, which is disabling verification with extra steps.

An integration that silently falls back to an unverified connection is worse than one that fails, because nobody investigates a working system, and the fallback becomes permanent.

Where a connection genuinely must proceed, log the failure at a level somebody sees and record how often it happens. A count that is not zero is a fault waiting to be understood in place of a setting. Why a certificate is trusted explains what the verification was establishing.