Certificate Management
Introduction
TLS certificate management is a critical operational responsibility. Expired certificates cause service outages, security warnings, and loss of user trust. Modern certificate management leverages the ACME protocol and Let's Encrypt to automate issuance and renewal at scale.
Let's Encrypt
Let's Encrypt is a free, automated, and open certificate authority (CA) that provides DV certificates trusted by all major browsers.
# Install Certbot (Let's Encrypt client)
sudo apt install certbot python3-certbot-nginx
# Obtain certificate with webroot authentication
sudo certbot certonly --webroot \
-w /var/www/example.com -d example.com \
-w /var/www/api.example.com -d api.example.com \
--email admin@example.com \
--agree-tos \
--non-interactive
# Obtain certificate with DNS challenge (for wildcards)
sudo certbot certonly --manual \
--preferred-challenges dns \
-d example.com -d *.example.com
ACME Protocol
The Automated Certificate Management Environment (ACME) protocol automates certificate issuance, renewal, and revocation.
import josepy as jose
from acme import client, messages
from cryptography import x509
from cryptography.hazmat.primitives import hashes
class ACMEClient:
def __init__(self, directory_url, email):
self.directory_url = directory_url
self.email = email
self.net = client.ClientNetwork(
jose.JWKRSA(key=rsa_private_key),
user_agent="my-acme-client/1.0"
)
self.directory = messages.Directory.from_json(
self.net.get(directory_url).json()
)
self.acme = client.ClientV2(self.directory, self.net)
def register_account(self):
"""Register ACME account with Let's Encrypt."""
terms = self.directory.meta.terms_of_service
registration = self.acme.new_account(
messages.NewRegistration(
key=self.net.key,
terms_of_service_agreed=True,
contact=[f"mailto:{self.email}"]
)
)
return registration
def request_certificate(self, domain, csr_pem):
"""Request certificate issuance."""
# Create authorization
order = self.acme.new_order(csr_pem)
# Complete challenges for each identifier
for auth in order.authorizations:
# HTTP-01 or DNS-01 challenge
challenge = auth.body.challenges[0]
self.respond_challenge(challenge)
# Finalize order
order = self.acme.finalize_order(order, csr_pem)
return order.fullchain_pem
Automated Renewal
Certificate renewal should be fully automated with monitoring and alerting.
# Certbot systemd timer for automatic renewal
# /etc/systemd/system/certbot-renewal.service
[Unit]
Description=Certbot Renewal
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --pre-hook "systemctl reload nginx"
ExecStartPost=/usr/bin/systemctl reload nginx
# /etc/systemd/system/certbot-renewal.timer
[Unit]
Description=Run certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 00:00,12:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
# Enable timer
sudo systemctl enable certbot-renewal.timer
sudo systemctl start certbot-renewal.timer
# Test renewal process
sudo certbot renew --dry-run
Certificate Monitoring
Monitor certificate expiration to catch renewal failures before they cause outages.
import ssl
import datetime
import socket
from typing import Dict, List
class CertificateMonitor:
def __init__(self, warning_days: int = 30, critical_days: int = 7):
self.warning_days = warning_days
self.critical_days = critical_days
def check_certificate(self, hostname: str, port: int = 443) -> Dict:
context = ssl.create_default_context()
context.check_hostname = True
with socket.create_connection((hostname, port), timeout=10) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
# Parse expiration
expires = datetime.datetime.strptime(
cert['notAfter'], '%b %d %H:%M:%S %Y %Z'
)
remaining = (expires - datetime.datetime.utcnow()).days
# Check issuer
issuer = dict(x[0] for x in cert['issuer'])
# Check SANs
sans = [san[1] for san in cert.get('subjectAltName', [])]
return {
'hostname': hostname,
'subject': cert['subject'],
'issuer': issuer.get('organizationName', 'Unknown'),
'expires': expires.isoformat(),
'remaining_days': remaining,
'sans': sans,
'serial': cert.get('serialNumber'),
'status': self._get_status(remaining),
}
def _get_status(self, remaining_days: int) -> str:
if remaining_days <= 0:
return 'EXPIRED'
elif remaining_days <= self.critical_days:
return 'CRITICAL'
elif remaining_days <= self.warning_days:
return 'WARNING'
return 'OK'
def monitor_domains(self, domains: List[str]) -> List[Dict]:
results = []
for domain in domains:
try:
result = self.check_certificate(domain)
results.append(result)
if result['status'] in ('CRITICAL', 'EXPIRED'):
self.alert(result)
except Exception as e:
results.append({
'hostname': domain,
'status': 'ERROR',
'error': str(e)
})
return results
# OpenSSL-based certificate check
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -enddate -subject -issuer
# Mass certificate check
for domain in $(cat domains.txt); do
expires=$(echo | openssl s_client -connect $domain:443 -servername $domain 2>/dev/null | \
openssl x509 -noout -enddate | cut -d= -f2)
remaining=$(( ($(date -d "$expires" +%s) - $(date +%s)) / 86400 ))
echo "$domain: expires in $remaining days ($expires)"
done
Certificate Revocation
# Check OCSP status
openssl ocsp -issuer chain.pem -cert cert.pem \
-url $(openssl x509 -in cert.pem -noout -ocsp_uri) \
-header "Host" $(openssl x509 -in cert.pem -noout -ocsp_uri | cut -d/ -f3) \
-CAfile root.pem
# CRL check
curl -O http://crl.example.com/intermediate.crl
openssl crl -in intermediate.crl -noout -text | grep -A1 "Serial Number"
Conclusion
Modern certificate management means automation. Use Let's Encrypt with Certbot for domain-validated certificates, implement ACME for custom automation, set up systemd timers for renewal, and deploy monitoring that alerts days or weeks before expiration. Never rely on manual renewal processes — they fail under pressure and at scale.