L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Déploiement et opérations · SKILL.md

ssl-tls

ssl-tls

SSL/TLS certificate management skill: Let's Encrypt, Certbot, certificate types, TLS version/cipher configuration, HSTS, OCSP stapling, renewal automation, and local development certificates. USE WHEN: - Obtaining and renewing Let's Encrypt certificates (standalone, webroot, DNS challenge) - Configuring TLS on Nginx or Apache (protocols, cipher suites, ses…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: ssl-tls description: | SSL/TLS certificate management skill: Let's Encrypt, Certbot, certificate types, TLS version/cipher configuration, HSTS, OCSP stapling, renewal automation, and local development certificates.

USE WHEN:

  • Obtaining and renewing Let's Encrypt certificates (standalone, webroot, DNS challenge)
  • Configuring TLS on Nginx or Apache (protocols, cipher suites, session parameters)
  • Setting up HSTS with preload, OCSP stapling, or DHParam generation
  • Automating certificate renewal via systemd timer and deploy hooks
  • Managing multi-domain (SAN) or wildcard certificates
  • Debugging TLS handshake failures, mixed content, or ACME challenge errors
  • Setting up mkcert for trusted local HTTPS development

DO NOT USE FOR:

  • mTLS / client certificate authentication (use service-mesh or api-gateway skill)
  • Application-level JWT or session token management
  • SSH key management
  • Internal CA for Kubernetes clusters (use cert-manager) allowed-tools: Read, Grep, Glob, Write, Edit, Bash

SSL/TLS Certificate Management

Certificate Types

TypeIssued toValidationBrowser trustWildcard support
DV (Domain Validated)Domain ownerAutomated DNS/HTTPYesVia wildcard DV
OV (Organization Validated)Legal entityManual org checkYesNo
EV (Extended Validation)Legal entity + strict checksExtensive manualYes (green bar deprecated)No
Wildcard (e.g. *.example.com)Domain ownerDNS challenge onlyYesYes — one level only
Self-signedAnyoneNoneNo — browser warningYes
mkcert (local CA)Localhost, IPsLocal trust storeYes (after trust-store install)Yes

Let's Encrypt issues DV certs only. Wildcard certs require the DNS-01 challenge.


ACME Protocol Basics

ACME (Automatic Certificate Management Environment, RFC 8555) works as follows:

  1. Client (Certbot) contacts the CA (Let's Encrypt) and creates an Order for one or more domains.
  2. CA issues a Challenge — either HTTP-01 (place a file at /.well-known/acme-challenge/<token>) or DNS-01 (create a _acme-challenge.<domain> TXT record).
  3. Client completes the challenge.
  4. CA verifies the challenge and issues the certificate.
  5. Client stores fullchain.pem (cert + intermediates) and privkey.pem.

Certificates are valid for 90 days; automate renewal to run at least every 60 days.


Certbot — All Challenge Types

Standalone (no existing web server on port 80)

# Install Certbot (Ubuntu/Debian)
sudo apt-get install -y certbot

# Obtain cert — Certbot spins up a temporary HTTP server on port 80
sudo certbot certonly \
  --standalone \
  --preferred-challenges http \
  -d example.com \
  -d www.example.com \
  --email admin@example.com \
  --agree-tos \
  --no-eff-email

# Files created in: /etc/letsencrypt/live/example.com/
#   fullchain.pem  — certificate + intermediates (use this in ssl_certificate)
#   privkey.pem    — private key
#   chain.pem      — intermediates only (use for OCSP stapling)
#   cert.pem       — leaf certificate only (rarely needed)

Webroot (Nginx / Apache keeps running)

# Ensure the webroot is served by your web server at /.well-known/acme-challenge/
# Nginx: location /.well-known/acme-challenge/ { root /var/www/certbot; }

sudo certbot certonly \
  --webroot \
  --webroot-path /var/www/certbot \
  -d example.com \
  -d www.example.com \
  --email admin@example.com \
  --agree-tos \
  --no-eff-email

DNS Challenge — Cloudflare (wildcard support)

# Install Certbot Cloudflare DNS plugin
sudo apt-get install -y python3-certbot-dns-cloudflare

# Create credentials file (readable only by root)
sudo mkdir -p /etc/letsencrypt/credentials
sudo tee /etc/letsencrypt/credentials/cloudflare.ini > /dev/null <<'EOF'
# Cloudflare API token (Zone:DNS:Edit permission, scoped to zone)
dns_cloudflare_api_token = YOUR_CF_API_TOKEN_HERE
EOF
sudo chmod 600 /etc/letsencrypt/credentials/cloudflare.ini

# Obtain wildcard + apex certificate
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/credentials/cloudflare.ini \
  --dns-cloudflare-propagation-seconds 60 \
  -d example.com \
  -d "*.example.com" \
  --email admin@example.com \
  --agree-tos \
  --no-eff-email

Nginx SSL Block (Production-Ready)

# Inside server { listen 443 ssl; ... }

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;  # OCSP stapling

# Protocol: drop TLS 1.0 and 1.1 (deprecated in RFC 8996)
ssl_protocols TLSv1.2 TLSv1.3;

# Cipher suites: TLS 1.3 ciphers are implicit and non-configurable.
# The list below applies to TLS 1.2 only.
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256';

# Let clients (especially TLS 1.3) pick their preferred cipher
ssl_prefer_server_ciphers off;

# Session resumption (reduces handshake overhead for returning clients)
ssl_session_cache   shared:SSL:10m;   # 1MB ≈ 4000 sessions; 10m ≈ 40000 sessions
ssl_session_timeout 1d;
ssl_session_tickets off;              # Disable — tickets use a server-side key that
                                      # if leaked undermines forward secrecy

# DHE key exchange parameters (generated once; 2048-bit minimum)
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

# OCSP Stapling: server fetches and caches the OCSP response;
# eliminates client round-trip to CA's OCSP responder
ssl_stapling        on;
ssl_stapling_verify on;
resolver            1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout    5s;

# HSTS: tell browsers to always use HTTPS for this domain
# preload: submit to browser preload lists (irreversible for ~1 year)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

DHParam Generation

# Generate 2048-bit DH parameters (runs once; takes ~30 seconds)
sudo mkdir -p /etc/nginx/ssl
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

# 4096-bit for highest security (takes several minutes — overkill for most apps)
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096

Renewal Automation — systemd Timer

/etc/systemd/system/certbot-renewal.service

[Unit]
Description=Certbot Renewal
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew \
    --quiet \
    --deploy-hook /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
StandardOutput=journal
StandardError=journal

/etc/systemd/system/certbot-renewal.timer

[Unit]
Description=Run Certbot renewal twice daily

[Timer]
OnCalendar=*-*-* 03,15:00:00   # 3 AM and 3 PM UTC
RandomizedDelaySec=3600         # Spread load on Let's Encrypt's servers
Persistent=true                 # Run immediately if last run was missed (e.g. server was off)

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renewal.timer
sudo systemctl list-timers certbot-renewal

Deploy Hook — /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

#!/bin/bash
# Called by Certbot after a successful renewal.
# RENEWED_LINEAGE is set to the path of the renewed certificate.
set -euo pipefail

echo "[certbot-deploy] Reloading Nginx after renewal of ${RENEWED_LINEAGE}"

# Validate new config before reloading
nginx -t || { echo "[certbot-deploy] ERROR: Nginx config test failed"; exit 1; }

systemctl reload nginx
echo "[certbot-deploy] Nginx reloaded successfully"
sudo chmod 700 /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Local Development — mkcert

# Install mkcert (Linux/macOS)
# macOS: brew install mkcert
# Linux: download binary from https://github.com/FiloSottile/mkcert/releases
sudo apt-get install -y libnss3-tools   # Required for Chrome/Firefox trust store
wget -q "https://dl.filippo.io/mkcert/latest?for=linux/amd64" -O mkcert
chmod +x mkcert && sudo mv mkcert /usr/local/bin/

# Install local CA into system and browser trust stores
mkcert -install

# Generate certificate for local domains and IPs
mkcert localhost 127.0.0.1 ::1 myapp.local "*.myapp.local"

# Output: localhost+5.pem + localhost+5-key.pem in current directory
# Reference these files in your local Nginx / dev server TLS config

Certificate Inspection Commands

# Inspect the live certificate served by a host
openssl s_client -connect example.com:443 -servername example.com \
  </dev/null 2>/dev/null | openssl x509 -noout -text

# Check expiry date only
openssl s_client -connect example.com:443 -servername example.com \
  </dev/null 2>/dev/null | openssl x509 -noout -dates

# Inspect a cert file on disk
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem \
  -noout -text | grep -E 'DNS:|Not (Before|After)|Subject:|Issuer:'

# Verify cert chain
openssl verify -CAfile /etc/letsencrypt/live/example.com/chain.pem \
  /etc/letsencrypt/live/example.com/cert.pem

# Test OCSP stapling response
openssl s_client -connect example.com:443 -servername example.com \
  -status </dev/null 2>/dev/null | grep -A6 "OCSP Response"

# List all managed Certbot certificates and expiry
sudo certbot certificates

# Dry-run renewal test (does not modify files or contact Let's Encrypt production)
sudo certbot renew --dry-run

# Check certificate SANs
openssl x509 -in fullchain.pem -noout -ext subjectAltName

# Check that private key matches certificate
openssl x509 -noout -modulus -in cert.pem | md5sum
openssl rsa  -noout -modulus -in privkey.pem | md5sum
# Both MD5 hashes must match

ssl_session_cache vs ssl_session_tickets

SettingMechanismForward secrecyRecommended
ssl_session_cache shared:SSL:10mServer-side session storePreserved (new ephemeral key per session)Yes
ssl_session_tickets onClient holds encrypted session tokenTicket key acts like a long-term secret — if leaked, all sessions exposedOff (ssl_session_tickets off)

Anti-Patterns

Anti-patternWhy it's harmfulFix
TLS 1.0 / 1.1 enabledBoth deprecated (RFC 8996); vulnerable to BEAST, POODLEssl_protocols TLSv1.2 TLSv1.3; only
Self-signed cert in productionBrowser shows security warning; blocks automated clientsUse Let's Encrypt (free) or a paid CA
No renewal monitoringCertificate expires without warning — site goes downMonitor expiry: Prometheus ssl_expiry_seconds, UptimeRobot TLS check, or certbot renew --dry-run in CI
Weak DHParam (512/768-bit or none)DHE key exchange vulnerable to LogjamGenerate 2048-bit: openssl dhparam -out dhparam.pem 2048
ssl_session_tickets on (default in Nginx)Ticket key is rotated rarely; long-term key exposure breaks forward secrecyssl_session_tickets off;
No HSTS headerBrowsers re-attempt HTTP on each visit; MITM downgrade possibleAdd HSTS; start with short max-age; add preload only when stable
HSTS preload added too earlyPreload list submission is hard to reverse (~1 year)Test with short max-age=300, then extend to 63072000 before submitting
ssl_prefer_server_ciphers on with TLS 1.3TLS 1.3 ignores this directive; may penalise clients unnecessarily on TLS 1.2Set off; ensure cipher list is already strong
No CAA DNS recordAny CA can issue a cert for your domainAdd CAA 0 issue "letsencrypt.org" record to your DNS zone
Credential/API token in certbot ini file without chmod 600Anyone with shell access can read the tokenchmod 600 /etc/letsencrypt/credentials/cloudflare.ini

Troubleshooting

SymptomLikely causeDiagnostic / Fix
ACME HTTP-01 challenge failsPort 80 not reachable from internet, or wrong webroot pathCheck firewall; curl http://example.com/.well-known/acme-challenge/test; verify Nginx root for challenge location
DNS-01 challenge failsTXT record not propagated within propagation-seconds windowIncrease --dns-cloudflare-propagation-seconds 120; verify with dig TXT _acme-challenge.example.com @8.8.8.8
Renewal error: "Problem binding to port 80"Another process (Nginx) is using port 80 during standalone challengeSwitch to webroot challenge; or stop Nginx, renew, restart
"certificate verify failed" in clientMissing intermediate in fullchain.pemUse fullchain.pem (not cert.pem) in ssl_certificate
HSTS lock-in on wrong redirectPreloaded or long max-age HSTS prevents HTTP accessCannot be quickly undone — must wait for max-age to expire; test with fresh browser profile
Mixed content warningsHTTPS page loading HTTP resourcesAudit page with browser DevTools; set Content-Security-Policy: upgrade-insecure-requests as interim fix
OCSP stapling "no response"Nginx can't reach OCSP responder (firewall or resolver missing)Ensure resolver directive is set; verify outbound HTTPS is allowed from server
Private key / cert mismatchWrong cert installed or key regeneratedCompare modulus MD5: openssl x509 -noout -modulus -in cert.pem | md5sum vs openssl rsa -noout -modulus -in privkey.pem | md5sum
"Too many certificates already issued"Let's Encrypt rate limit: 50 certs per registered domain per weekUse --staging flag for testing; wait for rate limit window to reset
mkcert cert not trusted in Chromemkcert -install not run, or browser uses its own trust storeRe-run mkcert -install; restart browser; for Chrome on Linux, also install in ~/.pki/nssdb
Skills enregistrées