SSL Certificate Management
HTTPS has become a baseline requirement for modern web services. This article explains how to obtain, configure, and manage SSL/TLS certificates on RHEL-based distributions, including free Let’s Encrypt certificates and self-signed certificates for internal use.
SSL/TLS Fundamentals
Section titled “SSL/TLS Fundamentals”Certificate Chain
Section titled “Certificate Chain”SSL certificate trust is built on a chain of certificates:
Root Certificate (Root CA) +-- Intermediate Certificate (Intermediate CA) +-- Server Certificate (Server Certificate)Browsers trust root Certificate Authorities (CAs). The root CA issues intermediate certificates, and intermediate certificates issue your server certificate. When configuring a server, you typically need to provide the server certificate + intermediate certificate as a complete chain.
Certificate File Types
Section titled “Certificate File Types”| Extension | Description |
|---|---|
.pem | Base64-encoded certificate/key (most common) |
.crt / .cer | Certificate file (usually PEM format) |
.key | Private key file |
.csr | Certificate Signing Request |
.p12 / .pfx | PKCS#12 format (contains certificate + private key) |
Let’s Encrypt + Certbot
Section titled “Let’s Encrypt + Certbot”Let’s Encrypt provides free DV (Domain Validation) certificates, valid for 90 days, with support for automatic renewal.
Installing Certbot
Section titled “Installing Certbot”# Install certbot and the corresponding web server pluginsudo dnf install epel-release -ysudo dnf install certbot -y
# Choose the plugin based on your web serversudo dnf install python3-certbot-nginx -y # Nginx# orsudo dnf install python3-certbot-apache -y # ApacheObtaining a Certificate (Nginx)
Section titled “Obtaining a Certificate (Nginx)”# Automatically obtain and configure (recommended)# certbot will automatically modify your Nginx configuration and reloadsudo certbot --nginx -d example.com -d www.example.com
# During the interactive process you will need to:# 1. Enter an email address (for expiration reminders)# 2. Agree to the terms of service# 3. Choose whether to redirect HTTP to HTTPSObtaining a Certificate (Apache)
Section titled “Obtaining a Certificate (Apache)”sudo certbot --apache -d example.com -d www.example.comObtaining a Certificate Only (Without Modifying Configuration)
Section titled “Obtaining a Certificate Only (Without Modifying Configuration)”If you want to configure the web server manually:
# Standalone mode (temporarily uses port 80)sudo certbot certonly --standalone -d example.com -d www.example.com
# Webroot mode (web server stays running)sudo certbot certonly --webroot -w /var/www/html -d example.com -d www.example.comCertificate files are saved by default under /etc/letsencrypt/live/example.com/:
/etc/letsencrypt/live/example.com/├── cert.pem # Server certificate├── chain.pem # Intermediate certificate chain├── fullchain.pem # Full certificate chain (cert + chain)└── privkey.pem # Private keyManual Nginx Configuration
Section titled “Manual Nginx Configuration”sudo tee /etc/nginx/conf.d/example.com.conf > /dev/null <<'EOF'server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri;}
server { listen 443 ssl http2; 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 security configuration ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; ssl_prefer_server_ciphers off;
# HSTS (once enabled, browsers will enforce HTTPS) add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# OCSP Stapling ssl_stapling on; ssl_stapling_verify on; ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem; resolver 8.8.8.8 8.8.4.4 valid=300s;
# Session cache ssl_session_cache shared:SSL:10m; ssl_session_timeout 1d; ssl_session_tickets off;
root /var/www/example.com; index index.html;
location / { try_files $uri $uri/ =404; }}EOF
sudo nginx -t && sudo systemctl reload nginxWildcard Certificates
Section titled “Wildcard Certificates”Wildcard certificates (*.example.com) require DNS validation:
# Obtain a wildcard certificate using DNS validationsudo certbot certonly \ --manual \ --preferred-challenges dns \ -d "example.com" \ -d "*.example.com"
# certbot will prompt you to add a _acme-challenge TXT record# Add it at your DNS provider, wait for propagation, then continueFor automated scenarios, use DNS plugins:
# Using Cloudflare as an examplesudo dnf install python3-certbot-dns-cloudflare -y
# Create the API credentials filesudo mkdir -p /etc/letsencryptsudo tee /etc/letsencrypt/cloudflare.ini > /dev/null <<'EOF'dns_cloudflare_api_token = your_cloudflare_api_tokenEOFsudo chmod 600 /etc/letsencrypt/cloudflare.ini
# Automatically obtain a wildcard certificatesudo certbot certonly \ --dns-cloudflare \ --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \ -d "example.com" \ -d "*.example.com"Common DNS plugins:
# Alibaba Cloud DNS (third-party plugin)pip install certbot-dns-aliyun
# Cloudflarednf install python3-certbot-dns-cloudflare
# Route53 (AWS)dnf install python3-certbot-dns-route53Automatic Renewal
Section titled “Automatic Renewal”Certbot configures a systemd timer for automatic renewal by default after installation:
# Check the renewal timer statussystemctl list-timers certbot*
# Test renewal manually (dry run)sudo certbot renew --dry-run
# If the timer does not exist, create it manuallysudo tee /etc/systemd/system/certbot-renew.timer > /dev/null <<'EOF'[Unit]Description=Certbot Automatic Renewal Timer
[Timer]OnCalendar=*-*-* 00,12:00:00RandomizedDelaySec=3600Persistent=true
[Install]WantedBy=timers.targetEOF
sudo tee /etc/systemd/system/certbot-renew.service > /dev/null <<'EOF'[Unit]Description=Certbot Automatic Renewal
[Service]Type=oneshotExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"EOF
sudo systemctl daemon-reloadsudo systemctl enable --now certbot-renew.timerThe --deploy-hook ensures the web server is automatically reloaded after the certificate is updated.
Managing Existing Certificates
Section titled “Managing Existing Certificates”# List all certificatessudo certbot certificates
# Delete a certificatesudo certbot delete --cert-name example.com
# Expand certificate domainssudo certbot certonly --nginx \ --cert-name example.com \ -d example.com \ -d www.example.com \ -d api.example.comSelf-Signed Certificates (Internal Use)
Section titled “Self-Signed Certificates (Internal Use)”Self-signed certificates are suitable for internal services, development, and testing scenarios that do not require public trust.
Quick Generation
Section titled “Quick Generation”# Generate a self-signed certificate in one command (valid for 3650 days)openssl req -x509 -nodes -days 3650 \ -newkey rsa:2048 \ -keyout /etc/pki/tls/private/selfsigned.key \ -out /etc/pki/tls/certs/selfsigned.crt \ -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=internal.example.com"
# Set permissionssudo chmod 600 /etc/pki/tls/private/selfsigned.keysudo chmod 644 /etc/pki/tls/certs/selfsigned.crtCreating an Internal CA
Section titled “Creating an Internal CA”For multiple internal servers, it is recommended to create your own CA so that all issued certificates are uniformly trusted:
# 1. Create the CA private key and certificatemkdir -p /etc/pki/internal-cacd /etc/pki/internal-ca
# Generate the CA private keyopenssl genrsa -aes256 -out ca.key 4096
# Generate the CA certificate (valid for 10 years)openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \ -out ca.crt \ -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=MyCompany Internal CA"
# 2. Generate a certificate for the server# Generate the server private keyopenssl genrsa -out server.key 2048
# Create a Certificate Signing Request (CSR)openssl req -new -key server.key \ -out server.csr \ -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/CN=app.internal.example.com"
# Create an extensions configuration (with SAN support)tee server-ext.cnf > /dev/null <<'EOF'authorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEnciphermentsubjectAltName = @alt_names
[alt_names]DNS.1 = app.internal.example.comDNS.2 = *.internal.example.comIP.1 = 192.168.1.10EOF
# Sign the server certificate with the CAopenssl x509 -req -in server.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out server.crt -days 825 -sha256 \ -extfile server-ext.cnf
# 3. Trust the internal CA on client machinessudo cp ca.crt /etc/pki/ca-trust/source/anchors/mycompany-ca.crtsudo update-ca-trustViewing and Verifying Certificates
Section titled “Viewing and Verifying Certificates”Viewing Certificate Information
Section titled “Viewing Certificate Information”# View certificate detailsopenssl x509 -in /etc/pki/tls/certs/selfsigned.crt -text -noout
# View key information onlyopenssl x509 -in cert.pem -noout -subject -issuer -dates
# View a remote server's certificateopenssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \ openssl x509 -text -noout
# View expiration dates onlyecho | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \ openssl x509 -noout -datesVerifying the Certificate Chain
Section titled “Verifying the Certificate Chain”# Verify that the certificate and private key matchopenssl x509 -noout -modulus -in cert.pem | openssl md5openssl rsa -noout -modulus -in key.pem | openssl md5# Both MD5 values should be identical
# Verify certificate chain integrityopenssl verify -CAfile ca.crt -untrusted intermediate.crt server.crt
# Test the HTTPS connectioncurl -vI https://example.com 2>&1 | grep -A 5 "SSL connection"Troubleshooting SSL Issues
Section titled “Troubleshooting SSL Issues”Common Problems and Solutions
Section titled “Common Problems and Solutions”Problem 1: Incomplete certificate chain
# Symptom: browser shows "certificate not trusted" even though the certificate hasn't expired# Check:openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>&1 | \ grep -i "verify"
# Solution: ensure the full certificate chain is configured# In Nginx, ssl_certificate should use fullchain.pem, not cert.pemcat cert.pem intermediate.pem > fullchain.pemProblem 2: Certificate expired
# Check certificate expiration dateecho | openssl s_client -connect example.com:443 2>/dev/null | \ openssl x509 -noout -enddate
# Batch check multiple domainsfor domain in example.com api.example.com www.example.com; do expiry=$(echo | openssl s_client -connect ${domain}:443 -servername ${domain} 2>/dev/null | \ openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) echo "${domain}: ${expiry}"done
# Check if certbot renewal is workingsudo certbot renew --dry-runjournalctl -u certbot-renewProblem 3: Protocol or cipher suite incompatibility
# Test supported TLS versionsopenssl s_client -connect example.com:443 -tls1_2 < /dev/nullopenssl s_client -connect example.com:443 -tls1_3 < /dev/null
# List cipher suites supported by the server (requires nmap)nmap --script ssl-enum-ciphers -p 443 example.comProblem 4: Nginx configuration error
# Check configuration syntaxsudo nginx -t
# Common errors:# 1. Private key doesn't match the certificate# 2. Incorrect file paths# 3. Permission issues (private key file permissions should be 600)
ls -la /etc/letsencrypt/live/example.com/Problem 5: SELinux blocking access to certificate files
# Check SELinux audit logsausearch -m avc -ts recent | grep ssl
# Restore correct SELinux contextssudo restorecon -Rv /etc/letsencrypt/sudo restorecon -Rv /etc/pki/tls/Certificate Expiration Monitoring Script
Section titled “Certificate Expiration Monitoring Script”#!/bin/bash# Monitor SSL certificate expiration
DOMAINS=( "example.com" "api.example.com" "www.example.com")WARN_DAYS=30
for domain in "${DOMAINS[@]}"; do expiry_date=$(echo | openssl s_client -connect "${domain}:443" \ -servername "$domain" 2>/dev/null | \ openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry_date" ]; then echo "[ERROR] Unable to retrieve certificate info for ${domain}" continue fi
expiry_epoch=$(date -d "$expiry_date" +%s) now_epoch=$(date +%s) days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -le 0 ]; then MSG="[CRITICAL] ${domain} certificate has expired!" echo "$MSG" echo "$MSG" | mail -s "SSL Certificate Expiration Alert" "$MAILTO" elif [ "$days_left" -le "$WARN_DAYS" ]; then MSG="[WARNING] ${domain} certificate expires in ${days_left} days (${expiry_date})" echo "$MSG" echo "$MSG" | mail -s "SSL Certificate Expiring Soon" "$MAILTO" else echo "[OK] ${domain} certificate has ${days_left} days remaining (${expiry_date})" fidonechmod +x /usr/local/bin/check_ssl.sh
# Add to cron for daily checksecho '0 9 * * * /usr/local/bin/check_ssl.sh >> /var/log/ssl_check.log 2>&1' | sudo crontab -Security Best Practices
Section titled “Security Best Practices”Recommended SSL Configuration
Section titled “Recommended SSL Configuration”# Generate DH parameters (enhances forward secrecy)sudo openssl dhparam -out /etc/pki/tls/dhparam.pem 2048
# Reference in Nginx# ssl_dhparam /etc/pki/tls/dhparam.pem;Security Checklist
Section titled “Security Checklist”- Enable only TLS 1.2 and TLS 1.3 (disable SSLv3, TLS 1.0, TLS 1.1)
- Use strong cipher suites and disable weak ciphers
- Configure the full certificate chain
- Enable the HSTS header
- Enable OCSP Stapling
- Set private key file permissions to 600
- Configure automatic renewal and verify it works correctly
- Regularly check certificate expiration dates
- Use SSL Labs online testing to evaluate your configuration
Online Testing
Section titled “Online Testing”After configuration, use the following tools to test your SSL configuration quality:
# SSL Labs (Web)# Visit https://www.ssllabs.com/ssltest/ and enter your domain
# Command-line testingcurl -I https://example.comcurl -vI https://example.com 2>&1 | grep -E "SSL|TLS|subject|expire"The goal is to achieve an A or A+ rating on the SSL Labs test.