# Ops Runbook: TLS Certificate Management This runbook documents procedures for diagnosing, renewing, and monitoring TLS certificates for CleverAgents infrastructure hostnames. ## Affected Hostnames | Hostname | Purpose | |---|---| | `git.cleverthis.com` | Primary Forgejo git server (production) | | `git.dev.cleveragents.com` | Development Forgejo git server | | `git.cleveragents.com` | Alias / secondary git endpoint | ## Diagnosing TLS Issues ### Symptom: `gnutls_handshake() failed: The server name sent was not recognized` This error indicates one of: 1. **Missing SAN** — The TLS certificate does not list the target hostname as a Subject Alternative Name (SAN). 2. **SNI misconfiguration** — The web server (nginx/caddy/traefik) is not routing the SNI hostname to the correct virtual host. 3. **Expired certificate** — The certificate has passed its `notAfter` date. ### Step 1: Inspect the Certificate Use `openssl s_client` to retrieve and inspect the certificate: ```bash # Check SANs and expiry for git.dev.cleveragents.com openssl s_client \ -connect git.dev.cleveragents.com:443 \ -servername git.dev.cleveragents.com \ 2>/dev/null | openssl x509 -noout -text | grep -A 10 "Subject Alternative Name" # Check expiry date openssl s_client \ -connect git.dev.cleveragents.com:443 \ -servername git.dev.cleveragents.com \ 2>/dev/null | openssl x509 -noout -dates ``` Or use the project's TLS check script (no external tools required): ```bash python scripts/check-tls-cert.py git.dev.cleveragents.com git.cleveragents.com ``` ### Step 2: Identify the Root Cause | Symptom | Root Cause | Fix | |---|---|---| | `CERTIFICATE_VERIFY_FAILED` with `hostname mismatch` | Hostname not in SANs | Reissue certificate with correct SANs | | `SSL_ERROR_RX_RECORD_TOO_LONG` | HTTP served on HTTPS port | Fix web server config | | `gnutls_handshake() failed: The server name sent was not recognized` | SNI virtual host not configured | Add SNI vhost binding | | Certificate expired | `notAfter` in the past | Renew certificate immediately | | `GIT_SSL_NO_VERIFY` bypasses but still fails | Server-side SNI rejection | Fix SNI routing (not a client cert issue) | ### Step 3: Verify SNI Routing If the certificate has the correct SANs but the error persists, the web server may not be routing the SNI hostname to the correct virtual host: ```bash # nginx: check for server_name directive grep -r "server_name.*git.dev.cleveragents.com" /etc/nginx/ # caddy: check Caddyfile grep -r "git.dev.cleveragents.com" /etc/caddy/ # traefik: check router rules grep -r "git.dev.cleveragents.com" /etc/traefik/ ``` --- ## Certificate Renewal Procedure ### Let's Encrypt (Certbot) — Recommended Most CleverAgents infrastructure uses Let's Encrypt certificates managed by certbot. The renewal process is: #### 1. Verify certbot is installed and configured ```bash certbot --version certbot certificates ``` #### 2. Check which certificate covers the affected hostname ```bash certbot certificates | grep -A 5 "git.dev.cleveragents.com" ``` #### 3. Renew the certificate ```bash # Dry run first to verify renewal will succeed certbot renew --dry-run --cert-name # Actual renewal certbot renew --cert-name ``` #### 4. If the hostname is missing from the certificate's SANs The certificate must be reissued with the additional hostname: ```bash # Add git.dev.cleveragents.com to an existing certificate certbot certonly \ --expand \ --cert-name \ -d git.cleverthis.com \ -d git.cleveragents.com \ -d git.dev.cleveragents.com ``` > **Note:** Replace `` with the actual certificate name shown by > `certbot certificates`. The `-d` flags must list ALL hostnames the certificate > should cover, including existing ones. #### 5. Reload the web server After renewal, reload the web server to pick up the new certificate: ```bash # nginx systemctl reload nginx # caddy systemctl reload caddy # traefik (usually auto-reloads; check logs) journalctl -u traefik -n 50 ``` #### 6. Verify the fix ```bash python scripts/check-tls-cert.py git.dev.cleveragents.com git.cleveragents.com # Or with openssl openssl s_client \ -connect git.dev.cleveragents.com:443 \ -servername git.dev.cleveragents.com \ 2>/dev/null | openssl x509 -noout -text | grep -A 10 "Subject Alternative Name" ``` #### 7. Verify git clone works ```bash git clone https://git.dev.cleveragents.com/cleveragents/cleveragents-core.git /tmp/test-clone rm -rf /tmp/test-clone ``` --- ### Manual Certificate (non-Let's Encrypt) If the infrastructure uses manually managed certificates: 1. Generate a new CSR including all required SANs: ```bash # Create OpenSSL config with SANs cat > /tmp/san.cnf << EOF [req] distinguished_name = req_distinguished_name req_extensions = v3_req prompt = no [req_distinguished_name] CN = git.cleverthis.com [v3_req] subjectAltName = @alt_names [alt_names] DNS.1 = git.cleverthis.com DNS.2 = git.cleveragents.com DNS.3 = git.dev.cleveragents.com EOF # Generate private key and CSR openssl req -new -newkey rsa:4096 -nodes \ -keyout /etc/ssl/private/git-cleverthis.key \ -out /tmp/git-cleverthis.csr \ -config /tmp/san.cnf ``` 2. Submit the CSR to your CA and obtain the signed certificate. 3. Install the certificate and reload the web server. --- ## Certificate Expiry Monitoring ### Automated Monitoring with the TLS Check Script Run the TLS check script regularly to detect expiring certificates before they cause outages: ```bash # Check all infrastructure hostnames with 30-day warning threshold python scripts/check-tls-cert.py \ git.cleverthis.com \ git.cleveragents.com \ git.dev.cleveragents.com \ --warn-days 30 ``` ### Recommended Alert Thresholds | Threshold | Action | |---|---| | 30 days | Warning — schedule renewal | | 14 days | Urgent — renew immediately | | 7 days | Critical — escalate to on-call | | 0 days | Outage — emergency renewal | ### Cron Job Setup Add a daily cron job to check certificate health: ```bash # /etc/cron.d/tls-cert-check 0 8 * * * root cd /path/to/cleveragents-core && \ python scripts/check-tls-cert.py \ git.cleverthis.com \ git.cleveragents.com \ git.dev.cleveragents.com \ --warn-days 30 \ | mail -s "TLS Cert Check" ops@cleverthis.com ``` ### Let's Encrypt Auto-Renewal Let's Encrypt certificates are valid for 90 days. Certbot installs a systemd timer or cron job that attempts renewal when the certificate has fewer than 30 days remaining: ```bash # Check certbot timer status systemctl status certbot.timer # Manually trigger renewal check certbot renew --dry-run ``` --- ## Related Issues - **#1532** — BUG-HUNT: TLS Configuration Error on `git.cleveragents.com` - **#1541** — TEST-INFRA: Unable to push to repository - **#1543** — fix(infra): resolve TLS handshake failure on `git.dev.cleveragents.com` --- ## Escalation Path 1. **First responder:** Check certificate with `scripts/check-tls-cert.py` 2. **If renewal needed:** Follow [Certificate Renewal Procedure](#certificate-renewal-procedure) 3. **If SNI misconfiguration:** Contact server admin with web server config access 4. **If CA/PKI issue:** Escalate to infrastructure team lead --- *Last updated: 2026-04-02 — Added as part of issue #1543 resolution.*