SSL/TLS: Certificate Management and Configuration

SSL/TLS certificates are the foundation of encrypted web communication, but they're frequently misunderstood. At their core, certificates bind a public key to an identity through a chain of trust....

Key Insights

  • Certificate management is critical infrastructure work—automate everything from acquisition to renewal, or you’ll face outages when certificates expire at 2 AM.
  • Modern TLS configuration requires aggressive deprecation of old protocols and ciphers; supporting TLS 1.0/1.1 in 2024 is a security liability, not a compatibility feature.
  • Mutual TLS (mTLS) provides the strongest authentication model for service-to-service communication, but introduces operational complexity that requires proper tooling and monitoring.

SSL/TLS Fundamentals and Certificate Basics

SSL/TLS certificates are the foundation of encrypted web communication, but they’re frequently misunderstood. At their core, certificates bind a public key to an identity through a chain of trust. When a client connects to your server, the TLS handshake validates this chain, verifies the certificate hasn’t expired, and establishes an encrypted session.

The certificate chain typically consists of three components: your server certificate (leaf certificate), intermediate certificates from your Certificate Authority (CA), and a root certificate that clients inherently trust. Each certificate is signed by the one above it in the chain, creating cryptographic proof of authenticity.

Every certificate contains critical metadata: the subject (who it identifies), the issuer (who signed it), validity dates, the public key, and extensions like Subject Alternative Names (SANs) that allow one certificate to cover multiple domains.

Examine any certificate’s details with OpenSSL:

openssl x509 -in certificate.crt -text -noout

This reveals everything: the signature algorithm, key size, validity period, and extensions. Pay attention to the “Subject Alternative Name” field—this is where additional domains are listed. Modern browsers ignore the Common Name (CN) field entirely, making SANs mandatory for multi-domain certificates.

Certificate Acquisition and Generation

For production systems, Let’s Encrypt has revolutionized certificate management by providing free, automated certificates with 90-day validity periods. The short validity period is a feature, not a bug—it forces automation and limits the damage window if a private key is compromised.

Install Certbot and obtain a certificate:

# Install certbot (Ubuntu/Debian)
sudo apt-get install certbot python3-certbot-nginx

# Obtain and install certificate for Nginx
sudo certbot --nginx -d example.com -d www.example.com

# Or use standalone mode (requires port 80)
sudo certbot certonly --standalone -d example.com

Certbot handles the ACME protocol challenge, obtains the certificate, and can automatically configure your web server. For DNS-based validation (useful when port 80 isn’t accessible), use DNS plugins:

sudo certbot certonly --dns-cloudflare \
  --dns-cloudflare-credentials ~/.secrets/cloudflare.ini \
  -d example.com -d "*.example.com"

For development environments, self-signed certificates are appropriate. Never use self-signed certificates in production—the trust warnings train users to ignore security indicators.

Generate a self-signed certificate:

# Generate private key and certificate in one command
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -days 365 -nodes -subj "/CN=localhost"

# Or create a CSR first (for CA submission)
openssl req -new -newkey rsa:4096 -nodes -keyout private.key \
  -out request.csr -subj "/C=US/ST=State/L=City/O=Org/CN=example.com"

The CSR (Certificate Signing Request) approach is what you’ll use with commercial CAs. You generate the CSR, submit it to the CA, and receive back a signed certificate. The private key never leaves your server—a critical security principle.

Web Server Configuration

Obtaining a certificate is half the battle. Configuration determines whether you’re actually secure or just displaying a padlock icon.

Here’s a modern Nginx configuration:

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

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

    # Modern cipher suite (TLS 1.2+)
    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;

    # Performance optimizations
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;

    location / {
        proxy_pass http://backend;
    }
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

Key configuration decisions:

  • TLS 1.3 only if possible, TLS 1.2 minimum. TLS 1.0/1.1 are deprecated and vulnerable.
  • Disable ssl_prefer_server_ciphers for TLS 1.3 to let clients choose the best cipher they support.
  • Enable OCSP stapling to allow clients to verify certificate revocation status without contacting the CA.
  • HSTS header tells browsers to only connect via HTTPS for the specified period.

For Apache, the equivalent configuration:

<VirtualHost *:443>
    ServerName example.com
    
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/cert.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
    SSLCertificateChainFile /etc/letsencrypt/live/example.com/chain.pem
    
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
    SSLHonorCipherOrder off
    
    SSLUseStapling on
    Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>

Certificate Renewal and Automation

Let’s Encrypt certificates expire after 90 days. Manual renewal is operational malpractice. Automate it.

Certbot includes automatic renewal, but verify it’s configured:

# Test renewal process (dry run)
sudo certbot renew --dry-run

# Check systemd timer (on systems using systemd)
sudo systemctl list-timers | grep certbot

# Or add cron job manually
echo "0 0,12 * * * root certbot renew --quiet" | sudo tee -a /etc/crontab

Monitor certificate expiration proactively. Here’s a Python script to check expiration dates:

import ssl
import socket
from datetime import datetime, timedelta

def check_certificate_expiry(hostname, port=443):
    context = ssl.create_default_context()
    with socket.create_connection((hostname, port), timeout=10) as sock:
        with context.wrap_socket(sock, server_hostname=hostname) as ssock:
            cert = ssock.getpeercert()
            
    expiry_date = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
    days_remaining = (expiry_date - datetime.now()).days
    
    if days_remaining < 30:
        print(f"WARNING: {hostname} expires in {days_remaining} days")
        return False
    
    print(f"OK: {hostname} expires in {days_remaining} days")
    return True

# Check multiple domains
domains = ['example.com', 'api.example.com']
for domain in domains:
    check_certificate_expiry(domain)

For Kubernetes environments, cert-manager automates certificate lifecycle management:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: default
spec:
  secretName: example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - example.com
    - www.example.com
  renewBefore: 720h  # Renew 30 days before expiry

Mutual TLS and Advanced Scenarios

Mutual TLS (mTLS) requires both client and server to present certificates, providing strong authentication for service-to-service communication. This is increasingly common in microservices architectures and zero-trust networks.

Configure Nginx to require client certificates:

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

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

    # Client certificate verification
    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    location / {
        # Pass client cert info to backend
        proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
        proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
        proxy_pass http://backend;
    }
}

Using client certificates with Python requests:

import requests

response = requests.get(
    'https://api.example.com/endpoint',
    cert=('/path/to/client.crt', '/path/to/client.key'),
    verify='/path/to/ca.crt'
)

For Docker registries with mTLS:

# /etc/docker/daemon.json
{
  "registry-mirrors": ["https://registry.example.com"],
  "insecure-registries": [],
  "tls": {
    "ca": "/etc/docker/certs.d/registry.example.com/ca.crt",
    "cert": "/etc/docker/certs.d/registry.example.com/client.cert",
    "key": "/etc/docker/certs.d/registry.example.com/client.key"
  }
}

Troubleshooting and Security Best Practices

When TLS breaks, start with OpenSSL’s s_client:

# Test connection and view certificate chain
openssl s_client -connect example.com:443 -servername example.com

# Check specific TLS version support
openssl s_client -connect example.com:443 -tls1_2

# Verify certificate chain
openssl s_client -connect example.com:443 -CAfile ca-bundle.crt

# Check cipher suite negotiation
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256'

Common issues and solutions:

  • Certificate chain incomplete: Include intermediate certificates in your bundle. Use fullchain.pem from Let’s Encrypt, not just cert.pem.
  • SNI problems: Always specify -servername with OpenSSL when testing servers hosting multiple domains.
  • Mixed content warnings: Ensure all resources (images, scripts, stylesheets) load via HTTPS.

Automate security testing with SSL Labs API:

# Trigger new scan
curl -s "https://api.ssllabs.com/api/v3/analyze?host=example.com&startNew=on"

# Check results
curl -s "https://api.ssllabs.com/api/v3/analyze?host=example.com" | jq .

Enable HSTS preloading for maximum security. After confirming your HTTPS setup works perfectly, submit your domain to the HSTS preload list. This hardcodes HTTPS-only access into browsers themselves.

Security headers checklist:

# Nginx security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

Certificate management isn’t glamorous, but it’s critical infrastructure. Automate everything, monitor proactively, and stay current with TLS best practices. The security of your entire application stack depends on getting this right.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.