From 5c0af117354c3b0c6db62791b6c79adeb41e9ac9 Mon Sep 17 00:00:00 2001 From: kaffa Date: Sun, 15 Feb 2026 14:40:11 +0900 Subject: [PATCH] Fix list_certs to use PEM files + openssl instead of acme.sh acme.sh --list failed in uv/systemd context. Now scans certs directory directly and uses openssl for cert details + HAProxy show ssl cert API for loaded status. Co-Authored-By: Claude Opus 4.6 --- haproxy_mcp/tools/certificates.py | 87 ++++++++++++++++++------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/haproxy_mcp/tools/certificates.py b/haproxy_mcp/tools/certificates.py index 6d6fefb..1d76e8a 100644 --- a/haproxy_mcp/tools/certificates.py +++ b/haproxy_mcp/tools/certificates.py @@ -110,53 +110,68 @@ def restore_certificates() -> int: def _haproxy_list_certs_impl() -> str: """Implementation of haproxy_list_certs.""" try: - result = run_command([ACME_SH, "--list"], timeout=SUBPROCESS_TIMEOUT) - if result.returncode != 0: - return f"Error: {result.stderr}" - - lines = result.stdout.strip().split("\n") - if len(lines) <= 1: - return "No certificates found" - + # Get loaded certs from HAProxy Runtime API try: haproxy_certs = haproxy_cmd("show ssl cert") except HaproxyError as e: logger.debug("Could not get HAProxy certs: %s", e) haproxy_certs = "" + # Scan PEM files in certs directory + pem_files = sorted( + f for f in os.listdir(CERTS_DIR) + if f.endswith(".pem") + ) if os.path.isdir(CERTS_DIR) else [] + + if not pem_files: + return "No certificates found" + certs = [] - for line in lines[1:]: - parts = line.split() - if len(parts) >= 4: - domain = parts[0] - ca = "unknown" - created = "unknown" - renew = "unknown" + for pem_file in pem_files: + domain = pem_file[:-4] # strip .pem + host_path = f"{CERTS_DIR}/{pem_file}" + _, container_path = get_pem_paths(domain) - for part in parts: - if "Google" in part or "LetsEncrypt" in part or "ZeroSSL" in part: - ca = part - elif part.endswith("Z") and "T" in part: - if created == "unknown": - created = part - else: - renew = part + # Get cert details via openssl + issuer = "unknown" + not_after = "unknown" + sans = "" + try: + result = run_command( + ["openssl", "x509", "-in", host_path, "-noout", + "-issuer", "-enddate", "-ext", "subjectAltName"], + timeout=SUBPROCESS_TIMEOUT, + ) + if result.returncode == 0: + for line in result.stdout.split("\n"): + line = line.strip() + if line.startswith("issuer="): + # Extract CN or O from issuer + for field in line.split(","): + field = field.strip() + if field.startswith("CN =") or field.startswith("CN="): + issuer = field.split("=", 1)[1].strip() + elif field.startswith("O =") or field.startswith("O="): + if issuer == "unknown": + issuer = field.split("=", 1)[1].strip() + elif line.startswith("notAfter="): + not_after = line.split("=", 1)[1].strip() + elif "DNS:" in line: + sans = ", ".join( + s.strip().replace("DNS:", "") + for s in line.split(",") + if "DNS:" in s + ) + except (TimeoutError, OSError): + pass - host_path, container_path = get_pem_paths(domain) - if container_path in haproxy_certs: - status = "loaded" - elif _file_exists(host_path): - status = "file exists (not loaded)" - else: - status = "not deployed" - - certs.append(f"• {domain} ({ca})\n Created: {created}\n Renew: {renew}\n Status: {status}") + status = "loaded" if container_path in haproxy_certs else "file only" + san_info = f"\n SANs: {sans}" if sans else "" + certs.append( + f"• {domain} ({issuer})\n Expires: {not_after}{san_info}\n Status: {status}" + ) return "\n\n".join(certs) if certs else "No certificates found" - except (TimeoutError, subprocess.TimeoutExpired): - return "Error: Command timed out" - except FileNotFoundError: - return "Error: acme.sh not found" except OSError as e: logger.error("Error listing certificates: %s", e) return f"Error: {e}"