Self-signed vs CA-signed is a question about who vouches for the binding between a public key and a name. A self-signed certificate is signed with its own key, so it proves only that its holder has that key; a client trusts it only if it was told to trust that exact certificate. A CA-signed certificate is signed by a certificate authority whose root the client already trusts: a public CA for the internet, or a private CA that you run and distribute yourself.
At a glance
| Self-signed | Private CA | Public CA | |
|---|---|---|---|
| Who signs the leaf | The leaf's own key | Your root or intermediate | A CA in the browser and OS root stores |
| How clients learn to trust it | Each client pins or installs this exact certificate | Install one root (MDM, configuration management, container image) | Already trusted by default |
| Rotating the leaf | Every client must be updated | Transparent to clients | Transparent to clients |
| Revocation | None; remove it from every client | Your own CRL, OCSP or short lifetimes | CRLs published by the CA |
| Names it can carry | Anything | Anything the root is constrained to | Public domain names and public IPs only |
| Lifetime limits | None from browsers; Apple platforms cap TLS certificates issued since July 2019 at 825 days | Same 825-day cap on Apple platforms | 200 days now, 47 days from March 2029 |
| Certificate Transparency | Not logged | Not logged | Logged publicly, including every hostname |
| Cost and effort | None up front; high per rotation | Running the CA, protecting its key | Free with ACME; needs DNS or HTTP validation |
| Typical tools | openssl req -x509, mkcert for local development | step-ca, HashiCorp Vault PKI, cloud private CAs, cert-manager | Let's Encrypt and other ACME CAs |
How clients decide to trust a certificate
Path validation (RFC 5280 §6) builds a chain from the server's certificate up to a trust anchor in the client's root store, checking each signature, validity period, basic constraints, key usage and name constraints on the way. A self-signed certificate has the same issuer and subject and is signed by its own key, so the only chain possible is the certificate itself. It validates only if that certificate is itself a trust anchor for the client, which is exactly what "pinning" or "installing" a self-signed certificate means.
A CA-signed leaf is signed by an intermediate CA, which in turn chains to a root CA. The client never needs to know the leaf in advance; it needs only the root. That indirection is what makes rotation, revocation and automation possible.
The script below, run with OpenSSL 3.6.5, creates a self-signed certificate, a private root limited to names under .internal, and two leaf certificates signed by that root.
#!/bin/sh
dir=$(mktemp -d)
cd "$dir" || exit 1
P256="-newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc"
# 1. Self-signed server certificate
openssl req -x509 $P256 -keyout self.key -out self.pem -days 30 \
-subj "/CN=dev.internal" \
-addext "subjectAltName=DNS:dev.internal,IP:127.0.0.1" \
-addext "basicConstraints=critical,CA:FALSE" 2>/dev/null
openssl verify self.pem
openssl verify -CAfile self.pem self.pem
# 2. Private root CA limited to .internal names
openssl req -x509 $P256 -keyout ca.key -out ca.pem -days 3650 \
-subj "/CN=Example Internal Root" \
-addext "keyUsage=critical,keyCertSign,cRLSign" \
-addext "nameConstraints=critical,permitted;DNS:.internal" 2>/dev/null
# 3. Two server certificates signed by that CA
for name in api.internal www.example.com; do
openssl req -x509 $P256 -CA ca.pem -CAkey ca.key -keyout "$name.key" \
-out "$name.pem" -days 90 -subj "/CN=$name" \
-addext "subjectAltName=DNS:$name" \
-addext "basicConstraints=critical,CA:FALSE" \
-addext "extendedKeyUsage=serverAuth" 2>/dev/null
done
openssl verify api.internal.pem
openssl verify -CAfile ca.pem api.internal.pem
openssl verify -CAfile ca.pem www.example.com.pem
cd / && rm -rf "$dir"
CN=dev.internal
error 18 at 0 depth lookup: self-signed certificate
error self.pem: verification failed
self.pem: OK
CN=api.internal
error 20 at 0 depth lookup: unable to get local issuer certificate
error api.internal.pem: verification failed
api.internal.pem: OK
CN=www.example.com
error 47 at 0 depth lookup: permitted subtree violation
error www.example.com.pem: verification failed
The self-signed certificate verifies only when it is passed as its own trust anchor. The private CA's leaf verifies once the root is trusted, and the root's name constraints stop it from vouching for www.example.com, which limits the damage if the CA key is ever misused. Note that openssl req -x509 marks certificates as CAs by default through its configuration file; the explicit CA:FALSE keeps the self-signed server certificate from being usable as an issuer.
What clients say when they do not trust it
We pointed five clients at a local server presenting each certificate without adding anything to their trust stores (20 October 2026, macOS).
| Client | Self-signed certificate | Leaf from an untrusted private CA |
|---|---|---|
| Node.js 26.10 | DEPTH_ZERO_SELF_SIGNED_CERT | UNABLE_TO_VERIFY_LEAF_SIGNATURE |
| Python 3.14 (OpenSSL 3.6.5) | certificate verify failed: self-signed certificate | certificate verify failed: unable to get local issuer certificate |
| Go 1.25 (macOS verifier) | x509: certificate signed by unknown authority | x509: “localhost” certificate is not trusted |
| curl 8.7.1 (LibreSSL) | (60) SSL certificate problem: self signed certificate | (60) SSL certificate problem: unable to get local issuer certificate |
OpenSSL 3.6.5 s_client | Verify return code: 18 (self-signed certificate) | Verify return code: 20 (unable to get local issuer certificate) |
Browsers show NET::ERR_CERT_AUTHORITY_INVALID (Chrome) or MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT (Firefox). The fix is never to disable verification (NODE_TLS_REJECT_UNAUTHORIZED=0, verify=False, InsecureSkipVerify, curl -k): it removes authentication for every connection the process makes. Instead, give the client the certificate or root it should trust (ca: in Node.js, cafile= in Python, a custom RootCAs pool in Go, --cacert in curl), or install the root in the operating system store. A certificate without a DNS name in the Subject Alternative Name extension fails differently: Chrome stopped matching the common name in version 58 (2017), and Apple platforms since iOS 13 and macOS 10.15.
The middle path: a private CA
A private CA keeps the main benefit of self-signing (no outside party, any internal name, no public logging) while removing its main cost (redistributing trust at every rotation). Practical rules:
- Keep the root offline and issue from an intermediate; a compromised intermediate can be replaced without touching every client.
- Constrain names to your internal zones, as in the demo, and keep
pathlenand key usage tight. - Automate leaves: step-ca, Vault PKI and cert-manager can issue through ACME or their own APIs, so leaves can live for hours or days and revocation becomes unnecessary.
- Respect client rules anyway: Apple platforms require a DNS name in the SAN, the
serverAuthextended key usage and at most 825 days of validity for TLS certificates issued since 1 July 2019, also for private roots; the 398-day limit applies only to publicly trusted roots. - Plan root distribution: operating system stores, Java keystores, container images, browsers with their own stores (Firefox) and software that ships its own bundle (the
certifipackage behind Python'srequests, and Node.js, which uses its built-in Mozilla bundle unless started with--use-system-ca) each need the root.
Where a public CA cannot help
Public CAs may not issue certificates containing internal names or reserved IP addresses (Baseline Requirements §4.2.2). Names such as printer.local, single-label hosts, 10.0.0.5, or anything under .internal, which ICANN permanently reserved for private use on 29 July 2024, can therefore only come from a private CA or a self-signed certificate. Internal hosts with public names (for example db.corp.example.com) can get public certificates through the DNS-01 challenge without being reachable from the internet, but every name then appears in public Certificate Transparency logs, which reveals your internal topology to anyone searching them.
When to choose which
- Public websites, APIs and mail servers: public CA with ACME.
- Internal services, service meshes, mTLS between workloads: private CA with short-lived leaves.
- Local development: a local development CA such as
mkcert, which installs its root in your own trust stores, rather than self-signed certificates that every tool rejects differently. - Devices and appliances on first boot: a self-signed certificate whose fingerprint the user or the provisioning system verifies once, followed by enrolment in a private CA.
- Test fixtures: self-signed certificates generated by the test itself and passed explicitly to the client under test.
- Partners and B2B integrations over mTLS: a private CA dedicated to that relationship, never your general internal root.
Migration notes
- Inventory self-signed certificates in use: scan internal endpoints and collect the certificates clients pin; the certificate decoder shows issuer, SANs and expiry.
- Stand up a private CA with an offline root and an issuing intermediate, constrained to your internal domains; generate requests with the CSR generator.
- Distribute the root before switching any server, then replace leaves service by service; the chain builder confirms each server sends its intermediate.
- Remove insecure workarounds (
-k,verify=False,InsecureSkipVerify) once clients trust the root, and search the codebase so they do not come back. - Move public-facing names to a public CA with ACME, and keep pinning (if any) on keys rather than certificates; the SPKI pin generator computes pins that survive renewal with the same key.
References
- RFC 5280 §6, certification path validation and §4.2.1.10, name constraints
- CA/Browser Forum Baseline Requirements §4.2.2
- Apple: requirements for trusted certificates in iOS 13 and macOS 10.15
- Apple: about upcoming limits on trusted certificates (398 days)
- ICANN Board resolution 2024.07.29.06 reserving .INTERNAL
- RFC 9162, Certificate Transparency 2.0
- RFC 8555, ACME