mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-10-07 05:56:40 +00:00
fix(oci): confirm scoped host-monitor firewall access
This commit is contained in:
@@ -291,6 +291,14 @@ def _deployment_summary_text(template: dict[str, Any], deployment: dict[str, Any
|
||||
network = plan.get("network", {})
|
||||
if plan.get("host_monitor"):
|
||||
row(translate("Network"), translate("IP address and firewall of the host"))
|
||||
firewall = plan.get("host_firewall")
|
||||
if firewall:
|
||||
row(translate("Host firewall"),
|
||||
translate("allow TCP {port} from {subnet} via {bridge}").format(
|
||||
port=firewall["port"], subnet=firewall["source"], bridge=firewall["bridge"]
|
||||
))
|
||||
else:
|
||||
row(translate("Host firewall"), translate("not changed"))
|
||||
elif "frontend_bridge" in network:
|
||||
addresses = [network[key] for key in ("frontend_ipv4", "machine_learning_frontend_ipv4") if network.get(key)]
|
||||
addresses += [service["frontend_ipv4"] for service in plan.get("services", []) if service.get("frontend_ipv4")]
|
||||
|
||||
@@ -61,6 +61,23 @@ def default_bridge(preferred: str) -> str:
|
||||
return names[0]
|
||||
|
||||
|
||||
def ipv4_subnet(bridge: str) -> str | None:
|
||||
"""The IPv4 subnet configured on ``bridge``, if it has one.
|
||||
|
||||
This is deliberately taken from the node's bridge configuration instead
|
||||
of guessing from a container address. A host-monitor shares the host
|
||||
network namespace, so its firewall source scope must be the selected
|
||||
host bridge's network.
|
||||
"""
|
||||
row = next((item for item in bridges(include_private=True)
|
||||
if item.get("iface") == bridge), None)
|
||||
try:
|
||||
interface = ipaddress.ip_interface(str((row or {}).get("cidr") or ""))
|
||||
except ValueError:
|
||||
return None
|
||||
return str(interface.network) if interface.version == 4 else None
|
||||
|
||||
|
||||
def timezone() -> str:
|
||||
try:
|
||||
value = Path("/etc/timezone").read_text(encoding="utf-8").strip()
|
||||
|
||||
@@ -111,6 +111,45 @@ def ask_bridge(ui, text: str, default: str, mode: str = ADVANCED_MODE) -> str:
|
||||
return selected
|
||||
|
||||
|
||||
def host_monitor_firewall_plan(template: dict[str, Any], bridge: str) -> dict[str, Any] | None:
|
||||
"""Build the narrow firewall change a host-monitor profile explicitly declares.
|
||||
|
||||
Profiles without this opt-in declaration never propose a host firewall
|
||||
change. The source network comes from the selected Proxmox bridge, not
|
||||
from a catalog constant or the address of a particular test lab.
|
||||
"""
|
||||
profile = template.get("proxmox", {}).get("installer_profile", {})
|
||||
declared = profile.get("host_monitor_firewall")
|
||||
if declared is None:
|
||||
return None
|
||||
if not isinstance(declared, dict) or declared.get("protocol") != "tcp":
|
||||
raise InstallError(translate("The host-monitor firewall declaration is invalid"))
|
||||
port = declared.get("web_port")
|
||||
if not isinstance(port, int) or not 1 <= port <= 65535:
|
||||
raise InstallError(translate("The host-monitor firewall port is invalid"))
|
||||
subnet = host.ipv4_subnet(bridge)
|
||||
if not subnet:
|
||||
raise InstallError(translate("The selected bridge has no IPv4 subnet for the host-monitor firewall"))
|
||||
return {"bridge": bridge, "source": subnet, "protocol": "tcp", "port": port,
|
||||
"confirmed": True}
|
||||
|
||||
|
||||
def confirm_host_monitor_firewall(ui, template: dict[str, Any], bridge: str) -> dict[str, Any] | None:
|
||||
"""Ask separately before allowing a narrowly-scoped host firewall rule."""
|
||||
plan = host_monitor_firewall_plan(template, bridge)
|
||||
if plan is None:
|
||||
return None
|
||||
summary = translate("The host-monitor web interface uses the host network.")
|
||||
question = translate("Allow TCP port {port} from {subnet} through the host firewall? Existing firewall rules are not changed.").format(
|
||||
port=plan["port"], subnet=plan["source"]
|
||||
)
|
||||
if ui.confirm(f"{summary}\n\n{translate('Selected bridge:')} {plan['bridge']}\n"
|
||||
f"{translate('Allowed source subnet:')} {plan['source']}\n"
|
||||
f"{translate('Allowed web port:')} TCP {plan['port']}\n\n{question}", False):
|
||||
return plan
|
||||
return None
|
||||
|
||||
|
||||
def _confirm_warning(ui, warning: str | None, fallback: str, question: str) -> bool:
|
||||
return ui.confirm(f"{translate(warning) if warning else translate(fallback)}\n\n{translate(question)}", False)
|
||||
|
||||
@@ -258,6 +297,8 @@ def build_deployment(
|
||||
onboot = bool(defaults["onboot"])
|
||||
start_after = True
|
||||
|
||||
host_firewall = confirm_host_monitor_firewall(ui, template, bridge) if host_monitor else None
|
||||
|
||||
environment: list[dict[str, str]] = []
|
||||
for item in template["container_contract"]["environment"]:
|
||||
name = item["name"]
|
||||
@@ -423,6 +464,7 @@ def build_deployment(
|
||||
return {
|
||||
"host_monitor": host_monitor,
|
||||
"monitor_scope": monitor_scope,
|
||||
"host_firewall": host_firewall,
|
||||
"vmid": int(vmid_text) if vmid_text else None,
|
||||
"ostype": defaults.get("ostype", "auto-from-image"),
|
||||
"hostname": hostname,
|
||||
|
||||
Reference in New Issue
Block a user