How to check if a certificate is self-signed

Print the subject and the issuer of the served certificate and compare the two strings: openssl s_client -connect host:443 -servername host </dev/null | openssl x509 -noout -subject -issuer. Identical strings mean the certificate signed itself, and the same handshake reports Verify return code: 18 (self-signed certificate).

Checker offline. Follow the manual steps below, they give the same answer.

Why check this

A self-signed certificate on staging is normal. The same certificate on the host a customer reaches is an outage, because browsers refuse the navigation and mobile clients throw a trust exception. Run this check after any infrastructure change that touches the load balancer, and on the first deploy to a new environment, where the placeholder certificate written by the provisioning script is often still in place.

Prerequisites

Steps

  1. Step 1.

    Print the subject and the issuer of the certificate the server sends.

    openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer
    
    subject=C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com
    issuer=C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com

    The two lines are character for character the same. That is the definition of self-signed.

  2. Step 2.

    Read the verification verdict from the same handshake.

    openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com </dev/null 2>&1 | grep -E 'verify error|Verify return code'
    
    verify error:num=18:self-signed certificate
      Verify return code: 18 (self-signed certificate)
  3. Step 3.

    Run the same read against a host with a publicly issued certificate, to see what a passing result looks like.

    openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer
    
    subject=CN = example.com
    issuer=C = US, O = SSL Corporation, CN = Cloudflare TLS Issuing ECC CA 3
  4. Step 4.

    Separate a self-signed leaf from a leaf issued by a private CA, which fails for a different reason.

    openssl s_client -connect untrusted-root.badssl.com:443 -servername untrusted-root.badssl.com </dev/null 2>&1 | grep -E 's:|i:|Verify return code'
    
     0 s:C = US, ST = California, L = San Francisco, O = BadSSL, CN = *.badssl.com
     i:C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Untrusted Root Certificate Authority
    1 s:C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Untrusted Root Certificate Authority
     i:C = US, ST = California, L = San Francisco, O = BadSSL, CN = BadSSL Untrusted Root Certificate Authority
      Verify return code: 19 (self-signed certificate in certificate chain)

    The leaf at position 0 carries an issuer of its own. The self-signature sits one level up, on the root at position 1, so the code is 19 and not 18.

  5. Step 5.

    Prove the signature belongs to the key inside the same certificate.

    openssl s_client -connect self-signed.badssl.com:443 -servername self-signed.badssl.com </dev/null 2>/dev/null | openssl x509 -outform PEM > leaf.pem && openssl verify -CAfile leaf.pem leaf.pem
    
    leaf.pem: OK

    A certificate that validates while acting as its own trust anchor cannot have been signed by anything else.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | subject and issuer are identical | The certificate signed itself | Replace it before the host is public, or pin it if the host stays internal | | Verify return code: 18 | A self-signed leaf | The code to quote in the ticket, because it names the exact failure | | Verify return code: 19 | A real chain ending at a root the local store does not hold | Install the private root on the clients, or move the host to a public CA | | Verify return code: 0 (ok) with different names | A publicly trusted certificate | Nothing here. Read expiry and SAN coverage next | | openssl verify -CAfile leaf.pem leaf.pem prints OK | Confirmed self-signature | Step 1 is settled, no further parsing needed |

Common mistakes

Sign: The certificate carries Basic Constraints CA:FALSE, so the team rules out a self-signed certificate.Cause: A self-signed leaf does not have to be a CA certificate. The certificate served by self-signed.badssl.com carries CA:FALSE and still names itself as its own issuer. CA:FALSE describes what this certificate may sign next, not who signed it.
Sign: Return code 19 is read as self-signed and the team reissues the leaf.Cause: Code 19 means the chain terminates at a root the local store does not trust. Reissuing the leaf from the same private CA changes nothing. The fix belongs on the trust store or on the chain the server sends.
Sign: The s_client command never returns and the terminal appears to hang.Cause: s_client holds the connection open and reads stdin so you can type a request by hand. Without </dev/null it waits until the server drops the idle connection.
Sign: A host that fails on the build agent passes on a developer laptop.Cause: The laptop holds the certificate or its root from an earlier local setup, so the same bytes verify there. Compare verify codes from both machines instead of comparing whether the page loads.

What to check next

FAQ

Can a self-signed certificate still be valid?

It is valid in the sense that the signature verifies and the dates are current. It is not trusted, because no party outside the server vouches for it. Clients reject it unless that exact certificate was installed in their trust store first.

Is a self-signed certificate the same as an untrusted root?

No. A self-signed leaf has no issuer above it and returns code 18. A leaf from a private CA has a real chain and returns code 19. The two need different fixes, which is why step 4 is on this page.

Why does an internal service work in the browser but fail in tests?

Browsers on managed machines usually carry the internal root through device policy. A test container starts with the public root store only. Point the test client at the same root file the browser trusts, or the run keeps failing on trust rather than on your code.

Does a self-signed certificate encrypt traffic?

Yes. The key exchange works and the bytes on the wire are encrypted. What is missing is proof of who holds the key, so anyone in the network path can present a self-signed certificate of their own and the client cannot tell the two apart.

Verified

Verified by Maks Vernyopenssl 3.1.1

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic5 minpublished updated Maks Verny