Creating TLS Certificates: private keys, CSRs, self-signed certs, your own CA and Let's Encrypt
openssl req -new -newkey rsa:3072 -nodes -keyout server.key -out server.csr
A TLS certificate (often called an SSL certificate, and technically an X.509 certificate) proves that a server really is the name it claims to be, and lets clients set up an encrypted
connection to it (the padlock in your browser, HTTPS, secure mail, LDAPS, database and API traffic). This guide shows how to create the pieces yourself with OpenSSL:
the private key, the certificate signing request (CSR), a self-signed certificate for testing, your own certificate authority (CA) for an internal network, and free trusted certificates
from Let's Encrypt. It also covers converting formats, installing certificates, checking them, renewing, and fixing the usual errors.
This is NOT the same as SSH keys (see the "Creating SSH Keys" guide): TLS certificates are for websites and services, and are signed by a CA that clients trust.
Examples use the name www.example.com and OpenSSL 3.x. Commands work on OpenSSL 1.1.1 and newer unless noted. Check yours with: openssl version
The moving parts (vocabulary)
Private key Secret file that proves ownership of the certificate. Stays on the server. NEVER share it or send it to a CA
Public key The matching half. It is INSIDE the certificate and safe to share
CSR Certificate Signing Request: a file containing your public key and the names you want, signed with your private key. You send it to a CA. It contains no secrets
Certificate The public key plus names (and validity dates) signed by a CA. Anyone can see it. Also called the "leaf", "server" or "end-entity" certificate
CA Certificate Authority: signs certificates so that clients trust them. Public CAs (Let's Encrypt, DigiCert, Sectigo) are trusted by browsers. A private CA (yours) is trusted only where you install it
Root CA The top of the trust chain. Its certificate is pre-installed in browsers and operating systems (or in your own trust store)
Intermediate CA A CA signed by the root, which signs the actual server certificates, so the root key can stay offline
Chain Your certificate + the intermediate(s). Servers must send the whole chain (except the root). "fullchain.pem" is your cert followed by the intermediates
SAN Subject Alternative Name: the list of DNS names and IP addresses the certificate is valid for. MODERN CLIENTS IGNORE THE COMMON NAME (CN) AND USE THE SAN ONLY
Common flow: make a private key > make a CSR (with the names) > a CA checks you own the names > the CA signs it > you install the certificate and the chain next to your private key.
Trust: a client accepts a certificate if it chains up to a root CA it trusts, the name matches, the dates are valid, and it has not been revoked.
Which kind of certificate do you need?
Public website or service that anyone visits A certificate from a public CA. Free and automatic: Let's Encrypt (see below). Or a paid CA if you need OV/EV, warranties or special support
Internal servers, labs, private networks Your own private CA (below), or an internal PKI (Active Directory Certificate Services, HashiCorp Vault, step-ca, EJBCA). Clients must trust your root
Quick test on your own laptop A self-signed certificate, or mkcert (which makes a local CA and installs it for you)
A machine-to-machine login (mutual TLS) Client certificates from your private CA (see "Client certificates")
Wildcard (*.example.com) Let's Encrypt (DNS challenge) or a paid CA. Covers ONE level of subdomains only (not example.com itself, and not a.b.example.com)
Validation levels: DV (domain validated: proves control of the name: what Let's Encrypt gives), OV (organization validated), EV (extended validation: the browser no longer shows a special bar). For encryption they are equal.
Certificate lifetimes are getting SHORTER: publicly trusted certificates are limited to about a year, and the industry is moving toward roughly 200 days and then much less over the next few years. Let's Encrypt uses 90 days.
Plan for AUTOMATIC renewal from day one. Check your CA's current rules.
Choose a key type
ECDSA P-256 (prime256v1) Small and fast, and supported by all modern browsers and servers. A great default for new servers
RSA 2048 The minimum still accepted. Works everywhere, including very old clients
RSA 3072 / 4096 Stronger and slower. Fine for CAs and long-lived keys. 3072 is a good server choice when you need RSA
Ed25519 Great for SSH, but not widely accepted for web server certificates from public CAs. Avoid for TLS certificates for now
Signature hash: SHA-256 or better (SHA-1 and MD5 are forbidden). OpenSSL uses SHA-256 by default in current versions.
If you need to support very old systems (Windows XP, old Java 6/7 or Android 2.x), use RSA. Otherwise ECDSA P-256 is excellent.
Step 1: create a private key
ECDSA (recommended for most new servers):
openssl ecparam -name prime256v1 -genkey -noout -out server.key # a traditional "EC PRIVATE KEY" file
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out server.key # the newer command: produces a "PRIVATE KEY" (PKCS#8) file
RSA:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out server.key
openssl genrsa -out server.key 3072 # the older command (output format differs by OpenSSL version)
Protect the key file immediately:
chmod 600 server.key # only the owner can read it
Encrypt the key with a passphrase (good for CA keys, but a web server then asks for the passphrase at every start, so servers usually use unencrypted keys with tight file permissions):
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -aes-256-cbc -out ca.key
openssl pkey -in server.key -aes256 -out server-enc.key # add a passphrase to an existing key
openssl pkey -in server-enc.key -out server.key # REMOVE the passphrase (asks for it)
Inspect a key: openssl pkey -in server.key -noout -text | head openssl pkey -in server.key -pubout (print the public half)
Never generate keys on a machine you do not control, and never email or paste a private key. Generate it on the server that will use it when possible.
Step 2: create a CSR (certificate signing request)
With a config file (clear and repeatable). Save as server.cnf :
[req]
default_bits = 3072
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
C = US
ST = Texas
L = Austin
O = Example Inc
OU = IT
CN = www.example.com
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com
IP.1 = 203.0.113.10
Then create the key and the CSR together (or use an existing key):
openssl req -new -newkey rsa:3072 -nodes -keyout server.key -out server.csr -config server.cnf # new key + CSR. -nodes = do NOT encrypt the key (no passphrase)
openssl req -new -key server.key -out server.csr -config server.cnf # CSR from an EXISTING key
Or in one line, without a config file (OpenSSL 1.1.1+ supports -addext):
openssl req -new -key server.key -out server.csr -subj "/C=US/ST=Texas/L=Austin/O=Example Inc/CN=www.example.com" -addext "subjectAltName=DNS:www.example.com,DNS:example.com"
Check the CSR BEFORE you send it (are the names and key size right?):
openssl req -in server.csr -noout -text | less
openssl req -in server.csr -noout -verify (verify the signature: prints "verify OK")
openssl req -in server.csr -noout -subject -ext subjectAltName
Rules of thumb: use the FULL fully qualified domain name in the CN, list EVERY name in the SAN (including the CN name, since browsers only read the SAN), and only put names you really need on the certificate.
Public CAs ignore fields like O and OU in DV certificates, and Let's Encrypt does not use a CSR file at all: certbot creates it for you.
Send the CSR file (never the key) to your CA and follow their validation process. They return the certificate and the intermediate chain.
Step 3: self-signed certificate (testing only)
A certificate that signs itself. Browsers will warn (nobody trusts it), but it is fine for testing, labs, and internal tools where you install it manually.
One command: key + certificate valid for one year, with SAN (OpenSSL 1.1.1+):
openssl req -x509 -newkey rsa:3072 -sha256 -days 365 -nodes -keyout selfsigned.key -out selfsigned.crt \
-subj "/CN=test.example.internal" -addext "subjectAltName=DNS:test.example.internal,DNS:localhost,IP:127.0.0.1"
(ECDSA version: -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 )
chmod 600 selfsigned.key
Or make it from an existing key and CSR:
openssl x509 -req -in server.csr -signkey server.key -sha256 -days 365 -out selfsigned.crt -extfile <(printf "subjectAltName=DNS:test.example.internal,DNS:localhost")
Use it: curl --cacert selfsigned.crt https://test.example.internal/ (the self-signed cert acts as its own CA) or import selfsigned.crt into the client's trust store (see "Trusting a private CA").
Do not use self-signed certificates in production: users learn to click through warnings, which is dangerous. Use Let's Encrypt or a private CA instead.
Quick local development alternative: mkcert
sudo apt install mkcert libnss3-tools (or brew install mkcert)
mkcert -install # creates a local CA and adds it to your system and browser trust stores (only on THIS machine)
mkcert example.test "*.example.test" localhost 127.0.0.1 ::1 # makes example.test+3.pem and example.test+3-key.pem, trusted by your browser with no warnings
Never share the CA key it creates (rootCA-key.pem under mkcert -CAROOT ), and never use mkcert certificates outside development.
Step 4: your own private CA (for internal servers)
A private CA lets every internal server get a proper certificate, and you trust ONE root certificate on your devices instead of clicking through warnings on each server.
Keep the CA private key extremely safe: offline if possible, encrypted, backed up. Anyone who has it can create certificates your devices will trust for ANY name.
A. Create the root CA (once):
mkdir -p ~/pki/root && cd ~/pki/root
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -aes-256-cbc -out ca.key # asks for a passphrase: keep it in a password manager
chmod 600 ca.key
openssl req -x509 -new -key ca.key -sha256 -days 3650 -out ca.crt \
-subj "/C=US/O=Example Inc/CN=Example Inc Internal Root CA" \
-addext "basicConstraints=critical,CA:TRUE" -addext "keyUsage=critical,keyCertSign,cRLSign" -addext "subjectKeyIdentifier=hash"
openssl x509 -in ca.crt -noout -text | grep -A2 -E "Basic Constraints|Key Usage" # check
B. Create and sign a server certificate:
1) Server key and CSR (as in Steps 1 and 2), for example server.key and server.csr with the SANs
2) An extension file server.ext that says what the certificate may be used for:
authorityKeyIdentifier = keyid,issuer
basicConstraints = CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.internal
DNS.2 = example.internal
IP.1 = 10.0.0.5
3) Sign it:
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 397 -sha256 -extfile server.ext
4) Build the chain file that servers send (leaf first, then intermediates. For a root-signed cert the root itself is optional):
cat server.crt ca.crt > fullchain.pem
Use a validity of about 1 year or less (many clients refuse longer for TLS server certificates). Automate renewal.
C. Trust the root on your clients (once per device): see "Trusting a private CA" below. Distribute ca.crt (the CERTIFICATE, never ca.key) by configuration management, group policy, or MDM.
D. Better structure for a real PKI: root CA (offline) > intermediate CA (online, signs server certs) > servers. Create the intermediate the same way as a server certificate but with:
basicConstraints = critical,CA:TRUE,pathlen:0 keyUsage = critical,keyCertSign,cRLSign
and send servers server.crt + intermediate.crt in the chain. If the intermediate key leaks you revoke and replace it without touching the root that is installed on every device.
E. Track what you issue with "openssl ca" (a serial number database, revocation, CRLs). A minimal setup in ~/pki/ca:
mkdir -p ~/pki/ca/{certs,newcerts,crl,private} && cd ~/pki/ca && touch index.txt && echo 1000 > serial && echo 1000 > crlnumber
ca.cnf:
[ca]
default_ca = CA_default
[CA_default]
dir = .
certs = $dir/certs
new_certs_dir = $dir/newcerts
database = $dir/index.txt
serial = $dir/serial
crlnumber = $dir/crlnumber
certificate = $dir/ca.crt
private_key = $dir/private/ca.key
default_md = sha256
default_days = 397
policy = policy_any
copy_extensions = copy # copy the SAN from the CSR into the certificate (only do this if you trust who makes the CSRs, and review them)
unique_subject = no
[policy_any]
commonName = supplied
[server_ext]
basicConstraints = CA:FALSE
keyUsage = critical,digitalSignature,keyEncipherment
extendedKeyUsage = serverAuth
Sign: openssl ca -config ca.cnf -in server.csr -out server.crt -extensions server_ext -notext -batch
Revoke: openssl ca -config ca.cnf -revoke newcerts/1000.pem then openssl ca -config ca.cnf -gencrl -out crl/ca.crl
List: cat index.txt (V = valid, R = revoked, E = expired)
Publish the CRL where clients can fetch it (and add a crlDistributionPoints extension to issued certificates), or use short-lived certificates instead of revocation.
Ready-made private CA tools that do all this for you (with an API and automatic renewal): step-ca (Smallstep), HashiCorp Vault PKI, cfssl, EasyRSA (mainly for VPN), and Active Directory Certificate Services on Windows networks.
Step 5: free trusted certificates with Let's Encrypt
Let's Encrypt is a public CA that issues free DV certificates automatically using the ACME protocol. Certificates are valid for about 90 days and renew by themselves. Your server must be reachable from the internet (for HTTP validation), or you can prove control via DNS.
Requirements: a public domain name that points to your server, and port 80 reachable (for HTTP-01) or DNS API access (for DNS-01). It does not issue certificates for private IPs or internal-only names.
Install certbot:
sudo apt install certbot python3-certbot-nginx python3-certbot-apache # Debian/Ubuntu (choose the plugin you need). Snap users: sudo snap install --classic certbot
sudo dnf install certbot python3-certbot-nginx # RHEL family (EPEL may be needed)
Easiest: let certbot edit your web server for you:
sudo certbot --nginx -d example.com -d www.example.com # or --apache
It gets the certificate, edits the config to use it, and sets up automatic renewal. Test the result: https://example.com and sudo certbot certificates
Other ways to get a certificate (you install it yourself):
sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com # proves control by placing a file in the web root (the server keeps running)
sudo certbot certonly --standalone -d mail.example.com # certbot runs its own tiny web server on port 80 (stop other things using port 80 first). Good for mail servers and other non-web services
sudo certbot certonly --nginx -d example.com # uses the nginx plugin but leaves your config alone
Wildcard certificates (*.example.com) require DNS validation (a TXT record):
sudo certbot certonly --manual --preferred-challenges dns -d "*.example.com" -d example.com # you add the TXT record by hand: it CANNOT renew automatically
sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cloudflare.ini -d "*.example.com" -d example.com # automatic with a DNS plugin (also route53, azure, google, digitalocean, ...)
(chmod 600 the credentials file, and use a token limited to editing DNS for that zone)
Where the files go: /etc/letsencrypt/live/example.com/
fullchain.pem your certificate + the intermediate(s) >>> use this for ssl_certificate in nginx and SSLCertificateFile in Apache
privkey.pem the private key >>> ssl_certificate_key / SSLCertificateKeyFile
cert.pem your certificate only chain.pem the intermediate(s) only
These are symlinks to the newest files in ../../archive/. Always point your services at the "live" paths.
Renewal:
sudo certbot renew --dry-run # test renewal without changing anything (do this once after setup)
sudo systemctl list-timers | grep certbot # the package installs a systemd timer (or a cron job in /etc/cron.d/certbot) that runs twice a day
sudo certbot renew # renew anything within 30 days of expiry
Reload services after renewal with a deploy hook (the service must pick up the new file):
sudo certbot renew --deploy-hook "systemctl reload nginx" or put scripts in /etc/letsencrypt/renewal-hooks/deploy/ (for example systemctl reload postfix dovecot )
Certificate list and status: sudo certbot certificates Delete one: sudo certbot delete --cert-name example.com Revoke: sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem
Practice first with the staging server (no rate limits, untrusted certificate): sudo certbot certonly --staging --webroot -w /var/www/html -d example.com
Limits to know: rate limits on certificates per domain per week and failed validations per hour (see the Let's Encrypt documentation). Use --dry-run and --staging while testing.
Alternatives: acme.sh (a shell script client: acme.sh --issue -d example.com -w /var/www/html), lego, Caddy and Traefik (get certificates automatically), cloud-managed certificates (AWS Certificate Manager, Azure App Service managed certificates and Key Vault, Google-managed certificates). They renew automatically.
Allowed access for services: the private key is readable by root only. If a non-root service needs it, copy it with a deploy hook to a location with the right group and mode 640, or use a group that both certbot and the service share (Debian has the ssl-cert group).
Buying a certificate from a commercial CA
1. Generate the private key and CSR yourself (Steps 1 and 2). Keep the key
2. Paste or upload the CSR. Choose the validation type (DV, OV or EV) and the validation method (email, DNS record, or HTTP file)
3. Complete validation, then download: your certificate and the intermediate chain (sometimes called the "CA bundle")
4. Combine them: cat www_example_com.crt intermediate.crt > fullchain.pem (your certificate FIRST, then the intermediate(s), and not the root)
5. Install and test (see below). Put the expiry date in your calendar, or better, automate with the CA's ACME service if it has one
If the CA sends a root certificate too, do not put the root in the chain your server sends: clients already have it.
File formats and conversions
PEM Text format with -----BEGIN CERTIFICATE----- lines (base64). Used by Linux, nginx, Apache. Extensions: .pem .crt .cer .key
DER Binary version of the same data. Some Java and Windows tools. Extensions: .der .cer
PKCS#12 Binary file (.p12 or .pfx) that bundles the private key, certificate and chain, usually protected with a password. Used by Windows, Java, browsers, and many appliances
JKS/PKCS12 Java keystores (.jks, or modern .p12)
PKCS#7 .p7b bundles of certificates (no private key). Common with Windows and Java for chains
Convert:
openssl x509 -in cert.pem -outform DER -out cert.der # PEM to DER
openssl x509 -in cert.der -inform DER -out cert.pem # DER to PEM
openssl pkcs12 -export -out bundle.p12 -inkey server.key -in server.crt -certfile chain.pem -name "www.example.com" # PEM parts to PKCS#12 (asks for an export password)
openssl pkcs12 -in bundle.p12 -nodes -out everything.pem # PKCS#12 back to PEM (then split the sections into key, cert and chain)
openssl pkcs12 -in bundle.p12 -nokeys -clcerts -out cert.pem # only the certificate
openssl pkcs12 -in bundle.p12 -nocerts -nodes -out server.key # only the private key
openssl crl2pkcs7 -nocrl -certfile chain.pem -out chain.p7b # a PKCS#7 bundle
openssl pkcs7 -in chain.p7b -print_certs -out chain.pem # PKCS#7 to PEM
openssl pkcs8 -topk8 -nocrypt -in old_rsa.key -out server_pkcs8.key # traditional RSA key to PKCS#8
Java keystore:
keytool -importkeystore -srckeystore bundle.p12 -srcstoretype PKCS12 -destkeystore keystore.jks -deststoretype JKS # PKCS#12 to a JKS keystore (or keep the PKCS#12 file: modern Java uses it directly)
keytool -list -v -keystore keystore.jks
keytool -importcert -alias myca -file ca.crt -keystore truststore.jks # trust a private CA in Java
OpenSSL 3 and old PKCS#12 files: if you see "unsupported" or error:0308010C, add -legacy to the openssl pkcs12 command (and consider recreating the file with modern algorithms).
Split a file with several certificates into separate files: csplit -z -f cert- -b '%02d.pem' fullchain.pem '/-----BEGIN CERTIFICATE-----/' '{*}'
The order matters in a chain file: your certificate first, then the intermediate that signed it, then the next intermediate (and not the root).
Check a certificate (do this before and after you install it)
openssl x509 -in server.crt -noout -text | less # everything: issuer, subject, validity, SAN, key usage, signature algorithm
openssl x509 -in server.crt -noout -subject -issuer -serial -dates # the short version
openssl x509 -in server.crt -noout -ext subjectAltName # the names it is valid for (OpenSSL 1.1.1+)
openssl x509 -in server.crt -noout -fingerprint -sha256 # a fingerprint to compare
openssl x509 -in server.crt -noout -checkend 2592000 && echo "OK for 30 days" || echo "EXPIRES WITHIN 30 DAYS" # non-zero exit if it expires within that many seconds
Does the key match the certificate? (the three hashes must be identical)
openssl x509 -in server.crt -noout -pubkey | openssl sha256
openssl pkey -in server.key -pubout | openssl sha256
openssl req -in server.csr -noout -pubkey | openssl sha256
A mismatch causes "key values mismatch" when starting the server: you are using the wrong key, or the wrong certificate.
Does the chain verify?
openssl verify -CAfile ca.crt server.crt # leaf signed directly by a root
openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt # with an intermediate
openssl verify -CApath /etc/ssl/certs -untrusted chain.pem server.crt # against the system trust store
Test a LIVE server (what clients really see):
openssl s_client -connect www.example.com:443 -servername www.example.com -showcerts </dev/null # the chain the server sends (-servername sets SNI, essential on shared hosts)
echo | openssl s_client -connect www.example.com:443 -servername www.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates
openssl s_client -connect mail.example.com:587 -starttls smtp -servername mail.example.com </dev/null # STARTTLS ports (smtp, imap, pop3, ftp)
openssl s_client -connect www.example.com:443 -tls1_2 </dev/null # test a specific TLS version (-tls1_3, -tls1_2)
look for "Verification: OK" (newer OpenSSL) or "Verify return code: 0 (ok)" and the lines "depth=0/1/2" that show each certificate in the chain
curl -vI https://www.example.com 2>&1 | grep -E "subject|issuer|expire|SSL certificate" # what curl sees
nmap --script ssl-cert,ssl-enum-ciphers -p 443 www.example.com # certificate and cipher summary
Online: SSL Labs (ssllabs.com/ssltest) grades a public server thoroughly.
Watch expiry for many hosts (cron or monitoring):
for h in www.example.com mail.example.com; do printf '%s ' "$h"; echo | openssl s_client -connect "$h:443" -servername "$h" 2>/dev/null | openssl x509 -noout -enddate; done
Prometheus blackbox exporter, Nagios/Icinga check_http -C 30, Zabbix, or a hosted monitor can alert 30 and 7 days before expiry. Do not rely on email reminders from the CA alone.
Install the certificate
nginx (in the server block):
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
sudo nginx -t && sudo systemctl reload nginx
Apache:
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol -all +TLSv1.2 +TLSv1.3
sudo apachectl configtest && sudo systemctl reload apache2 (a2enmod ssl first on Debian/Ubuntu)
HAProxy: one file with the certificate, the chain and the KEY: cat fullchain.pem privkey.pem > /etc/haproxy/certs/example.com.pem then bind *:443 ssl crt /etc/haproxy/certs/
Postfix and Dovecot: see the Postfix guide (smtpd_tls_cert_file / smtpd_tls_key_file) and its Dovecot section (ssl_cert / ssl_key). Reload them after every renewal.
Kubernetes: kubectl create secret tls example-tls --cert=fullchain.pem --key=server.key -n prod (or use cert-manager to issue and renew certificates automatically)
Windows/IIS: import the .pfx (PKCS#12) into the machine's certificate store, then bind it to the site.
File permissions: keys 600 (owner root, or the service's group with 640), certificates 644, and the folder 755 or 750. Never make a private key world-readable.
Deploy the full chain, not just the leaf. A missing intermediate makes the site work in some browsers (which fetch it) and fail in curl, Java, mobile apps and monitoring.
Trusting a private CA on clients (so your internal certificates work)
Give every device the ROOT CA certificate (ca.crt, never the key):
Debian/Ubuntu: sudo cp ca.crt /usr/local/share/ca-certificates/example-root-ca.crt && sudo update-ca-certificates (the file must end in .crt)
RHEL family: sudo cp ca.crt /etc/pki/ca-trust/source/anchors/example-root-ca.crt && sudo update-ca-trust
Windows: certutil -addstore -f "ROOT" ca.crt (an administrator prompt). For a whole domain use Group Policy: Computer Configuration > Windows Settings > Security Settings > Public Key Policies > Trusted Root Certification Authorities
macOS: sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt
Firefox: has its own store (Settings > Privacy & Security > Certificates > View Certificates > Authorities > Import), or enable enterprise roots (security.enterprise_roots.enabled)
Java: keytool -importcert -trustcacerts -alias example-ca -file ca.crt -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit (or keep a private truststore)
Python/curl/Node one-off: REQUESTS_CA_BUNDLE=/path/ca.crt python app.py curl --cacert ca.crt https://host NODE_EXTRA_CA_CERTS=/path/ca.crt node app.js
Containers: copy ca.crt into the image and run update-ca-certificates in the Dockerfile, or mount the bundle
Only trust roots you created or that your organization gave you. A trusted root can vouch for any website on that device.
Client certificates (mutual TLS: the server checks the client too)
Useful for APIs, VPNs, service-to-service traffic and admin portals.
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out client.key
openssl req -new -key client.key -out client.csr -subj "/CN=alice@example.com"
client.ext: basicConstraints = CA:FALSE ; keyUsage = digitalSignature ; extendedKeyUsage = clientAuth
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256 -extfile client.ext
openssl pkcs12 -export -inkey client.key -in client.crt -certfile ca.crt -out alice.p12 (import this into the browser or send it securely to the user)
nginx server side: ssl_client_certificate /etc/nginx/ca.crt; ssl_verify_client on; Test: curl --cert client.crt --key client.key https://api.example.com/
Use extendedKeyUsage = clientAuth for client certificates and serverAuth for server certificates. Keep the two roles separate.
Renewing and replacing
- Automatic (best): Let's Encrypt/ACME clients renew before expiry. Test with certbot renew --dry-run, and make sure services reload
- Manual: create a NEW CSR (reuse the old key only if you must; a new key is better), have it signed, install the new certificate and chain, reload, and verify with s_client
- Renew at about two thirds of the lifetime (for 90-day certificates, at 60 days)
- Keep old certificates and keys until the new one works. After a suspected key compromise, do NOT reuse the key: create a new key and CSR, then revoke the old certificate with your CA
- After renewal check the live site (s_client or a browser) rather than trusting the file on disk: a service that was not reloaded still serves the old certificate
- Keep an inventory of every certificate: name, where it is installed, who owns it, the expiry date, and how it renews. Expired certificates are one of the most common causes of outages
Security best practices
[ ] Keep private keys private: mode 600, never in git, chat, tickets or shared drives. Generate them on the server that uses them
[ ] Use SANs for every name, ECDSA P-256 or RSA 3072+, SHA-256 or better, and TLS 1.2 or 1.3 only on servers
[ ] The CA private key is offline and encrypted, and the root has a long life while servers get short ones
[ ] Automate issuance and renewal. Short lifetimes limit the damage of a leaked key
[ ] Send the full chain (leaf + intermediates). Never send the root or the private key
[ ] Use a private CA for internal names (public CAs cannot issue for them). Do not install a random third-party root
[ ] Monitor expiry, and watch Certificate Transparency logs for unexpected certificates issued for your domains (crt.sh)
[ ] Consider CAA DNS records to say which CAs may issue for your domain: example.com. CAA 0 issue "letsencrypt.org"
[ ] Enable HSTS only once HTTPS works everywhere, and consider OCSP stapling. Use Mozilla's SSL Configuration Generator for current server settings
[ ] Rotate and revoke: if a key may have leaked, replace the certificate immediately
Common problems and errors
Browser: "Your connection is not private" / NET::ERR_CERT_AUTHORITY_INVALID The certificate is self-signed or from a CA the device does not trust. Install the private root, or use a public certificate
NET::ERR_CERT_COMMON_NAME_INVALID / "does not match" The name in the address bar is not in the certificate's SAN. Reissue with the right names (SAN!), or use the exact name the certificate covers
NET::ERR_CERT_DATE_INVALID / "certificate has expired" Expired (or the client's clock is wrong: check the date and time on both sides). Renew and reload the service
Works in the browser but curl says "unable to get local issuer certificate" (60) The server does not send the intermediate. Serve fullchain.pem (leaf + intermediate), not just the leaf
"self signed certificate in certificate chain" / "self-signed certificate" A private or self-signed CA is in the chain and is not trusted by this client. Add the root to the trust store, or pass --cacert
"key values mismatch" / "PEM_read_bio_X509 ... no start line" The key does not match the certificate (compare the hashes above), or the file is not PEM, or has extra text. Check the file with head -1
"Permission denied" reading the key / the service fails to start after renewal The service user cannot read privkey.pem. Fix the group and mode (for example ssl-cert on Debian) or use a deploy hook to copy it with the right permissions
nginx: "SSL: error:...:PEM routines: ..." on reload Wrong file, wrong order, missing chain, or a passphrase-protected key. Run sudo nginx -t for the exact message
The site still shows the old certificate after renewal The service did not reload. Reload it (systemctl reload nginx) and add a deploy hook. Also check that a load balancer or CDN is not serving a cached certificate
Certbot: "Timeout during connect" / "Connection refused" / 404 on the challenge Port 80 is blocked (firewall, security group, or another program), DNS points somewhere else, or the webroot path is wrong. Check with curl from outside
Certbot: "too many certificates already issued" or rate limit Wait, and test with --staging or --dry-run. Check the Let's Encrypt rate-limit documentation
Certbot renew fails silently after a web server change The renewal config in /etc/letsencrypt/renewal/*.conf points at old paths or a stopped authenticator. Fix it and run certbot renew --dry-run
Wildcard certificate cannot renew You used --manual DNS validation. Switch to a DNS plugin or acme.sh with a DNS API
Java "PKIX path building failed" Java does not trust the CA or the chain is incomplete. Import the CA into the truststore, and send the full chain
Windows: certificate imports but "private key missing" Import the .pfx (with the key), not the .cer. Check "Mark this key as exportable" if you need to move it
"unsupported" or "error:0308010C" with PKCS#12 files on OpenSSL 3 Add -legacy to the pkcs12 command, or re-create the bundle with modern algorithms
"bad decrypt" / "bad password read" Wrong passphrase, or the file is not encrypted the way you think. Try openssl pkey -in file -noout
IP address in the certificate does not work The IP must be in the SAN as IP:x.x.x.x (not DNS:). Public CAs rarely issue IP certificates; use a private CA
Old clients cannot connect after "hardening" They lack TLS 1.2, ECDSA support or modern ciphers. Decide whether to support them (RSA certificate, TLS 1.2) or retire them
HSTS or the browser keeps forcing HTTPS after removing the certificate HSTS is cached by the browser. Fix the certificate. Do not turn HSTS on until HTTPS is stable
mkcert certificates fail on another computer The mkcert root is installed only on the computer that created it. Use a real CA for anything shared
Always put every name in the SAN. Browsers and modern clients ignore the CN, so a certificate with only a CN fails.
Serve the full chain (fullchain.pem: your certificate, then the intermediates). A missing intermediate is the most common reason curl or an app fails while the browser works.
For public sites use Let's Encrypt with automatic renewal, then test with certbot renew --dry-run and make sure services reload after renewal.
For internal servers run a private CA (or a tool like step-ca), keep its key offline and encrypted, and trust only the ROOT certificate on your devices.
Keep private keys at mode 600 and out of git. Never send a private key to a CA. Generate keys on the server that uses them.
Check before you install: does the key match the certificate (matching hashes), does the chain verify, and do the names and dates look right?
Test the live server with openssl s_client -connect host:443 -servername host, not just the file on disk.
Use short lifetimes and automation. Track every certificate, and monitor expiry with alerts at 30 and 7 days.
Use self-signed certificates only for tests, and mkcert only for local development.
Certificate rules (maximum lifetimes, validation methods) keep changing. Check your CA's current policy and the Let's Encrypt documentation.