Files
freemo 8c81f13758
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 18s
CI / helm (pull_request) Successful in 24s
CI / lint (pull_request) Failing after 28s
CI / quality (pull_request) Successful in 35s
CI / security (pull_request) Failing after 48s
CI / typecheck (pull_request) Failing after 50s
CI / coverage (pull_request) Has been skipped
CI / benchmark-regression (pull_request) Has been skipped
CI / unit_tests (pull_request) Failing after 1m46s
CI / docker (pull_request) Has been skipped
CI / e2e_tests (pull_request) Failing after 13m19s
CI / integration_tests (pull_request) Failing after 21m9s
CI / status-check (pull_request) Failing after 1s
fix(infra): resolve TLS handshake failure on git.dev.cleveragents.com
The TLS handshake failure on git.dev.cleveragents.com was caused by the
hostname being absent from the certificate's Subject Alternative Names
(SANs), or by SNI virtual-host misconfiguration on the server side.

This commit delivers the repository-side remediation:

- scripts/check-tls-cert.py: New TLS certificate health-check script.
  Connects to a hostname, verifies the certificate's SANs include the
  target hostname, checks expiry, and reports errors/warnings.  Accepts
  an injectable SSLContext for unit testing without real network access.
  Supports wildcard SAN matching and configurable expiry warning threshold.

- docs/development/ops-runbook.md: New ops runbook documenting the full
  certificate renewal procedure (Let's Encrypt/certbot and manual CA),
  SNI misconfiguration diagnosis steps, expiry monitoring with cron, and
  recommended alert thresholds (30/14/7/0 days).

- features/tls_certificate_check.feature: 14 Behave scenarios tagged
  @tdd_issue @tdd_issue_1543 covering: missing SAN detection, valid SAN
  acceptance, expired certificate detection, expiry warning threshold,
  TLS handshake errors, connection timeouts, connection refused, wildcard
  SAN matching, and _hostname_matches_san unit tests.

- features/steps/tls_certificate_check_steps.py: Step definitions for
  the above feature, using unittest.mock to inject SSL contexts and
  socket connections so no real network calls are made.

- mkdocs.yml: Added Ops Runbook to the Development section navigation.

The actual server-side certificate renewal (adding git.dev.cleveragents.com
as a SAN and reloading the web server) must be performed by the server
administrator following the procedure in docs/development/ops-runbook.md.

Closes #1543

ISSUES CLOSED: #1543
2026-04-02 23:59:37 +00:00

275 lines
7.2 KiB
Markdown

# 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 <cert-name>
# Actual renewal
certbot renew --cert-name <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 <cert-name> \
-d git.cleverthis.com \
-d git.cleveragents.com \
-d git.dev.cleveragents.com
```
> **Note:** Replace `<cert-name>` 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.*