Skip to content
Search RunEntLinux

Searches general documentation on RunEntLinux. Each distribution has its own documentation search.

    Browse this documentation

    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 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.

    ExtensionDescription
    .pemBase64-encoded certificate/key (most common)
    .crt / .cerCertificate file (usually PEM format)
    .keyPrivate key file
    .csrCertificate Signing Request
    .p12 / .pfxPKCS#12 format (contains certificate + private key)

    Let’s Encrypt provides free DV (Domain Validation) certificates, valid for 90 days, with support for automatic renewal.

    Terminal window
    # Install certbot and the corresponding web server plugin
    sudo dnf install epel-release -y
    sudo dnf install certbot -y
    # Choose the plugin based on your web server
    sudo dnf install python3-certbot-nginx -y # Nginx
    # or
    sudo dnf install python3-certbot-apache -y # Apache
    Terminal window
    # Automatically obtain and configure (recommended)
    # certbot will automatically modify your Nginx configuration and reload
    sudo 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 HTTPS
    Terminal window
    sudo certbot --apache -d example.com -d www.example.com

    Obtaining a Certificate Only (Without Modifying Configuration)

    Section titled “Obtaining a Certificate Only (Without Modifying Configuration)”

    If you want to configure the web server manually:

    Terminal window
    # 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.com

    Certificate 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 key
    Terminal window
    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 nginx

    Wildcard certificates (*.example.com) require DNS validation:

    Terminal window
    # Obtain a wildcard certificate using DNS validation
    sudo 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 continue

    For automated scenarios, use DNS plugins:

    Terminal window
    # Using Cloudflare as an example
    sudo dnf install python3-certbot-dns-cloudflare -y
    # Create the API credentials file
    sudo mkdir -p /etc/letsencrypt
    sudo tee /etc/letsencrypt/cloudflare.ini > /dev/null <<'EOF'
    dns_cloudflare_api_token = your_cloudflare_api_token
    EOF
    sudo chmod 600 /etc/letsencrypt/cloudflare.ini
    # Automatically obtain a wildcard certificate
    sudo certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
    -d "example.com" \
    -d "*.example.com"

    Common DNS plugins:

    Terminal window
    # Alibaba Cloud DNS (third-party plugin)
    pip install certbot-dns-aliyun
    # Cloudflare
    dnf install python3-certbot-dns-cloudflare
    # Route53 (AWS)
    dnf install python3-certbot-dns-route53

    Certbot configures a systemd timer for automatic renewal by default after installation:

    Terminal window
    # Check the renewal timer status
    systemctl list-timers certbot*
    # Test renewal manually (dry run)
    sudo certbot renew --dry-run
    # If the timer does not exist, create it manually
    sudo tee /etc/systemd/system/certbot-renew.timer > /dev/null <<'EOF'
    [Unit]
    Description=Certbot Automatic Renewal Timer
    [Timer]
    OnCalendar=*-*-* 00,12:00:00
    RandomizedDelaySec=3600
    Persistent=true
    [Install]
    WantedBy=timers.target
    EOF
    sudo tee /etc/systemd/system/certbot-renew.service > /dev/null <<'EOF'
    [Unit]
    Description=Certbot Automatic Renewal
    [Service]
    Type=oneshot
    ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
    EOF
    sudo systemctl daemon-reload
    sudo systemctl enable --now certbot-renew.timer

    The --deploy-hook ensures the web server is automatically reloaded after the certificate is updated.

    Terminal window
    # List all certificates
    sudo certbot certificates
    # Delete a certificate
    sudo certbot delete --cert-name example.com
    # Expand certificate domains
    sudo certbot certonly --nginx \
    --cert-name example.com \
    -d example.com \
    -d www.example.com \
    -d api.example.com

    Self-signed certificates are suitable for internal services, development, and testing scenarios that do not require public trust.

    Terminal window
    # 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 permissions
    sudo chmod 600 /etc/pki/tls/private/selfsigned.key
    sudo chmod 644 /etc/pki/tls/certs/selfsigned.crt

    For multiple internal servers, it is recommended to create your own CA so that all issued certificates are uniformly trusted:

    Terminal window
    # 1. Create the CA private key and certificate
    mkdir -p /etc/pki/internal-ca
    cd /etc/pki/internal-ca
    # Generate the CA private key
    openssl 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 key
    openssl 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,issuer
    basicConstraints=CA:FALSE
    keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
    subjectAltName = @alt_names
    [alt_names]
    DNS.1 = app.internal.example.com
    DNS.2 = *.internal.example.com
    IP.1 = 192.168.1.10
    EOF
    # Sign the server certificate with the CA
    openssl 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 machines
    sudo cp ca.crt /etc/pki/ca-trust/source/anchors/mycompany-ca.crt
    sudo update-ca-trust
    Terminal window
    # View certificate details
    openssl x509 -in /etc/pki/tls/certs/selfsigned.crt -text -noout
    # View key information only
    openssl x509 -in cert.pem -noout -subject -issuer -dates
    # View a remote server's certificate
    openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
    openssl x509 -text -noout
    # View expiration dates only
    echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
    openssl x509 -noout -dates
    Terminal window
    # Verify that the certificate and private key match
    openssl x509 -noout -modulus -in cert.pem | openssl md5
    openssl rsa -noout -modulus -in key.pem | openssl md5
    # Both MD5 values should be identical
    # Verify certificate chain integrity
    openssl verify -CAfile ca.crt -untrusted intermediate.crt server.crt
    # Test the HTTPS connection
    curl -vI https://example.com 2>&1 | grep -A 5 "SSL connection"

    Problem 1: Incomplete certificate chain

    Terminal window
    # 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.pem
    cat cert.pem intermediate.pem > fullchain.pem

    Problem 2: Certificate expired

    Terminal window
    # Check certificate expiration date
    echo | openssl s_client -connect example.com:443 2>/dev/null | \
    openssl x509 -noout -enddate
    # Batch check multiple domains
    for 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 working
    sudo certbot renew --dry-run
    journalctl -u certbot-renew

    Problem 3: Protocol or cipher suite incompatibility

    Terminal window
    # Test supported TLS versions
    openssl s_client -connect example.com:443 -tls1_2 < /dev/null
    openssl 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.com

    Problem 4: Nginx configuration error

    Terminal window
    # Check configuration syntax
    sudo 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

    Terminal window
    # Check SELinux audit logs
    ausearch -m avc -ts recent | grep ssl
    # Restore correct SELinux contexts
    sudo restorecon -Rv /etc/letsencrypt/
    sudo restorecon -Rv /etc/pki/tls/
    /usr/local/bin/check_ssl.sh
    #!/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})"
    fi
    done
    Terminal window
    chmod +x /usr/local/bin/check_ssl.sh
    # Add to cron for daily checks
    echo '0 9 * * * /usr/local/bin/check_ssl.sh >> /var/log/ssl_check.log 2>&1' | sudo crontab -
    Terminal window
    # 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;
    • 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

    After configuration, use the following tools to test your SSL configuration quality:

    Terminal window
    # SSL Labs (Web)
    # Visit https://www.ssllabs.com/ssltest/ and enter your domain
    # Command-line testing
    curl -I https://example.com
    curl -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.