SSL Toolkit

SSL & TLS Guide

Install, configure and renew certificates with Let's Encrypt and Certbot on every OS, front them with Cloudflare, and keep an OpenSSL cheat sheet at hand. Every command block has a Copy button.

Updated 2026-08-29 · Commands target current stable releases; check the linked upstream docs when a distribution changes packaging.

1. Concepts you need once

Certificate binds a public key to names (CN and Subject Alternative Names). Private key never leaves your server. Chain = leaf → intermediate(s) → root; servers must send the leaf and intermediates, never the root. Validation levels: DV (domain, what Let's Encrypt issues), OV/EV (organization-vetted, paid). Key types: RSA 2048+ or ECDSA P-256 (smaller, faster; default in modern Certbot). Formats: PEM (Base64 text, .pem/.crt/.key), DER (binary, .der/.cer), PKCS#7 (.p7b, certs only), PKCS#12 (.pfx/.p12, cert + key + chain, password-protected).

Use the SSL Toolkit to decode certificates and CSRs, match keys, convert formats and check a live host. Nothing you paste there leaves your browser except the hostname in the Check tab.

2. Install Certbot

Let's Encrypt recommends the snap package where snapd exists (always current); distributions also ship native packages. Pick one method per machine — do not mix snap and apt/dnf installs.

Ubuntu / Debian (snap, recommended)

Ubuntu 20.04+ · Debian 11+
sudo apt update && sudo apt install -y snapd
sudo snap install core && sudo snap refresh core
sudo apt remove -y certbot            # remove any distro package first
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
certbot --version

Ubuntu / Debian (apt package)

apt · includes the Nginx and Apache plugins
sudo apt update
sudo apt install -y certbot python3-certbot-nginx python3-certbot-apache

RHEL · Rocky · AlmaLinux · CentOS Stream

EPEL + dnf
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx python3-certbot-apache
# or snap: sudo dnf install -y snapd && sudo systemctl enable --now snapd.socket && sudo snap install --classic certbot

Fedora

dnf
sudo dnf install -y certbot python3-certbot-nginx python3-certbot-apache

Arch Linux · Manjaro

pacman
sudo pacman -Syu certbot certbot-nginx certbot-apache

openSUSE

zypper
sudo zypper install -y certbot python3-certbot-nginx python3-certbot-apache

Alpine Linux

apk
sudo apk add --no-cache certbot certbot-nginx certbot-apache

macOS (Homebrew)

brew · development machines
brew install certbot
# certificates and config live under /opt/homebrew/etc/letsencrypt (Apple Silicon) or /usr/local/etc/letsencrypt (Intel)
sudo certbot certonly --standalone -d example.com

Windows (win-acme)

PowerShell · IIS or standalone
# Install win-acme (Certbot for Windows is discontinued)
winget install --id win-acme.win-acme
# Interactive wizard (binds to IIS sites automatically)
wacs.exe
# Unattended: web root validation, install into IIS, schedule renewal
wacs.exe --target manual --host example.com,www.example.com --webroot C:\inetpub\wwwroot --installation iis --accepttos --emailaddress admin@example.com

Docker

Official certbot image · webroot shared with the web server
docker run --rm -it \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/www/certbot:/var/www/certbot \
  certbot/certbot certonly --webroot -w /var/www/certbot \
  -d example.com -d www.example.com --email admin@example.com --agree-tos --no-eff-email

# Renew (cron/systemd on the host)
docker run --rm -v /etc/letsencrypt:/etc/letsencrypt -v /var/www/certbot:/var/www/certbot certbot/certbot renew --quiet \
  && docker exec nginx nginx -s reload

3. Obtain a certificate

Prerequisites: DNS A/AAAA records point at this server, ports 80 (HTTP-01) or DNS API access (DNS-01) are available, and the server clock is correct. Use --dry-run against the staging environment first to avoid rate limits (50 certificates per registered domain per week).

Nginx plugin (installs and edits the server block)

HTTP-01 via Nginx
sudo certbot --nginx -d example.com -d www.example.com --email admin@example.com --agree-tos --redirect --hsts
# certificate only (configure Nginx yourself):
sudo certbot certonly --nginx -d example.com

Apache plugin

HTTP-01 via Apache
sudo certbot --apache -d example.com -d www.example.com --email admin@example.com --agree-tos --redirect

Webroot (no web-server plugin, zero downtime)

HTTP-01 · serve /.well-known/acme-challenge from a directory
sudo mkdir -p /var/www/certbot
sudo certbot certonly --webroot -w /var/www/certbot -d example.com -d www.example.com --email admin@example.com --agree-tos

Standalone (nothing listening on port 80 yet)

HTTP-01 · Certbot binds port 80 temporarily
sudo certbot certonly --standalone -d example.com --email admin@example.com --agree-tos
# with a running web server: --pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx"

DNS-01 with Cloudflare (wildcards, internal hosts, no port 80)

certbot-dns-cloudflare · API token with Zone:DNS:Edit
# plugin: snap
sudo snap set certbot trust-plugin-with-root=ok
sudo snap install certbot-dns-cloudflare
# plugin: pip/apt alternative: pip install certbot-dns-cloudflare  |  apt install python3-certbot-dns-cloudflare

sudo install -m 600 /dev/null /etc/letsencrypt/cloudflare.ini
echo 'dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN' | sudo tee /etc/letsencrypt/cloudflare.ini >/dev/null

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 30 \
  -d example.com -d '*.example.com' --email admin@example.com --agree-tos

Options worth knowing

Key type · staging · account
--key-type ecdsa --elliptic-curve secp384r1   # default is ecdsa P-256 on Certbot 2.x; use --key-type rsa --rsa-key-size 4096 for RSA
--dry-run                                    # staging CA, no rate-limit usage
--cert-name example.com                      # stable lineage name under /etc/letsencrypt/live/
--preferred-chain "ISRG Root X1"             # pin a chain when clients trust only one root
certbot certificates                         # list lineages, expiry and paths
certbot delete --cert-name example.com       # remove a lineage

4. Configure the server

Files after issuance: /etc/letsencrypt/live/example.com/fullchain.pem (leaf + intermediates) and privkey.pem. Always reference fullchain.pem, never cert.pem alone.

Nginx

/etc/nginx/sites-available/example.com
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    location /.well-known/acme-challenge/ { root /var/www/certbot; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com www.example.com;

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

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:10m;
    ssl_session_tickets off;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    root /var/www/example.com;
    index index.html;
}
Test and reload
sudo nginx -t && sudo systemctl reload nginx

Apache

/etc/apache2/sites-available/example.com-ssl.conf
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>

<VirtualHost *:443>
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/example.com

    SSLEngine on
    SSLCertificateFile    /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    SSLHonorCipherOrder off
    SSLUseStapling on
    Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
</VirtualHost>
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
Enable and reload
sudo a2enmod ssl headers http2
sudo a2ensite example.com-ssl
sudo apachectl configtest && sudo systemctl reload apache2   # httpd on RHEL family

Caddy (automatic HTTPS, no Certbot needed)

/etc/caddy/Caddyfile
example.com, www.example.com {
    root * /var/www/example.com
    file_server
    encode gzip
    # Caddy obtains and renews Let's Encrypt certificates automatically.
    # Cloudflare DNS challenge (wildcards): tls { dns cloudflare {env.CF_API_TOKEN} }
}

Node.js (Express) behind Nginx, or direct TLS

Direct TLS with the Let's Encrypt files (reload on renewal)
import https from 'node:https';
import fs from 'node:fs';
const options = {
  key:  fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
  cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem'),
  minVersion: 'TLSv1.2',
};
https.createServer(options, app).listen(443);
// Prefer terminating TLS at Nginx/Caddy and proxying to Node on 127.0.0.1 — one place to renew and harden.

5. Renewal and deploy hooks

Let's Encrypt certificates last 90 days. Snap and most packages install a timer that runs certbot renew twice daily; renewal happens when fewer than 30 days remain. Add a deploy hook so the web server reloads with the new files.

Verify the timer and simulate renewal
systemctl list-timers | grep certbot        # snap: snap.certbot.renew.timer
sudo certbot renew --dry-run
sudo certbot renew --force-renewal --cert-name example.com   # only when you must
Deploy hook · runs only after a successful renewal
sudo tee /etc/letsencrypt/renewal-hooks/deploy/reload.sh >/dev/null <<'SH'
#!/bin/sh
systemctl reload nginx   # or: systemctl reload apache2 / docker exec nginx nginx -s reload
SH
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload.sh
Cron alternative (when no systemd timer exists)
echo "0 3,15 * * * root certbot renew --quiet --deploy-hook 'systemctl reload nginx'" | sudo tee /etc/cron.d/certbot-renew

6. Cloudflare SSL

With the orange cloud (proxied) on, browsers talk to Cloudflare's edge certificate (Universal SSL, automatic) and Cloudflare talks to your origin. The SSL/TLS mode decides how that second hop is protected.

ModeBrowser → CloudflareCloudflare → OriginUse when
OffHTTPHTTPNever for production
FlexibleHTTPSHTTP (unencrypted)Origin cannot do TLS at all — traffic to origin is exposed; avoid
FullHTTPSHTTPS, certificate not validatedSelf-signed origin certificate
Full (strict)HTTPSHTTPS, certificate validatedRecommended: Let's Encrypt or Cloudflare Origin CA on the origin
Strict (SSL-only origin pull)HTTPSHTTPS always, validatedEnterprise; origin never serves HTTP

Option A — Cloudflare Origin CA certificate (free, 15-year, only trusted by Cloudflare)

Dashboard → SSL/TLS → Origin Server → Create Certificate (RSA or ECDSA, hostnames incl. wildcard). Install the certificate and key on the origin and set the mode to Full (strict). Browsers never see this certificate, so it must only be used behind the proxy.

Nginx with Origin CA + Authenticated Origin Pulls
sudo mkdir -p /etc/ssl/cloudflare
# paste the Origin Certificate and Private Key from the dashboard:
sudo nano /etc/ssl/cloudflare/origin.pem
sudo nano /etc/ssl/cloudflare/origin.key   && sudo chmod 600 /etc/ssl/cloudflare/origin.key
# Authenticated Origin Pulls CA (so only Cloudflare can reach the origin):
sudo curl -fsSL https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem -o /etc/ssl/cloudflare/origin-pull-ca.pem

# nginx server block (443)
ssl_certificate        /etc/ssl/cloudflare/origin.pem;
ssl_certificate_key    /etc/ssl/cloudflare/origin.key;
ssl_client_certificate /etc/ssl/cloudflare/origin-pull-ca.pem;
ssl_verify_client on;   # then enable SSL/TLS → Origin Server → Authenticated Origin Pulls

Option B — Let's Encrypt on the origin behind Cloudflare

HTTP-01 works through the proxy as long as /.well-known/acme-challenge/ is reachable over HTTP (disable "Always Use HTTPS" for that path or use a Page Rule / Configuration Rule), or use DNS-01 with an API token (section 3) — the reliable choice, and required for wildcards.

Cloudflare API token scope
Dashboard → My Profile → API Tokens → Create Token → "Edit zone DNS" template
Permissions: Zone · DNS · Edit      Zone Resources: Include · Specific zone · example.com
Store it in /etc/letsencrypt/cloudflare.ini (chmod 600); never commit it.

Recommended edge settings

  • SSL/TLS → Overview: Full (strict).
  • Edge Certificates: Always Use HTTPS on, Automatic HTTPS Rewrites on, Minimum TLS Version 1.2, TLS 1.3 on, Opportunistic Encryption on.
  • HSTS: enable only after everything serves HTTPS; start with a short max-age.
  • Origin Server: Authenticated Origin Pulls on once the origin verifies the client certificate.
  • Universal SSL covers example.com and *.example.com only; deeper levels need Advanced Certificate Manager or a custom certificate.
Error 525 = Cloudflare could not complete the TLS handshake with the origin (no certificate, wrong port, cipher mismatch). Error 526 = the origin certificate is invalid under Full (strict) (expired, self-signed, wrong hostname). Check the origin directly with openssl s_client below, or with the Check tab of the toolkit against the origin IP.

7. OpenSSL cheat sheet

The SSL Toolkit's OpenSSL tab fills these in with your own domain, key type and file names; the general forms are listed here.

Keys and CSRs
# RSA 2048 / 4096 private key (PKCS#8)
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out example.com.key
# ECDSA P-256 private key
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:prime256v1 -out example.com.key
# CSR with SANs (OpenSSL 1.1.1+)
openssl req -new -key example.com.key -out example.com.csr \
  -subj "/C=ID/ST=Jakarta/L=Jakarta/O=PT Lancar Inovasi Digital/CN=example.com" \
  -addext "subjectAltName=DNS:example.com,DNS:www.example.com"
# Self-signed certificate for testing (365 days)
openssl req -x509 -newkey rsa:2048 -nodes -keyout test.key -out test.crt -days 365 \
  -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
Inspect
openssl x509 -in cert.pem -noout -text                      # full certificate
openssl x509 -in cert.pem -noout -subject -issuer -dates -ext subjectAltName
openssl x509 -in cert.pem -noout -fingerprint -sha256
openssl req  -in request.csr -noout -text -verify           # CSR (and signature check)
openssl pkey -in key.pem -noout -text | head                # private key
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null   # live host
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null | openssl x509 -noout -dates
openssl verify -CAfile chain.pem cert.pem                    # chain validation
Match key ↔ certificate ↔ CSR (identical public-key hashes = match)
openssl x509 -in cert.pem -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256
openssl pkey -in key.pem -pubout -outform DER | openssl dgst -sha256
openssl req  -in request.csr -noout -pubkey | openssl pkey -pubin -outform DER | openssl dgst -sha256
Convert formats
# PEM → DER and back
openssl x509 -in cert.pem -outform DER -out cert.der
openssl x509 -in cert.der -inform DER -out cert.pem
# PEM → PKCS#7 (.p7b, certificates only) and back
openssl crl2pkcs7 -nocrl -certfile cert.pem -certfile chain.pem -out bundle.p7b
openssl pkcs7 -in bundle.p7b -print_certs -out bundle.pem
# PEM → PKCS#12 (.pfx: cert + key + chain, password-protected) and back
openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile chain.pem -name "example.com" -out example.com.pfx
openssl pkcs12 -in example.com.pfx -nocerts -nodes -out key.pem
openssl pkcs12 -in example.com.pfx -clcerts -nokeys -out cert.pem
openssl pkcs12 -in example.com.pfx -cacerts -nokeys -chain -out chain.pem
# Private key: PKCS#1 ↔ PKCS#8, encrypt / decrypt
openssl pkcs8 -topk8 -nocrypt -in key.pkcs1.pem -out key.pkcs8.pem
openssl rsa -in key.pkcs8.pem -out key.pkcs1.pem
openssl pkcs8 -topk8 -v2 aes-256-cbc -in key.pem -out key.enc.pem
openssl pkey -in key.enc.pem -out key.pem

8. Troubleshooting checklist

  • "Certificate is not trusted" in browsers but fine in curl → intermediate missing; serve fullchain.pem.
  • NET::ERR_CERT_COMMON_NAME_INVALID → hostname not in SANs; reissue with every name (-d per name).
  • Certbot "Connection refused" / timeout → port 80 closed (firewall, security group) or DNS points elsewhere; use DNS-01.
  • Too many certificates already issued → weekly rate limit; wait or use --dry-run while testing.
  • Renewal succeeded but the site still shows the old certificate → the server was not reloaded; add the deploy hook.
  • Cloudflare 525/526 → origin TLS broken or not strict-valid; check the origin directly and the SSL mode.
  • Key does not match → compare public-key hashes (Match tab or the OpenSSL commands above) before touching the server config.
  • Clock skew → "certificate is not yet valid" means the server or client clock is wrong; enable NTP.

Upstream references: certbot.eff.org/instructions · letsencrypt.org/docs · developers.cloudflare.com/ssl · win-acme.com · ssl-config.mozilla.org.