Skip to main content

How to Create TLS Certificates

Key Concepts and Terminology

  • Private key: Secret key kept on your server. Never share or commit it.
  • CSR (Certificate Signing Request): A request that proves your public key and contains your domain names.
  • CA (Certificate Authority): The entity that issues and signs your TLS certificates. (Let’s Encrypt)
  • Certificate: The CA‑signed document binding your domain name(s) to your public key.
  • Chain / Full chain: Your certificate + intermediate certificates the browser needs to trust it.
  • ACME (Automatic Certificate Management Environment): Protocol Let’s Encrypt uses to automate domain control validation and issuance.
  • ACME Client: Software that implements the ACME protocol (e.g., Certbot) and automates the process of obtaining and renewing TLS certificates.
  • Validation challenges: Ways to prove you control a domain: (HTTP‑01, DNS‑01, TLS‑ALPN‑01).

ℹ️ Modern best practice is to let Certbot automate key generation, CSR creation, validation, issuance, and renewal.


Requirements

  • A registered domain name you control (e.g., example.com).

  • Public DNS records pointing to the server that will serve TLS (A/AAAA, and optionally CNAMEs).

  • Server access (root/sudo) and the ability to open validation ports as needed:

    • HTTP‑01 requires port 80 reachable from the internet.
    • TLS‑ALPN‑01 requires port 443 reachable from the internet.
    • DNS‑01 requires neither 80 nor 443 to be reachable.

    If port 80 is blocked or not desirable, use DNS‑01 or TLS‑ALPN‑01 instead.

  • Certbot (ACME client) installed. Plugins for your web server or DNS provider are optional but helpful.

  • Optional: openssl for inspection or if you want to do manual key/CSR management.

You do not need to create a manual account at Let’s Encrypt; Certbot registers an ACME account automatically on first run.


Choose a Validation Method

  • HTTP‑01 (common): Proves control by serving a token over http://<domain>/.well-known/acme-challenge/.... Must be on port 80.
  • DNS‑01 (wildcards & complex setups): Proves control by creating a TXT record under _acme-challenge.<domain>.
  • TLS‑ALPN‑01 (advanced): Proves control over port 443 using a special ALPN protocol.

Tips

  • Use DNS‑01 for *.example.com wildcard certs.
  • Use HTTP‑01 for most single‑host or simple multi‑host certs.
  • Wildcards match one label only: *.example.com matches api.example.com but not a.b.example.com, and it does not cover the apex example.com (include both if needed).
  • Certbot’s nginx/apache plugins do not implement TLS‑ALPN‑01. If you truly need ALPN validation, use DNS‑01, a standalone run, another ACME client, or a third‑party Certbot plugin (e.g., certbot-ualpn).

1) Issue a certificate (HTTP‑01)

Webroot — uses your existing web server by placing a validation file in its document root. Works with any web server:

How it works: Certbot places a file in the /.well-known/acme-challenge/ directory of your web server, which Let's Encrypt then requests to verify your control over the domain.

sudo certbot certonly \
--webroot -w /var/www/html \
-d example.com -d www.example.com

Nginx plugin (auto‑edits config safely):

How it works: Certbot automatically configures Nginx to serve the validation file.

sudo certbot --nginx -d example.com -d www.example.com

Standalone (no web server running; Certbot binds to port 80):

How it works: Certbot temporarily stops your web server and binds to port 80 to serve the validation file.

sudo systemctl stop nginx || true
sudo certbot certonly --standalone -d example.com
sudo systemctl start nginx || true

2) Wildcard or complex domains (DNS‑01)

Manual TXT record:

sudo certbot certonly \
--manual --preferred-challenges dns \
-d "*.example.com" -d example.com

Follow Certbot’s prompt to add the _acme-challenge TXT record, wait for DNS to propagate, then continue.

If your DNS provider supports a Certbot plugin (e.g., Cloudflare, Route 53), prefer that for fully automated renewals.

3) Files Certbot creates

  • Private key: /etc/letsencrypt/live/<domain>/privkey.pem
  • Leaf certificate: /etc/letsencrypt/live/<domain>/cert.pem
  • Full chain (leaf + intermediates): /etc/letsencrypt/live/<domain>/fullchain.pem
  • Bundle directory: /etc/letsencrypt/archive/<domain>/ (rotating history)

Use fullchain.pem for ssl_certificate and privkey.pem for ssl_certificate_key.

4) Minimal web‑server wiring

Nginx (snippet inside a server { listen 443 ssl; ... } block):

ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

Optional (compatibility): serve both ECDSA and RSA

# Provide ECDSA and RSA certs; nginx will select the best per client
ssl_certificate /etc/letsencrypt/live/example.com-ecdsa/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com-ecdsa/privkey.pem;
ssl_certificate /etc/letsencrypt/live/example.com-rsa/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com-rsa/privkey.pem;

Apache (inside your SSL vhost):

SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

Apache 2.4.8+ accepts fullchain.pem directly for SSLCertificateFile. Older Apaches expected a separate chain file.


Option B (Advanced): Generate Your Own Key & CSR with OpenSSL

Only if you have a specific reason (custom storage/HSM, key algorithm control). Certbot can accept an existing CSR.

ECDSA P‑256 (recommended modern default):

openssl ecparam -genkey -name prime256v1 -noout -out private.key
openssl req -new -key private.key -out request.csr \
-subj "/CN=example.com" \
-addext "subjectAltName=DNS:example.com,DNS:www.example.com"

RSA 2048 (widely compatible):

openssl genrsa -out private.key 2048
openssl req -new -key private.key -out request.csr \
-subj "/CN=example.com" \
-addext "subjectAltName=DNS:example.com,DNS:www.example.com"

Then issue using the CSR (HTTP‑01 example):

sudo certbot certonly --webroot -w /var/www/html \
--csr request.csr --fullchain-path fullchain.pem --cert-path cert.pem

Note: Modern clients match names in SAN; the CN is ignored when SANs are present.


Renewal & Automation

  • Let’s Encrypt certs are valid for 90 days by default.
  • Certbot installs a systemd timer/cron to renew automatically, typically when <30 days remain.
  • You can test renewal anytime:
sudo certbot renew --dry-run
  • For manual cron setups (if needed):
0 3 * * * root certbot renew --quiet

Reload your web server after renewal if your stack doesn’t do this automatically (e.g., use deploy hooks).

Example deploy hook (Nginx reload):

sudo certbot renew --deploy-hook "systemctl reload nginx"

Optional 2025 update: short‑lived certificates

  • Let’s Encrypt now offers an optional 6‑day “short‑lived” certificate profile (rolling out in 2025). 90‑day certificates remain available and are the default. Short‑lived certs require ACME certificate profiles support in your client and are best used when your automation is rock‑solid.

Verifying Your Certificate

  • Show certs issued/managed by Certbot:
sudo certbot certificates
  • Inspect a local certificate:
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -text | less
  • Check what the world sees on port 443:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null

Troubleshooting Checklist

  • Ports: Use the right challenge + port. HTTP‑01 → port 80, TLS‑ALPN‑01 → port 443. If those ports are blocked, switch to DNS‑01.
  • DNS: A/AAAA records correct; for DNS‑01, TXT under _acme-challenge.<domain> has propagated.
  • Reverse proxies/LB: Ensure the challenge path reaches the right backend, or use DNS‑01.
  • CAA: If using CAA records, allow letsencrypt.org to issue for your domain. (See advanced CAA options below.)
  • Rate limits: Let’s Encrypt enforces rate limits; consolidate SANs and avoid excessive retries.
  • File perms: Keys in /etc/letsencrypt are root‑owned; don’t change ownership broadly. Give services read‑only access if needed.

Security Tips

  • Treat privkey.pem like a password—restrict read access.

  • Prefer ECDSA P‑256 or RSA‑2048; rotate keys periodically.

  • Certbot rotates private keys by default on renewal. Use --reuse-key only if you have a pinning/compliance reason to keep the same key.

  • Never store private keys in source control or public object storage.

  • Consider an HSM/KMS if compliance requires hardware‑backed keys.

  • Consider enabling HSTS once you’re confident in your HTTPS setup:

    Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

CAA Governance (optional, advanced)

If you publish CAA records, you can tighten issuance beyond issue "letsencrypt.org":

  • Use issuewild to set a different policy for wildcard certs.
  • Bind issuance to your specific Let’s Encrypt account (accounturi).
  • Restrict allowed validation methods (validationmethods).

Example

example.com.  CAA 0 issue     "letsencrypt.org"
example.com. CAA 0 issuewild "letsencrypt.org"
example.com. CAA 0 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456;validationmethods=http-01,dns-01"

Quick Glossary

  • ACME: Automated Certificate Management Environment, the protocol Certbot uses.
  • SAN: Subject Alternative Name—list of domain names a cert covers. (Modern clients match names in SAN.)
  • Wildcard: A cert that covers all first‑level subdomains, e.g., *.example.com (requires DNS‑01) and does not match the apex.

Appendix: Revocation (only if a key is compromised)

sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pem

Then replace the key, reissue, and deploy.

2025 update: Let’s Encrypt is ending OCSP support and removing OCSP Must‑Staple. Prefer short lifetimes and standard revocation (CRLs) rather than Must‑Staple.


At‑a‑glance: Typical Flow

  1. Install Certbot → 2) Choose validation (HTTP‑01 or DNS‑01) → 3) certbot issues and installs → 4) Point your web server to fullchain.pem + privkey.pem → 5) Automatic renewals with certbot renew.