feat: add generate_tailscale() for Tailscale cert generation
Add generate_tailscale(cert_path, key_path, hostname) to muxplex/tls.py. The function calls 'tailscale cert' to obtain real Let's Encrypt certificates via the Tailscale CLI. - Creates parent directories with mkdir(parents=True, exist_ok=True) - Runs subprocess with --cert-file and --key-file flags (timeout=30) - Returns None on non-zero exit, TimeoutExpired, OSError, or missing files - On success: sets key_path chmod(0o600), reads cert info, returns metadata dict with method='tailscale', cert_path, key_path, hostnames, expires Tests added: - test_generate_tailscale_calls_tailscale_cert: verifies --cert-file/--key-file flags - test_generate_tailscale_returns_none_on_failure: verifies None on rc=1
This commit is contained in:
@@ -166,6 +166,65 @@ def detect_tailscale() -> dict | None:
|
||||
}
|
||||
|
||||
|
||||
def generate_tailscale(cert_path, key_path, hostname: str) -> dict | None:
|
||||
"""Obtain a Let's Encrypt certificate via Tailscale.
|
||||
|
||||
Args:
|
||||
cert_path: Destination path for the certificate PEM file.
|
||||
key_path: Destination path for the private key PEM file.
|
||||
hostname: Tailscale hostname to request a certificate for.
|
||||
|
||||
Returns:
|
||||
dict with keys: method, cert_path, key_path, hostnames, expires.
|
||||
Returns None on failure (non-zero exit, timeout, OS error, or missing files).
|
||||
"""
|
||||
import subprocess
|
||||
|
||||
cert_path = Path(cert_path)
|
||||
key_path = Path(key_path)
|
||||
|
||||
cert_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
key_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"tailscale",
|
||||
"cert",
|
||||
"--cert-file",
|
||||
str(cert_path),
|
||||
"--key-file",
|
||||
str(key_path),
|
||||
hostname,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
except (subprocess.TimeoutExpired, OSError):
|
||||
return None
|
||||
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
|
||||
if not cert_path.exists() or not key_path.exists():
|
||||
return None
|
||||
|
||||
key_path.chmod(0o600)
|
||||
|
||||
info = get_cert_info(cert_path)
|
||||
if info is None:
|
||||
return None
|
||||
|
||||
return {
|
||||
"method": "tailscale",
|
||||
"cert_path": str(cert_path),
|
||||
"key_path": str(key_path),
|
||||
"hostnames": info["hostnames"],
|
||||
"expires": info["expires"],
|
||||
}
|
||||
|
||||
|
||||
def get_cert_info(cert_path) -> dict | None:
|
||||
"""Inspect a PEM certificate and return metadata.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user