feat: auto-generate self-signed certs, hoover modal processing guard, refactor backends/proxy pages
This commit is contained in:
@@ -981,10 +981,10 @@ def generate_self_signed(_request: Any, body: dict[str, Any] | None) -> dict[str
|
||||
raise ValueError("'domain' is required")
|
||||
days = body.get("days", 365)
|
||||
|
||||
cert_dir = _ACME_HOME / domain
|
||||
cert_dir.mkdir(parents=True, exist_ok=True)
|
||||
cert_file = cert_dir / "fullchain.cer"
|
||||
key_file = cert_dir / f"{domain}.key"
|
||||
certs_dir = PROJECT_DIR / "data" / "certs"
|
||||
certs_dir.mkdir(parents=True, exist_ok=True)
|
||||
cert_file = certs_dir / f"{domain}.crt"
|
||||
key_file = certs_dir / f"{domain}.key"
|
||||
|
||||
if cert_file.is_file() and key_file.is_file():
|
||||
logger.info("Self-signed cert for %s already exists, skipping", domain)
|
||||
|
||||
@@ -691,6 +691,10 @@ def set_masquerade(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]
|
||||
enable = body.get("enable")
|
||||
if not zone or enable is None:
|
||||
raise ValueError("'zone' and 'enable' (bool) are required")
|
||||
if zone == "public" and enable:
|
||||
raise ValueError(
|
||||
"Masquerade (NAT) is not supported on the public zone — enable it on 'internal' or 'vpn' instead"
|
||||
)
|
||||
action = "--add-masquerade" if enable else "--remove-masquerade"
|
||||
run(["firewall-cmd", f"--zone={zone}", action, "--permanent"], sudo=True)
|
||||
_reload()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -354,11 +355,54 @@ def _reload_nginx() -> None:
|
||||
logger.info("nginx configuration applied and reloaded")
|
||||
|
||||
|
||||
def _ensure_self_signed_cert(domain: str) -> None:
|
||||
"""Auto-generate a self-signed cert for *domain* if not yet present."""
|
||||
certs_dir = PROJECT_DIR / "data" / "certs"
|
||||
certs_dir.mkdir(parents=True, exist_ok=True)
|
||||
cert_file = certs_dir / f"{domain}.crt"
|
||||
key_file = certs_dir / f"{domain}.key"
|
||||
if cert_file.is_file() and key_file.is_file():
|
||||
return
|
||||
logger.info("Auto-generating self-signed cert for %s", domain)
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl",
|
||||
"req",
|
||||
"-x509",
|
||||
"-newkey",
|
||||
"rsa:2048",
|
||||
"-keyout",
|
||||
str(key_file),
|
||||
"-out",
|
||||
str(cert_file),
|
||||
"-days",
|
||||
"365",
|
||||
"-nodes",
|
||||
"-subj",
|
||||
f"/CN={domain}",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
cert_file.chmod(0o644)
|
||||
key_file.chmod(0o600)
|
||||
|
||||
|
||||
def _write_all_sites() -> None:
|
||||
"""Regenerate all site configs and ACME challenge site."""
|
||||
ensure_dirs(SITES_DIR)
|
||||
cfg = _get_config()
|
||||
backends = cfg.get("backends", {})
|
||||
# Auto-generate self-signed certs for management domains that need them
|
||||
for name, dom in cfg.get("domains", {}).items():
|
||||
cert = dom.get("cert")
|
||||
paths = _ngx_resolve_paths(dom, backends)
|
||||
has_management = any(p.get("is_management") for p in paths.values())
|
||||
if has_management and (cert == "selfsigned" or cert is None):
|
||||
_ensure_self_signed_cert(name)
|
||||
|
||||
existing = set(SITES_DIR.iterdir()) if SITES_DIR.exists() else set()
|
||||
written: set[str] = set()
|
||||
for name, dom in cfg.get("domains", {}).items():
|
||||
@@ -396,13 +440,19 @@ def _hash_password(password: str) -> str:
|
||||
return sha256_crypt.hash(password)
|
||||
|
||||
|
||||
def _write_htpasswd(user: str, password: str) -> None:
|
||||
"""Add or update a user entry in the .htpasswd file using SHA-256 hashing."""
|
||||
ensure_dirs(DATA_DIR)
|
||||
def _write_htpasswd(
|
||||
user: str, password: str, htpasswd_path: Path | None = None
|
||||
) -> None:
|
||||
"""Add or update a user entry in the .htpasswd file using SHA-256 hashing.
|
||||
|
||||
If *htpasswd_path* is not given, defaults to :data:`HTPASSWD_FILE`.
|
||||
"""
|
||||
target = htpasswd_path or HTPASSWD_FILE
|
||||
ensure_dirs(target.parent)
|
||||
hashed = _hash_password(password)
|
||||
existing: dict[str, str] = {}
|
||||
if HTPASSWD_FILE.exists():
|
||||
with open(HTPASSWD_FILE) as f:
|
||||
if target.exists():
|
||||
with open(target) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
@@ -411,12 +461,12 @@ def _write_htpasswd(user: str, password: str) -> None:
|
||||
if len(parts) == 2:
|
||||
existing[parts[0]] = line
|
||||
existing[user] = f"{user}:{hashed}"
|
||||
tmp = HTPASSWD_FILE.with_suffix(".tmp")
|
||||
tmp = target.with_suffix(".tmp")
|
||||
with open(tmp, "w") as f:
|
||||
for _uname, entry in existing.items():
|
||||
f.write(entry + "\n")
|
||||
os.chmod(tmp, 0o640)
|
||||
os.replace(tmp, HTPASSWD_FILE)
|
||||
os.replace(tmp, target)
|
||||
|
||||
|
||||
def _get_nginx_state() -> dict[str, Any]:
|
||||
@@ -502,7 +552,18 @@ def add_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
|
||||
auth = body.get("auth")
|
||||
if auth is not None:
|
||||
entry["auth"] = auth
|
||||
# Write htpasswd file when a password is provided, then store
|
||||
# only {user, htpasswd path} — never persist the raw password.
|
||||
if auth.get("user") and auth.get("pass"):
|
||||
htpasswd_path = auth.get(
|
||||
"htpasswd", str(PROJECT_DIR / "data" / "nginx" / ".htpasswd")
|
||||
)
|
||||
if isinstance(htpasswd_path, str) and not Path(htpasswd_path).is_absolute():
|
||||
htpasswd_path = PROJECT_DIR / htpasswd_path
|
||||
_write_htpasswd(auth["user"], auth["pass"], Path(htpasswd_path))
|
||||
entry["auth"] = {"user": auth["user"], "htpasswd": str(htpasswd_path)}
|
||||
else:
|
||||
entry["auth"] = auth
|
||||
|
||||
cfg["domains"][domain] = entry
|
||||
_save_config(cfg)
|
||||
@@ -561,7 +622,26 @@ def update_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
|
||||
if body["auth"] is None:
|
||||
entry.pop("auth", None)
|
||||
else:
|
||||
entry["auth"] = body["auth"]
|
||||
# Normalize: if auth has `pass`, write htpasswd and store only
|
||||
# `{user, htpasswd path}` — never persist the raw password.
|
||||
if body["auth"].get("user") and body["auth"].get("pass"):
|
||||
htpasswd_path = body["auth"].get(
|
||||
"htpasswd", str(PROJECT_DIR / "data" / "nginx" / ".htpasswd")
|
||||
)
|
||||
if (
|
||||
isinstance(htpasswd_path, str)
|
||||
and not Path(htpasswd_path).is_absolute()
|
||||
):
|
||||
htpasswd_path = PROJECT_DIR / htpasswd_path
|
||||
_write_htpasswd(
|
||||
body["auth"]["user"], body["auth"]["pass"], Path(htpasswd_path)
|
||||
)
|
||||
entry["auth"] = {
|
||||
"user": body["auth"]["user"],
|
||||
"htpasswd": str(htpasswd_path),
|
||||
}
|
||||
else:
|
||||
entry["auth"] = body["auth"]
|
||||
|
||||
_save_config(cfg)
|
||||
refresh_state(["nginx"])
|
||||
|
||||
Reference in New Issue
Block a user