OpenSSL Error: Troubleshooting 'self signed certificate in certificate chain' Validation

Resolve the 'self signed certificate in certificate chain' OpenSSL error in Nginx, Apache, and client applications. Learn to fix SSL/TLS validation issues.


When encountering an “OpenSSL error: self signed certificate in certificate chain” message, it signifies a critical trust issue during SSL/TLS handshake. This error means that while a connection attempt was made, the client could not validate the authenticity of the server’s certificate because one or more certificates in the chain presented by the server, up to a root Certificate Authority (CA), could not be trusted. This guide provides a highly technical, step-by-step approach to diagnose and resolve this common problem in various hosting environments and client applications.

Symptom & Error Signature

This error manifests as connection failures or warnings across various applications and tools attempting to connect to an SSL/TLS-enabled service. Here are typical error signatures you might encounter:

Curl:

$ curl https://untrusted.example.com/
curl: (60) SSL certificate problem: self signed certificate in certificate chain
More details here: https://curl.haxx.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

Wget:

$ wget https://untrusted.example.com/
--2026-06-27 10:30:00--  https://untrusted.example.com/
Resolving untrusted.example.com (untrusted.example.com)... 192.0.2.10
Connecting to untrusted.example.com (untrusted.example.com)|192.0.2.10|:443... connected.
ERROR: cannot verify untrusted.example.com's certificate, issued by 'CN=My Internal CA':
  Self-signed certificate in the chain.
To connect to untrusted.example.com insecurely, use '--no-check-certificate'.

OpenSSL CLI (s_client):

$ openssl s_client -connect untrusted.example.com:443 -showcerts
CONNECTED(00000003)
# ... (certificate details omitted) ...
Verify return code: 19 (self signed certificate in certificate chain)

Node.js / JavaScript (example using https module):

// Example of error in Node.js application logs
Error: self signed certificate in certificate chain
    at TLSSocket.onConnectSecure (_tls_wrap.js:1515:34)
    at TLSSocket.emit (events.js:400:28)
    at TLSSocket._finishInit (_tls_wrap.js:933:8)
    at TLSWrap.ssl.onhandshakedone (_tls_wrap.js:705:12) {
  code: 'DEPTH_ZERO_SELF_SIGNED_CERT' // Or similar
}

Python (requests library):

# Example of error in Python application logs
requests.exceptions.SSLError: HTTPSConnectionPool(host='untrusted.example.com', port=443): Max retries exceeded with url: / (Caused by SSLError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self signed certificate in certificate chain (_ssl.c:1129)'))

Root Cause Analysis

The “self signed certificate in certificate chain” error indicates that a client cannot establish a trusted path from the server’s presented certificate back to a root Certificate Authority (CA) that it implicitly trusts. This doesn’t necessarily mean the server’s end-entity certificate is self-signed, but rather that some certificate within the chain — be it an intermediate or the root — is not recognized or trusted.

Understanding the certificate chain is crucial:

  • Leaf Certificate: The actual server certificate for example.com.
  • Intermediate CA Certificate(s): One or more certificates that bridge the trust from the leaf certificate to the root CA. These are signed by the root CA or another intermediate CA.
  • Root CA Certificate: A highly trusted certificate, typically pre-installed in operating systems and browsers, which signs the intermediate CAs.

The error typically arises from one of these underlying reasons:

  1. Incomplete Certificate Chain on the Server:

    • Most Common Cause: The web server (e.g., Nginx, Apache) is configured to present only the leaf (server) certificate and omits one or more intermediate CA certificates that are required for the client to build a complete chain to a trusted root. Clients only have a bundle of trusted root CAs; they rely on the server to provide the intermediates.
    • Without the intermediates, the client cannot verify that the leaf certificate was genuinely issued by a trusted CA, as the path from leaf -> intermediate -> root is broken.
  2. Untrusted Intermediate or Root CA:

    • The server is presenting a complete certificate chain, but one of the intermediate certificates or the ultimate root CA certificate is not present in the client’s trusted CA store.
    • This is common in enterprise environments using their own internal Public Key Infrastructure (PKI) where custom CAs are used, and their root certificates haven’t been deployed to client machines.
    • It can also happen if the certificate was issued by a less common or new CA whose root hasn’t been widely adopted by older client systems.
  3. Truly Self-Signed Leaf Certificate:

    • The server’s actual end-entity certificate is self-signed (i.e., it was not issued by any CA at all). While this technically forms a chain of one “self-signed” certificate, the client will not trust it unless explicitly configured to do so. This is usually only acceptable for internal development or testing environments where security is less critical, or when clients are specifically configured to trust that particular self-signed certificate.

Step-by-Step Resolution

The resolution path depends on whether the issue is server-side (missing intermediates) or client-side (untrusted CA).

1. Analyze the Certificate Chain Presented by the Server

First, determine which certificate in the chain is causing the problem. Use openssl s_client to inspect the server’s certificate chain.

openssl s_client -connect yourdomain.com:443 -showcerts -servername yourdomain.com

Analyze the output:

  • Look for Verify return code: 19 (self signed certificate in certificate chain) or Verify return code: 21 (unable to verify the first certificate).
  • Review the certificate chain section (0 s:/CN=yourdomain.com, 1 s:/CN=Intermediate CA, 2 s:/CN=Root CA).
  • Pay attention to the depth and the Issuer / Subject fields for each certificate. If a certificate’s Issuer is the same as its Subject, it’s a self-signed certificate. If Verify return code is 19 and you see a certificate in the chain (depth > 0) with Issuer and Subject matching, that’s likely your culprit.
  • If depth=0 (your server certificate) is the only one shown, or if depth=1 (the intermediate) is missing, the server is likely not sending the full chain.

2. Verify Server-Side Certificate Configuration

This step is crucial if openssl s_client reveals a missing intermediate certificate (depth > 0) or an incomplete chain. The goal is to ensure your web server provides the full certificate chain (leaf + all intermediates).

  • Nginx Configuration: Ensure that ssl_certificate points to the fullchain.pem file, which typically contains your leaf certificate followed by all intermediate certificates.

    # /etc/nginx/sites-available/yourdomain.com
    server {
        listen 443 ssl http2;
        server_name yourdomain.com;
    
        ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; # <-- IMPORTANT: Use fullchain.pem
        ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;
    
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
        ssl_prefer_server_ciphers on;
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1h;
        ssl_stapling on;
        ssl_stapling_verify on;
        resolver 8.8.8.8 8.8.4.4 valid=300s;
        resolver_timeout 5s;
    
        # ... other configurations ...
    }

    After modifying, test Nginx configuration and restart:

    sudo nginx -t
    sudo systemctl restart nginx
  • Apache2 Configuration: Ensure SSLCertificateFile points to your fullchain.pem (which usually bundles leaf and intermediates) or, for older configurations, SSLCertificateFile points to the leaf cert and SSLCertificateChainFile (Apache < 2.4.8) or SSLCACertificateFile (Apache >= 2.4.8) points to the intermediate bundle.

    # /etc/apache2/sites-available/yourdomain-le-ssl.conf
    <IfModule mod_ssl.c>
    <VirtualHost *:443>
        ServerName yourdomain.com
        DocumentRoot /var/www/yourdomain.com/html
    
        SSLEngine on
        SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/fullchain.pem # <-- IMPORTANT: Use fullchain.pem
        # Or, if using separate files:
        # SSLCertificateFile /etc/letsencrypt/live/yourdomain.com/cert.pem
        # SSLCertificateChainFile /etc/letsencrypt/live/yourdomain.com/chain.pem # Apache 2.2/older
        # SSLCACertificateFile /etc/letsencrypt/live/yourdomain.com/chain.pem # Apache 2.4+ (can replace SSLCertificateChainFile)
    
        SSLCertificateKeyFile /etc/letsencrypt/live/yourdomain.com/privkey.pem
    
        # ... other configurations ...
    </VirtualHost>
    </IfModule>

    After modifying, test Apache configuration and restart:

    sudo apache2ctl configtest
    sudo systemctl restart apache2

[!IMPORTANT] For certificates issued by CAs like Let’s Encrypt, the fullchain.pem file is designed to contain both your domain’s certificate and the necessary intermediate certificate(s) in the correct order. Always ensure your web server configuration uses this fullchain.pem for ssl_certificate (Nginx) or SSLCertificateFile (Apache). Using cert.pem alone will lead to “self signed certificate in certificate chain” errors on the client side.

3. Client-Side Resolution: Trusting the Certificate Authority

If your server is configured correctly and presents the full chain, but clients still report the error, it means the client’s system or application does not trust one of the CAs in the chain (typically the root or an intermediate from a custom/private PKI).

  • System-wide Trust (Debian/Ubuntu Linux): To trust a custom Root or Intermediate CA across the entire operating system, you need to add its .crt file to the system’s trusted CA store.

    1. Obtain the CA certificate: Get the .crt or .pem file for the Root or Intermediate CA that is untrusted. If you extracted it from the openssl s_client output, save it as a .crt file (e.g., my-custom-ca.crt).
    2. Copy to trusted directory:
      sudo cp /path/to/my-custom-ca.crt /usr/local/share/ca-certificates/
    3. Update CA certificates:
      sudo update-ca-certificates
      You should see output indicating your CA was added (e.g., Adding debian:my-custom-ca.crt).
    4. Verify:
      ls /etc/ssl/certs/ | grep my-custom-ca
      This should show a symlink to your copied certificate.
  • Application-Specific Trust: Many applications and programming language runtimes maintain their own CA bundles or offer ways to specify additional trusted CAs.

    • curl:

      curl --cacert /path/to/my-custom-ca.crt https://yourdomain.com/

      [!WARNING] Using curl --insecure https://yourdomain.com/ will bypass SSL certificate verification entirely. While it gets rid of the error, it eliminates crucial security checks and should never be used in production environments or when handling sensitive data. It’s strictly for debugging or non-sensitive, isolated development testing.

    • wget:

      wget --ca-certificate=/path/to/my-custom-ca.crt https://yourdomain.com/

      [!WARNING] Similar to curl, wget --no-check-certificate https://yourdomain.com/ disables certificate validation, making your connection vulnerable to Man-in-the-Middle attacks. Avoid its use in production.

    • Node.js: You can specify extra CA certificates via an environment variable before launching your Node.js application:

      NODE_EXTRA_CA_CERTS=/path/to/my-custom-ca.crt node your_app.js

      Alternatively, for more fine-grained control or if not all traffic should trust this CA:

      const https = require('https');
      const fs = require('fs');
      
      const customCa = fs.readFileSync('/path/to/my-custom-ca.crt');
      
      const agent = new https.Agent({
        ca: [customCa], // Add your custom CA to the default bundle
        rejectUnauthorized: true // Ensure verification is still enabled
      });
      
      https.get('https://yourdomain.com', { agent }, (res) => {
        // ... handle response
      }).on('error', (e) => {
        console.error(`Error: ${e.message}`);
      });
    • Python (using requests library):

      export REQUESTS_CA_BUNDLE=/path/to/my-custom-ca.crt
      python your_script.py

      Or programmatically:

      import requests
      
      try:
          response = requests.get('https://yourdomain.com', verify='/path/to/my-custom-ca.crt')
          print(response.text)
      except requests.exceptions.SSLError as e:
          print(f"SSL Error: {e}")

      [!WARNING] Using verify=False in Python’s requests library will disable SSL certificate verification. This is insecure and should only be used for debugging in controlled environments, never in production.

    • Docker Daemon/Client (for Private Registries): If you’re pulling images from a private Docker registry that uses a custom CA, the Docker daemon needs to trust that CA.

      1. Create directory for the registry:
        sudo mkdir -p /etc/docker/certs.d/myregistry.local:5000
      2. Copy the CA certificate:
        sudo cp /path/to/my-custom-ca.crt /etc/docker/certs.d/myregistry.local:5000/ca.crt
      3. Restart Docker daemon:
        sudo systemctl restart docker

4. Re-issue or Renew the Certificate

If openssl s_client clearly shows that your leaf certificate (depth=0) is truly self-signed and was intended to be issued by a public, trusted CA (e.g., Let’s Encrypt), then the certificate itself is the problem.

  • For Let’s Encrypt: You may need to force a renewal to ensure a properly signed certificate and fullchain.pem are generated.
    sudo certbot renew --force-renewal
    If renewing doesn’t work or if it’s a new setup, try obtaining a fresh certificate:
    sudo certbot certonly --nginx -d yourdomain.com -d www.yourdomain.com
    Ensure your web server is configured correctly to use the newly issued fullchain.pem as per Step 2.

5. Verify the Entire CA Bundle (for Client-Side Trust Stores)

In rare cases, the system’s default CA bundle on the client side might be corrupted or severely outdated, even if no custom CAs are involved.

  • Reinstall ca-certificates (Debian/Ubuntu):
    sudo apt update
    sudo apt install --reinstall ca-certificates
    This ensures that your system has the latest set of commonly trusted root certificates.

By systematically working through these steps, starting with server-side chain analysis and progressing to client-side trust store management, you can effectively diagnose and resolve the “self signed certificate in certificate chain” validation error.