Skip to content
Published on

SSL/TLS Certificate Complete Guide: From Issuance to Auto-Renewal and Troubleshooting

Authors
  • Name
    Twitter

Introduction

HTTPS is no longer optional -- it is essential. Browsers display "Not Secure" warnings on HTTP sites, and search engines use HTTPS as a ranking signal. However, properly understanding and operating SSL/TLS certificates is more complex than you might think. There are many easy-to-miss details, including certificate type selection, correct configuration, auto-renewal, and chain construction.

This article covers everything from the fundamentals of SSL/TLS to real-world operations in one place.

1. How SSL/TLS Works

Symmetric vs Asymmetric Encryption

SSL/TLS uses both encryption methods together.

CategorySymmetric EncryptionAsymmetric Encryption
KeySingle key for encrypt/decryptPublic key (encrypt) + Private key (decrypt)
SpeedFastSlow (100~1000x)
AlgorithmAES-256, ChaCha20RSA, ECDSA, Ed25519
PurposeActual data transmissionKey exchange, certificate signing
Key Length128/256 bit2048/4096 bit (RSA)

TLS 1.3 Handshake Process

In TLS 1.3, the handshake has been reduced to 1-RTT.

Client                                           Server
  |                                                 |
  |--- ClientHello (supported ciphers, key share) ->|
  |                                                 |
  |<-- ServerHello (chosen cipher, key share,       |
  |    EncryptedExtensions, Certificate,            |
  |    CertificateVerify, Finished) ---------------|
  |                                                 |
  |--- Finished (encrypted) ---------------------->|
  |                                                 |
  |<========= Application Data (encrypted) =======>|

Key improvements in TLS 1.3:

  1. 1-RTT Handshake: Reduced from 2-RTT in TLS 1.2 to 1-RTT
  2. 0-RTT Resumption: Data can be sent without additional delay on reconnection
  3. Enhanced Security: Removed vulnerable cipher suites (RC4, 3DES, CBC mode, etc.)
  4. Mandatory Forward Secrecy: All key exchanges use ECDHE or DHE

Certificate Verification Flow

[Browser] -> Receives server certificate
    -> Checks certificate validity period
    -> Verifies domain name match
    -> Validates certificate chain (Server cert -> Intermediate CA -> Root CA)
    -> Checks revocation status via OCSP/CRL
    -> Verification successful -> Displays padlock icon

2. Certificate Types

Classification by Validation Level

TypeValidation LevelIssuance TimeCostUse Case
DV (Domain Validation)Domain ownership onlyMinutesFree~LowPersonal blogs, small services
OV (Organization Validation)Organization existence1~3 daysMediumCorporate websites
EV (Extended Validation)Legal entity verification1~2 weeksHighFinance, e-commerce

Classification by Coverage

TypeCoverageExampleCost
Single DomainOne FQDNwww.example.comLowest
WildcardAll subdomains at the same level*.example.comMedium
SAN/Multi-DomainMultiple domains in one certificateexample.com, example.orgAdditional cost per domain

Wildcard certificate considerations:

# What *.example.com covers
www.example.com    # O Covered
api.example.com    # O Covered
example.com        # X Not covered (must be added separately to SAN)
sub.api.example.com # X Not covered (second-level subdomain)

3. Let's Encrypt + Certbot in Practice

Installing Certbot

# Ubuntu/Debian
sudo apt update
sudo apt install -y certbot python3-certbot-nginx

# CentOS/Rocky Linux
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx

# macOS (for development)
brew install certbot

# Docker
docker run -it --rm \
  -v /etc/letsencrypt:/etc/letsencrypt \
  -v /var/lib/letsencrypt:/var/lib/letsencrypt \
  certbot/certbot certonly --help

Certificate Issuance Methods

Webroot Method (No Service Downtime)

# Issue while Nginx is running
sudo certbot certonly --webroot \
  -w /var/www/html \
  -d example.com \
  -d www.example.com \
  --email admin@example.com \
  --agree-tos \
  --no-eff-email

# Must add .well-known path in Nginx configuration
# location /.well-known/acme-challenge/ {
#     root /var/www/html;
# }

Standalone Method (Requires Port 80)

# Must temporarily stop the service using port 80
sudo systemctl stop nginx
sudo certbot certonly --standalone \
  -d example.com \
  -d www.example.com
sudo systemctl start nginx

DNS Method (Required for Wildcard Certificates)

# Verify via DNS TXT record
sudo certbot certonly --manual \
  --preferred-challenges dns \
  -d "*.example.com" \
  -d example.com

# Use DNS plugin for automation (Cloudflare example)
sudo pip install certbot-dns-cloudflare

# Create API token file
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini

# Automated issuance via Cloudflare DNS
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.example.com" \
  -d example.com

Issued Certificate File Structure

/etc/letsencrypt/live/example.com/
├── cert.pem       # Server certificate
├── chain.pem      # Intermediate CA certificate
├── fullchain.pem  # cert.pem + chain.pem (used by Nginx)
├── privkey.pem    # Private key
└── README

4. Nginx SSL Configuration (A+ Grade)

Basic SSL Configuration

# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    # HTTP -> HTTPS redirect
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

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

    # SSL protocols (allow only TLS 1.2 and 1.3)
    ssl_protocols TLSv1.2 TLSv1.3;

    # Cipher suites (server preference)
    ssl_prefer_server_ciphers on;
    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';

    # ECDH curves
    ssl_ecdh_curve X25519:prime256v1:secp384r1;

    # SSL session cache
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # 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;
    resolver_timeout 5s;

    # DH parameters (2048 bits or more)
    ssl_dhparam /etc/nginx/dhparam.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;

    root /var/www/example.com;
    index index.html;
}

Generating DH Parameters

# Generate 2048-bit DH parameters (takes about 1~2 minutes)
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048

# 4096-bit (more secure but takes 5~10 minutes)
sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096

SSL Labs Test

# Validate configuration
sudo nginx -t

# Apply configuration
sudo systemctl reload nginx

# Verify A+ grade on SSL Labs
# https://www.ssllabs.com/ssltest/analyze.html?d=example.com

# Verify via CLI
curl -sS https://api.ssllabs.com/api/v3/analyze?host=example.com | jq '.endpoints[0].grade'

5. AWS ACM Certificate Management

ACM Certificate Issuance

# Request certificate via AWS CLI
aws acm request-certificate \
  --domain-name example.com \
  --subject-alternative-names "*.example.com" \
  --validation-method DNS \
  --region ap-northeast-2

# Check validation status
aws acm describe-certificate \
  --certificate-arn arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123 \
  --query 'Certificate.DomainValidationOptions'

Automating ACM + Route53 with Terraform

# ACM certificate resource
resource "aws_acm_certificate" "main" {
  domain_name               = "example.com"
  subject_alternative_names = ["*.example.com"]
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }

  tags = {
    Environment = "production"
    ManagedBy   = "terraform"
  }
}

# Auto-create Route53 DNS validation records
resource "aws_route53_record" "cert_validation" {
  for_each = {
    for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  allow_overwrite = true
  name            = each.value.name
  records         = [each.value.record]
  ttl             = 60
  type            = each.value.type
  zone_id         = data.aws_route53_zone.main.zone_id
}

# Wait for certificate validation to complete
resource "aws_acm_certificate_validation" "main" {
  certificate_arn         = aws_acm_certificate.main.arn
  validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}

# Attach certificate to ALB
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port              = 443
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate_validation.main.certificate_arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

ACM vs Let's Encrypt Comparison

ItemAWS ACMLet's Encrypt
CostFree (when used with AWS services)Free
Validity13 months (auto-renewal)90 days (auto-renewal required)
WildcardSupportedSupported (DNS validation required)
ScopeAWS ALB, CloudFront, API GWCan be used anywhere
Certificate ExportNot possible (except Private CA)Possible
Management OverheadAlmost noneCertbot management required

6. Certificate Auto-Renewal

Renewal with Cron

# Test Certbot renewal
sudo certbot renew --dry-run

# Register cron job
sudo crontab -e

# Check for renewal daily at 3 AM (auto-renews when expiring within 30 days)
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
# /etc/systemd/system/certbot-renewal.service
cat > /etc/systemd/system/certbot-renewal.service << 'EOF'
[Unit]
Description=Certbot Renewal
After=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
ExecStartPost=/bin/bash -c 'echo "Certbot renewal completed at $(date)" >> /var/log/certbot-renewal.log'
EOF

# /etc/systemd/system/certbot-renewal.timer
cat > /etc/systemd/system/certbot-renewal.timer << 'EOF'
[Unit]
Description=Run certbot renewal twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true

[Install]
WantedBy=timers.target
EOF

# Enable
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renewal.timer

# Check timer status
sudo systemctl list-timers | grep certbot

Renewal Monitoring Script

#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh

DOMAINS=("example.com" "api.example.com" "admin.example.com")
ALERT_DAYS=14
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

for domain in "${DOMAINS[@]}"; do
    expiry_date=$(echo | openssl s_client -servername "$domain" -connect "$domain:443" 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null \
        | cut -d= -f2)

    if [ -z "$expiry_date" ]; then
        echo "ERROR: Cannot connect to $domain"
        continue
    fi

    expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry_date" +%s)
    current_epoch=$(date +%s)
    days_left=$(( (expiry_epoch - current_epoch) / 86400 ))

    echo "$domain: $days_left days until expiry ($expiry_date)"

    if [ "$days_left" -lt "$ALERT_DAYS" ]; then
        curl -s -X POST "$SLACK_WEBHOOK" \
            -H 'Content-type: application/json' \
            -d "{\"text\":\"SSL Certificate Expiry Warning: $domain - ${days_left} days remaining\"}"
    fi
done
# Grant execution permission and register cron
chmod +x /usr/local/bin/check-ssl-expiry.sh
echo "0 9 * * * /usr/local/bin/check-ssl-expiry.sh" | sudo crontab -

7. Certificate Debugging Command Collection

# Check certificate information
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -text -noout

# Check expiry date only
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -enddate

# Check remote server certificate
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
    | openssl x509 -noout -text

# Check certificate chain
echo | openssl s_client -showcerts -servername example.com -connect example.com:443 2>/dev/null

# Verify certificate and private key match
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in privkey.pem | openssl md5
# If both values match, they correspond

# Check certificate SAN
openssl x509 -in cert.pem -noout -ext subjectAltName

# PEM -> PFX(PKCS12) conversion
openssl pkcs12 -export -out cert.pfx \
    -inkey privkey.pem -in cert.pem -certfile chain.pem

# PFX -> PEM conversion
openssl pkcs12 -in cert.pfx -out cert.pem -nodes

8. Troubleshooting

Issue 1: Certificate Expired (ERR_CERT_DATE_INVALID)

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

# Force immediate renewal
sudo certbot renew --force-renewal --cert-name example.com
sudo systemctl reload nginx

Issue 2: Incomplete Certificate Chain (ERR_CERT_AUTHORITY_INVALID)

# Verify chain
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
    /etc/letsencrypt/live/example.com/fullchain.pem

# Verify Nginx is using fullchain.pem
# Incorrect configuration:
# ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
# Correct configuration:
# ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;

Issue 3: Mixed Content Warning

# Find http:// resources in HTML
grep -rn "http://" /var/www/html/ --include="*.html" --include="*.js" --include="*.css"

# Auto-upgrade with CSP header
# Add to Nginx configuration:
# add_header Content-Security-Policy "upgrade-insecure-requests;" always;

Issue 4: SSL Handshake Failure

# Check supported protocols
nmap --script ssl-enum-ciphers -p 443 example.com

# Test connection with specific TLS version
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

# Client does not support the cipher suite
# -> Add compatible suites to ssl_ciphers configuration
# Once HSTS header is set, you cannot go back to HTTP
# Use a short max-age during testing
add_header Strict-Transport-Security "max-age=300" always;

# Use a sufficiently long value in production
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Issue 6: Let's Encrypt Rate Limits

# Rate Limit details
# - Same domain: 5 issuances per week (excluding renewals)
# - Duplicate certificates: 5 per week
# - Per account: 300 requests per 3 hours

# Use staging server for testing
sudo certbot certonly --staging \
  --webroot -w /var/www/html \
  -d test.example.com

# Delete staging certificate and issue production certificate
sudo certbot delete --cert-name test.example.com

9. Advanced Configuration

mTLS (Mutual TLS Authentication)

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    # Require client certificate
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    location / {
        # Forward client certificate info to backend
        proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;
        proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
        proxy_pass http://backend;
    }
}

Generating Client Certificates

# Generate CA key and certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
    -subj "/C=KR/ST=Seoul/O=MyOrg/CN=Internal CA"

# Generate client key and CSR
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
    -subj "/C=KR/ST=Seoul/O=MyOrg/CN=client1"

# Issue client certificate
openssl x509 -req -days 365 \
    -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
    -out client.crt

# Test
curl --cert client.crt --key client.key https://api.example.com/

Certificate Transparency (CT) Log Check

# Search domain certificates in CT logs
# https://crt.sh/?q=example.com

# Verify via CLI
curl -s "https://crt.sh/?q=example.com&output=json" | jq '.[0:5] | .[] | {id, name_value, not_after}'

10. Operations Checklists

Initial Setup Checklist

  • Allow only TLS 1.2 and above (ssl_protocols TLSv1.2 TLSv1.3)
  • Use only strong cipher suites (ECDHE + AES-GCM/ChaCha20)
  • Configure HSTS header (Strict-Transport-Security)
  • HTTP -> HTTPS redirect (301)
  • Enable OCSP Stapling
  • Generate DH parameters with 2048 bits or more
  • Verify A+ grade on SSL Labs
  • Confirm no Mixed Content

Auto-Renewal Checklist

  • Configure Certbot auto-renewal (cron or systemd timer)
  • Test renewal (certbot renew --dry-run)
  • Set up Nginx reload hook after renewal
  • Deploy expiry monitoring script
  • Configure Slack/Email alerts (14 days before expiry)

Regular Inspection Checklist

  • Check certificate expiry dates monthly
  • Re-test with SSL Labs quarterly
  • Check for new security vulnerability patches (OpenSSL, Nginx)
  • Monitor CT logs for unauthorized certificate issuances
  • Review security header changes

Incident Response Checklist

  • Document immediate renewal procedures for certificate expiry
  • Familiarize with certificate revocation procedures
  • Prepare emergency certificate re-issuance procedures
  • Establish rollback plan (backup previous certificates)
  • Designate notification channels for incidents

Conclusion

SSL/TLS certificate management is not a one-time setup. Let's Encrypt's 90-day validity policy is a good practice that enforces automation. Based on the content covered in this article, we encourage you to build a secure and automated certificate management system.

Key Takeaways:

  1. Use TLS 1.3 as default, while allowing TLS 1.2 for compatibility
  2. Let's Encrypt + Certbot for free certificate automated issuance and renewal
  3. Nginx configuration optimized for SSL Labs A+ grade
  4. Auto-renewal configured with systemd timer with mandatory monitoring
  5. Troubleshooting starts with the openssl s_client command