OpenSSL Troubleshooting: Resolving the unable to get local issuer certificate
Error
When establishing secure connections with OpenSSL (such as making network requests in a PHP environment, running openssl
or curl
commands, or initiating SSL/TLS connections in Node.js/Python applications), developers may encounter the error message 20:unable to get local issuer certificate
. This is a common legacy issue related to how OpenSSL verifies peer certificates.
For security reasons, OpenSSL requires explicit knowledge of trusted root certificate authorities (CAs) when validating a certificate chain. If these trust anchors cannot be found or specified, OpenSSL can't determine the legitimacy of server certificates and will therefore report this error.
This article explains the underlying causes of this error and provides detailed methods to solve it within the ServBay environment—including how to configure OpenSSL’s trust store for PHP, Python, Node.js, and curl
commands.
The Error: 20:unable to get local issuer certificate
Problem Description
When OpenSSL tries to validate the remote server’s SSL/TLS certificate, it constructs a chain leading from the server certificate up to a trusted root CA. If OpenSSL cannot find any of the required intermediate certificates, the final root CA certificate locally, or a properly configured trust store (CAFile or CAPath) to search for these, certificate validation fails and the 20:unable to get local issuer certificate
error is returned.
Put simply, OpenSSL doesn’t know which certificate authority it should trust, so it can’t verify the identity of the server you are connecting to.
OpenSSL Versions and CA Certificate Paths in ServBay
As an integrated local web development environment, ServBay comes bundled with the OpenSSL package and commonly used public CA root certificates for easy developer use. ServBay’s OpenSSL version depends on your device’s processor:
- Apple Silicon (M series) ServBay: Uses OpenSSL version 3.2.1.
- Intel chip ServBay: Uses OpenSSL version 1.1.1u.
The relevant CA certificate file (cacert.pem
) and certificate directory (certs
) for each version reside within ServBay’s software package installation path. Locate the correct path according to your ServBay version:
# Bundle file containing all trusted root certificates
cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
# Directory containing individual certificate files (cacert.pem is sufficient for most use cases, but some apps may require capath)
capath=/Applications/ServBay/package/common/openssl/3.2/certs
2
3
4
# Bundle file containing all trusted root certificates
cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
# Directory containing individual certificate files
capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
2
3
4
The core solution to the unable to get local issuer certificate
error is to explicitly specify the above-mentioned cafile
or capath
path wherever OpenSSL is required, instructing OpenSSL where to find trusted CA certificates.
Solution Examples
Below are examples on how to specify OpenSSL’s CA trust store in different tools and language environments.
Example 1: Testing Connections with the openssl
Command
If you encounter the error when using the openssl s_client
command directly:
openssl s_client -quiet -connect gmail.com:443
You may see an error output like the following, which contains verify error:num=20:unable to get local issuer certificate
:
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... other connection info ...
2
3
4
5
6
7
8
Solution:
Specify the path to ServBay’s bundled CA certificate file explicitly using the -CAfile
parameter:
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/3.2/cacert.pem
openssl s_client -quiet -connect gmail.com:443 -CAfile /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
After a successful connection and certificate verification, the verify return
value will be 1
and you will not see any verify error:num=20
lines:
depth=2 C=US, O=Google Trust Services LLC, CN=GTS Root R1
verify return:1
depth=1 C=US, O=Google Trust Services, CN=WR2
verify return:1
depth=0 CN=gmail.com
verify return:1
# ... other connection info ...
2
3
4
5
6
7
Example 2: Using OpenSSL in PHP
Many of PHP’s network features—such as file_get_contents
accessing HTTPS URLs, stream_socket_client
establishing SSL connections, and the cURL extension—use OpenSSL under the hood. You can specify the CA trust store by editing php.ini
or by setting stream context options in your code.
Method A: Edit php.ini
(Recommended)
This is the most convenient global solution. Edit the php.ini
file associated with your current PHP version (the ServBay control panel can help you locate it), look for the [openssl]
section, and add or modify the following settings to specify openssl.cafile
and openssl.capath
. Make sure to use the correct paths according to your processor type.
[openssl]
; Specify the trusted CA certificate bundle
openssl.cafile=/Applications/ServBay/package/common/openssl/3.2/cacert.pem
; Specify the directory containing CA certificates (optional, but recommended)
openssl.capath=/Applications/ServBay/package/common/openssl/3.2/certs
2
3
4
5
[openssl]
; Specify the trusted CA certificate bundle
openssl.cafile=/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem
; Specify the directory containing CA certificates (optional, but recommended)
openssl.capath=/Applications/ServBay/package/common/openssl/1.1.1u/certs
2
3
4
5
After editing php.ini
, restart the PHP service within ServBay (or restart all of ServBay) to apply the changes.
Method B: Add Configuration in Code (Affects Only Current Connection)
If you don’t want to modify global php.ini
, you can provide the cafile
option in the SSL stream context when creating SSL/TLS connections using stream_context_create
.
<?php
// Example: Connect to an SSL/TLS-enabled SMTP server
$server = 'ssl0.ovh.net';
$port = 465;
// Choose the correct CA certificate file path for your ServBay version
// For Apple Silicon:
$caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// For Intel:
// $caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
$contextOptions = [
'ssl' => [
'verify_peer' => true, // Enable peer certificate verification
'verify_peer_name' => true, // Ensure the hostname in the certificate matches
'allow_self_signed' => false, // Do not allow self-signed certificates unless explicitly trusted
'cafile' => $caCertFile, // Specify CA certificate bundle
// 'capath' => '/Applications/ServBay/package/common/openssl/3.2/certs', // Optional: specify CA certs directory
],
];
$context = stream_context_create($contextOptions);
// Establish SSL/TLS connection with the given context
$connection = @stream_socket_client(
"ssl://$server:$port",
$errno,
$errstr,
30, // Timeout
STREAM_CLIENT_CONNECT,
$context // Pass context options
);
if ($connection) {
echo "Connection established to $server:$port\n";
// Example: Send EHLO command
fwrite($connection, "EHLO servbay.demo\r\n"); // Using a ServBay-branded sample domain
while (!feof($connection)) {
echo fgets($connection);
}
fclose($connection);
} else {
echo "Failed to connect to $server:$port. Error: $errstr ($errno)\n";
}
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Example 3: Using OpenSSL in Python (ssl Module)
Python’s ssl
module also allows you to create SSL/TLS contexts and specify trusted CA certificates.
import ssl
import socket
server = 'ssl0.ovh.net'
port = 465
# Choose the correct CA certificate file path for your ServBay version
# For Apple Silicon:
ca_cert_file = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem'
# For Intel:
# ca_cert_file = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem'
# Create a default SSL context and specify CA file
context = ssl.create_default_context(cafile=ca_cert_file)
# You could also specify capath: context = ssl.create_default_context(capath='/Applications/ServBay/package/common/openssl/3.2/certs')
try:
# Establish plain socket connection
with socket.create_connection((server, port)) as sock:
# Wrap the plain socket into an SSL socket
with context.wrap_socket(sock, server_hostname=server) as ssock:
print(f"SSL connection established. Negotiated Protocol: {ssock.version()}")
# Example: Send EHLO command
ssock.sendall(b"EHLO servbay.demo\r\n") # Using a ServBay-branded sample domain
while True:
data = ssock.recv(4096)
if not data:
break
print(data.decode())
except Exception as e:
print(f"Failed to connect or SSL error: {e}")
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Example 4: Using OpenSSL in Node.js (tls Module)
Node.js’s tls
module is used to establish TLS/SSL connections. You can specify trusted CA certificates in the connection options via the ca
property. The ca
value can be a string or Buffer containing one or more certificates. The easiest way is to read the ServBay-provided cacert.pem
file contents.
const tls = require('tls');
const fs = require('fs');
const server = 'www.google.com'; // Use a standard, trusted site
const port = 443;
// Choose the correct CA certificate file path for your ServBay version
// For Apple Silicon:
const caCertFile = '/Applications/ServBay/package/common/openssl/3.2/cacert.pem';
// For Intel:
// const caCertFile = '/Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem';
const options = {
host: server,
port: port,
// Read CA certificate file contents
ca: fs.readFileSync(caCertFile),
// Node.js tls module performs server hostname verification by default (checkServerIdentity),
// If the CA file is correctly specified and the server's cert is valid, it should succeed.
// Do not disable checkServerIdentity unless you have a specific reason and understand the risks.
// checkServerIdentity: () => { return null; } // <--- Don't use this line; it disables critical security checks!
};
const socket = tls.connect(options, () => {
console.log('SSL connection established');
// For HTTPS, you'd usually send an HTTP request here; this example just establishes the connection
// socket.write('GET / HTTP/1.1\r\nHost: ' + server + '\r\n\r\n');
});
socket.on('data', (data) => {
console.log(data.toString());
});
socket.on('close', () => {
console.log('Connection closed');
});
socket.on('error', (error) => {
console.error('Error:', error.message); // Print error messages
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
Note: In the Node.js example, we've removed checkServerIdentity: () => { return null; }
. Enabling this disables crucial hostname verification and is highly insecure. The OpenSSL error unable to get local issuer certificate
concerns the trust anchor, not hostname verification (verify_peer_name
). With the right ca
setting and a valid server certificate, Node.js’s default hostname verification should succeed. If you encounter hostname verification errors, it's likely a certificate problem, not an issue with your CA trust store.
Example 5: Using OpenSSL with the curl
Command
The curl
command uses OpenSSL (or another SSL library) for HTTPS requests. You can specify the CA certificate bundle using the --cacert
parameter.
# Use ServBay's provided CA certificate to access an HTTPS website
# Choose the correct path for your ServBay version
# For Apple Silicon:
curl --cacert /Applications/ServBay/package/common/openssl/3.2/cacert.pem https://example.com
# For Intel:
# curl --cacert /Applications/ServBay/package/common/openssl/1.1.1u/cacert.pem https://example.com
2
3
4
5
6
7
If the CA certificate path is correct and the server’s certificate is valid, curl
will fetch the content without reporting certificate verification errors.
Summary
The 20:unable to get local issuer certificate
error is very common when using OpenSSL for SSL/TLS connections, and fundamentally occurs because OpenSSL requires an explicitly specified trust store to validate server certificates. ServBay provides developers with a pre-bundled cacert.pem
file containing common public CA root certificates.
To fix this error, simply specify the path to ServBay’s cacert.pem
wherever appropriate in your development environment—through php.ini
, SSL context options in your code, or command-line parameters like openssl
’s -CAfile
or curl
’s --cacert
. Be sure to match the path to your macOS chip type (Apple Silicon or Intel) and the corresponding ServBay OpenSSL version. With correct CA trust store configuration, your local development environment can securely communicate with external SSL/TLS services.