📜 Creating TLS Certificates

Keys, CSRs, self-signed certs, your own CA and Let's Encrypt

Video: Creating TLS Certificates (sysadmin video)
Prefer to watch? This video runs the guide's main commands on a real Ubuntu 24.04 machine, with on-screen captions and no voiceover. Click to play, or watch it on YouTube. It is part of the Services, Storage and Security playlist.
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

Quick reference

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out server.key ; chmod 600 server.key           Key
openssl req -new -key server.key -out server.csr -subj "/CN=www.example.com" -addext "subjectAltName=DNS:www.example.com"       CSR
openssl req -x509 -newkey rsa:3072 -nodes -keyout k.pem -out c.pem -days 365 -subj "/CN=host" -addext "subjectAltName=DNS:host"    Self-signed
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 397 -sha256 -extfile server.ext      Sign with your CA
sudo certbot --nginx -d example.com -d www.example.com ; sudo certbot renew --dry-run                                               Let's Encrypt
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName                                                    Inspect
openssl s_client -connect host:443 -servername host -showcerts </dev/null                                                          Test a live server
openssl verify -CAfile ca.crt server.crt ; openssl x509 -in server.crt -noout -checkend 2592000                                      Verify and check expiry
openssl pkcs12 -export -out bundle.p12 -inkey server.key -in server.crt -certfile chain.pem                                         Bundle for Windows/Java
sudo cp ca.crt /usr/local/share/ca-certificates/ca.crt && sudo update-ca-certificates                                                Trust a private root (Debian/Ubuntu)

Tips

    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.