Short answer
"self signed certificate in certificate chain" means the client found a self-signed certificate (typically a root) in the chain that is not in the client's trust store. Either you are talking to a server using an internal CA, or a TLS-inspection proxy sits in the middle re-issuing certificates with its own root. The fix is to add the correct root to the trust store — not to disable certificate validation.
OpenSSL codes 18 and 19
In openssl s_client output the error maps to two verify codes:
- 18 —
self signed certificate: the server's leaf certificate is itself self-signed (no real CA behind it). - 19 —
self signed certificate in certificate chain: the chain ends in a self-signed root the client does not trust.
openssl s_client -connect internal.example.com:443 -servername internal.example.com 2>/dev/null \ | grep "Verify return code"
The three common causes
- TLS inspection (corporate proxy): Many organisations decrypt outbound HTTPS in a proxy/firewall that re-issues every certificate with the company's own root. Clients on machines without that root installed fail. This is why it "works at home but not at the office".
- Internal CA: The server uses a certificate from a private PKI. The root must be distributed to all clients (typically via MDM/GPO), otherwise validation fails.
- A wrongly generated self-signed certificate: A developer made a self-signed certificate for an environment that real clients now hit.
How to fix it — the right way
Add the self-signed root to the client's trust store so validation is meaningful again:
# Linux (system-wide) sudo cp corporate-root.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates # Test against a known root without changing the system openssl s_client -connect internal.example.com:443 -CAfile corporate-root.crt # curl against an internal CA curl --cacert corporate-root.crt https://internal.example.com
For Java and .NET the root must go into the relevant trust store rather than the system one — see "works in browser, not in Java/.NET".
What you should NOT do
The temptation is to disable validation: curl -k, verify=False in Python, or NODE_TLS_REJECT_UNAUTHORIZED=0. That makes the error disappear — and removes all protection against man-in-the-middle at the same time. Code running with validation disabled in production is a vulnerability, not a fix. Add the correct root instead.
Keeping track of internal and self-signed certificates
Self-signed and internal certificates are hard to track because they never appear in public Certificate Transparency logs. CertControl also discovers internal endpoints via its collector agent, shows which ones use self-signed or internal-CA certificates, and tracks their expiry — so they do not become a blind spot in your certificate inventory.
Frequently asked questions
Why does it work at home but not at the office?
Because the corporate network probably runs TLS inspection: a proxy decrypts traffic and re-issues certificates with the company's own root. That root is installed on company-managed machines but not on your own.
Is it safe to add an internal root to the trust store?
Yes, if you trust the party that owns the root (typically your own organisation). It is the correct way to handle internal CAs. It is entirely different from turning validation off.
What is the difference between code 18 and 19?
Code 18 means the server's own certificate is self-signed. Code 19 means the chain ends in a self-signed root the client does not trust — often an internal CA or an inspection proxy.
Can't I just use curl -k?
You can, but then you no longer validate the server's identity and are exposed to man-in-the-middle. Use it at most for one-off troubleshooting, never in code that runs in production.