feat: Add certificate coverage check to haproxy_add_domain
When adding a domain, now checks if an SSL certificate covers it: - Exact match: domain.com.pem - Wildcard match: parent.com.pem with *.parent.com SAN Output examples: - "SSL: Using certificate inouter.com (wildcard)" - "SSL: No certificate found. Use haproxy_issue_cert(...) to issue one." Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
"""Domain management tools for HAProxy MCP Server."""
|
"""Domain management tools for HAProxy MCP Server."""
|
||||||
|
|
||||||
import fcntl
|
import fcntl
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
|
|
||||||
from pydantic import Field
|
from pydantic import Field
|
||||||
@@ -12,6 +14,7 @@ from ..config import (
|
|||||||
MAX_SLOTS,
|
MAX_SLOTS,
|
||||||
StateField,
|
StateField,
|
||||||
STATE_MIN_COLUMNS,
|
STATE_MIN_COLUMNS,
|
||||||
|
SUBPROCESS_TIMEOUT,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
from ..exceptions import HaproxyError
|
from ..exceptions import HaproxyError
|
||||||
@@ -27,6 +30,51 @@ from ..file_ops import (
|
|||||||
remove_domain_from_config,
|
remove_domain_from_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Certificate paths
|
||||||
|
CERTS_DIR = "/opt/haproxy/certs"
|
||||||
|
|
||||||
|
|
||||||
|
def check_certificate_coverage(domain: str) -> tuple[bool, str]:
|
||||||
|
"""Check if a domain is covered by an existing certificate.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
domain: Domain name to check (e.g., api.example.com)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (is_covered, certificate_name or message)
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(CERTS_DIR):
|
||||||
|
return False, "Certificate directory not found"
|
||||||
|
|
||||||
|
# Check for exact match first
|
||||||
|
exact_pem = os.path.join(CERTS_DIR, f"{domain}.pem")
|
||||||
|
if os.path.exists(exact_pem):
|
||||||
|
return True, domain
|
||||||
|
|
||||||
|
# Check for wildcard coverage (e.g., api.example.com covered by *.example.com)
|
||||||
|
parts = domain.split(".")
|
||||||
|
if len(parts) >= 2:
|
||||||
|
# Try parent domain (example.com for api.example.com)
|
||||||
|
parent_domain = ".".join(parts[1:])
|
||||||
|
parent_pem = os.path.join(CERTS_DIR, f"{parent_domain}.pem")
|
||||||
|
|
||||||
|
if os.path.exists(parent_pem):
|
||||||
|
# Verify the certificate has wildcard SAN
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["openssl", "x509", "-in", parent_pem, "-noout", "-ext", "subjectAltName"],
|
||||||
|
capture_output=True, text=True, timeout=SUBPROCESS_TIMEOUT
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Check if wildcard covers this domain
|
||||||
|
wildcard = f"*.{parent_domain}"
|
||||||
|
if wildcard in result.stdout:
|
||||||
|
return True, f"{parent_domain} (wildcard)"
|
||||||
|
except (subprocess.TimeoutExpired, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False, "No matching certificate"
|
||||||
|
|
||||||
|
|
||||||
def register_domain_tools(mcp):
|
def register_domain_tools(mcp):
|
||||||
"""Register domain management tools with MCP server."""
|
"""Register domain management tools with MCP server."""
|
||||||
@@ -160,9 +208,18 @@ def register_domain_tools(mcp):
|
|||||||
remove_server_from_config(domain, 1)
|
remove_server_from_config(domain, 1)
|
||||||
return f"Domain {domain} added to {pool} but server config failed: {e}"
|
return f"Domain {domain} added to {pool} but server config failed: {e}"
|
||||||
|
|
||||||
return f"Domain {domain} added to {pool} with server {ip}:{http_port}"
|
result = f"Domain {domain} added to {pool} with server {ip}:{http_port}"
|
||||||
|
else:
|
||||||
|
result = f"Domain {domain} added to {pool} (no servers configured)"
|
||||||
|
|
||||||
return f"Domain {domain} added to {pool} (no servers configured)"
|
# Check certificate coverage
|
||||||
|
cert_covered, cert_info = check_certificate_coverage(domain)
|
||||||
|
if cert_covered:
|
||||||
|
result += f"\nSSL: Using certificate {cert_info}"
|
||||||
|
else:
|
||||||
|
result += f"\nSSL: No certificate found. Use haproxy_issue_cert(\"{domain}\") to issue one."
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
except HaproxyError as e:
|
except HaproxyError as e:
|
||||||
return f"Error: {e}"
|
return f"Error: {e}"
|
||||||
|
|||||||
Reference in New Issue
Block a user