mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 10:52:22 +00:00
oci: fix low-port and generated secret profiles
This commit is contained in:
@@ -65,8 +65,7 @@
|
|||||||
"example": "${GENERATED_CLICKHOUSE_PASSWORD}",
|
"example": "${GENERATED_CLICKHOUSE_PASSWORD}",
|
||||||
"required": true,
|
"required": true,
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"source": "upstream-documentation",
|
"source": "upstream-documentation"
|
||||||
"prompt_user": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT",
|
"name": "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT",
|
||||||
|
|||||||
@@ -65,8 +65,7 @@
|
|||||||
"example": "${GENERATED_MYSQL_ROOT_PASSWORD}",
|
"example": "${GENERATED_MYSQL_ROOT_PASSWORD}",
|
||||||
"required": true,
|
"required": true,
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"source": "upstream-documentation",
|
"source": "upstream-documentation"
|
||||||
"prompt_user": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"documentation": "https://doc.powerdns.com/recursor/",
|
"documentation": "https://doc.powerdns.com/recursor/",
|
||||||
"repository": "https://github.com/PowerDNS/pdns",
|
"repository": "https://github.com/PowerDNS/pdns",
|
||||||
"tips": [
|
"tips": [
|
||||||
"The image uses the upstream default recursor configuration. Keep it on a trusted LAN and review the allowed client networks before changing its configuration."
|
"The image runs as the non-root pdns user. ProxMenux sets net.ipv4.ip_unprivileged_port_start=53 in this LXC's network namespace so it can listen on DNS port 53. This lets unprivileged processes in this LXC bind ports 53–1023; it does not change the Proxmox host or make the LXC privileged. Keep it on a trusted LAN and review the allowed client networks before changing its configuration."
|
||||||
],
|
],
|
||||||
"mini_changelog": [],
|
"mini_changelog": [],
|
||||||
"display_version": null,
|
"display_version": null,
|
||||||
@@ -188,6 +188,14 @@
|
|||||||
"stop_signal": "import-from-oci-image"
|
"stop_signal": "import-from-oci-image"
|
||||||
},
|
},
|
||||||
"adaptations": [
|
"adaptations": [
|
||||||
|
{
|
||||||
|
"id": "dns-low-ports-in-container-namespace",
|
||||||
|
"upstream_behavior": "The official image runs as the non-root pdns user and listens on DNS port 53; Docker Engine can provide a per-container NET_BIND_SERVICE capability.",
|
||||||
|
"native_lxc_behavior": "ProxMenux sets net.ipv4.ip_unprivileged_port_start=53 through this LXC's own network namespace configuration while keeping the LXC unprivileged.",
|
||||||
|
"reason": "The native OCI-LXC runner does not provide Docker's per-process capability grant. This namespaced sysctl enables the default DNS port without adding a capability or changing a host sysctl.",
|
||||||
|
"behavioral_impact": "Unprivileged processes in this LXC may bind ports 53 through 1023; the Proxmox host and other guests are unchanged.",
|
||||||
|
"validation": "pending-live-retest"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "imported-compose-source",
|
"id": "imported-compose-source",
|
||||||
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
||||||
@@ -317,7 +325,16 @@
|
|||||||
"validation": "not-requested-by-compose"
|
"validation": "not-requested-by-compose"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"installer_profile": {},
|
"installer_profile": {
|
||||||
|
"security": {
|
||||||
|
"sysctls": [
|
||||||
|
{
|
||||||
|
"name": "net.ipv4.ip_unprivileged_port_start",
|
||||||
|
"value": "53"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"replaces_discovered_ids": []
|
"replaces_discovered_ids": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,7 @@
|
|||||||
"example": "${GENERATED_TYPESENSE_API_KEY}",
|
"example": "${GENERATED_TYPESENSE_API_KEY}",
|
||||||
"required": true,
|
"required": true,
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"source": "upstream-documentation",
|
"source": "upstream-documentation"
|
||||||
"prompt_user": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
|
|||||||
@@ -65,8 +65,7 @@
|
|||||||
"example": "${GENERATED_CLICKHOUSE_PASSWORD}",
|
"example": "${GENERATED_CLICKHOUSE_PASSWORD}",
|
||||||
"required": true,
|
"required": true,
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"source": "upstream-documentation",
|
"source": "upstream-documentation"
|
||||||
"prompt_user": false
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT",
|
"name": "CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT",
|
||||||
|
|||||||
@@ -65,8 +65,7 @@
|
|||||||
"example": "${GENERATED_MYSQL_ROOT_PASSWORD}",
|
"example": "${GENERATED_MYSQL_ROOT_PASSWORD}",
|
||||||
"required": true,
|
"required": true,
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"source": "upstream-documentation",
|
"source": "upstream-documentation"
|
||||||
"prompt_user": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
"documentation": "https://doc.powerdns.com/recursor/",
|
"documentation": "https://doc.powerdns.com/recursor/",
|
||||||
"repository": "https://github.com/PowerDNS/pdns",
|
"repository": "https://github.com/PowerDNS/pdns",
|
||||||
"tips": [
|
"tips": [
|
||||||
"The image uses the upstream default recursor configuration. Keep it on a trusted LAN and review the allowed client networks before changing its configuration."
|
"The image runs as the non-root pdns user. ProxMenux sets net.ipv4.ip_unprivileged_port_start=53 in this LXC's network namespace so it can listen on DNS port 53. This lets unprivileged processes in this LXC bind ports 53–1023; it does not change the Proxmox host or make the LXC privileged. Keep it on a trusted LAN and review the allowed client networks before changing its configuration."
|
||||||
],
|
],
|
||||||
"mini_changelog": [],
|
"mini_changelog": [],
|
||||||
"display_version": null,
|
"display_version": null,
|
||||||
@@ -188,6 +188,14 @@
|
|||||||
"stop_signal": "import-from-oci-image"
|
"stop_signal": "import-from-oci-image"
|
||||||
},
|
},
|
||||||
"adaptations": [
|
"adaptations": [
|
||||||
|
{
|
||||||
|
"id": "dns-low-ports-in-container-namespace",
|
||||||
|
"upstream_behavior": "The official image runs as the non-root pdns user and listens on DNS port 53; Docker Engine can provide a per-container NET_BIND_SERVICE capability.",
|
||||||
|
"native_lxc_behavior": "ProxMenux sets net.ipv4.ip_unprivileged_port_start=53 through this LXC's own network namespace configuration while keeping the LXC unprivileged.",
|
||||||
|
"reason": "The native OCI-LXC runner does not provide Docker's per-process capability grant. This namespaced sysctl enables the default DNS port without adding a capability or changing a host sysctl.",
|
||||||
|
"behavioral_impact": "Unprivileged processes in this LXC may bind ports 53 through 1023; the Proxmox host and other guests are unchanged.",
|
||||||
|
"validation": "pending-live-retest"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "imported-compose-source",
|
"id": "imported-compose-source",
|
||||||
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
"upstream_behavior": "The source definition deploys the complete Docker Compose application model.",
|
||||||
@@ -317,7 +325,16 @@
|
|||||||
"validation": "not-requested-by-compose"
|
"validation": "not-requested-by-compose"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"installer_profile": {},
|
"installer_profile": {
|
||||||
|
"security": {
|
||||||
|
"sysctls": [
|
||||||
|
{
|
||||||
|
"name": "net.ipv4.ip_unprivileged_port_start",
|
||||||
|
"value": "53"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"catalog": {
|
"catalog": {
|
||||||
"replaces_discovered_ids": []
|
"replaces_discovered_ids": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,8 +65,7 @@
|
|||||||
"example": "${GENERATED_TYPESENSE_API_KEY}",
|
"example": "${GENERATED_TYPESENSE_API_KEY}",
|
||||||
"required": true,
|
"required": true,
|
||||||
"sensitive": true,
|
"sensitive": true,
|
||||||
"source": "upstream-documentation",
|
"source": "upstream-documentation"
|
||||||
"prompt_user": false
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"volumes": [
|
"volumes": [
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import json
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "remote"))
|
||||||
|
|
||||||
|
import oci_runtime_settings
|
||||||
|
|
||||||
|
|
||||||
|
class PowerDNSRecursorProfileTest(unittest.TestCase):
|
||||||
|
def test_dns_port_is_available_to_the_non_root_image_only_inside_its_unprivileged_lxc(self):
|
||||||
|
curated = json.loads((ROOT / "catalog/curated/pdns-recursor.json").read_text())
|
||||||
|
app = json.loads((ROOT / "catalog/apps/pdns-recursor.json").read_text())
|
||||||
|
|
||||||
|
expected_sysctls = [{
|
||||||
|
"name": "net.ipv4.ip_unprivileged_port_start",
|
||||||
|
"value": "53",
|
||||||
|
}]
|
||||||
|
self.assertEqual(
|
||||||
|
curated["proxmox"]["installer_profile"]["security"]["sysctls"],
|
||||||
|
expected_sysctls,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
app["proxmox"]["installer_profile"]["security"]["sysctls"],
|
||||||
|
expected_sysctls,
|
||||||
|
)
|
||||||
|
self.assertTrue(curated["proxmox"]["defaults"]["unprivileged"])
|
||||||
|
self.assertNotIn(
|
||||||
|
"required_capabilities",
|
||||||
|
curated["proxmox"]["installer_profile"].get("security", {}),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
oci_runtime_settings.sysctl_content({"security": {"sysctls": expected_sysctls}}),
|
||||||
|
"lxc.sysctl.net.ipv4.ip_unprivileged_port_start = 53\n",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
oci_runtime_settings.include_path(111),
|
||||||
|
Path("/etc/pve/proxmenux/111.sysctls"),
|
||||||
|
)
|
||||||
|
installer = (ROOT / "remote/install_oci.sh").read_text()
|
||||||
|
self.assertIn('SYSCTL_INCLUDE="/etc/pve/proxmenux/${VMID}.sysctls"', installer)
|
||||||
|
self.assertIn('set_lxc_directive "lxc.include" "$SYSCTL_INCLUDE"', installer)
|
||||||
|
self.assertEqual(curated["compatibility"]["validated"], False)
|
||||||
|
self.assertEqual(curated["validation"]["clean_install"], "pending")
|
||||||
|
self.assertEqual(curated, app)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT / "src"))
|
||||||
|
|
||||||
|
from proxmenux_oci.catalog import Catalog
|
||||||
|
from proxmenux_oci.installer import DEFAULT_MODE, build_deployment
|
||||||
|
|
||||||
|
|
||||||
|
class RecordingUI:
|
||||||
|
def __init__(self):
|
||||||
|
self.asked = []
|
||||||
|
|
||||||
|
def ask(self, text, default=None, required=True):
|
||||||
|
self.asked.append(text)
|
||||||
|
return default if default not in (None, "") else "fixture-value"
|
||||||
|
|
||||||
|
def password(self, text, required=True):
|
||||||
|
self.asked.append(text)
|
||||||
|
return "fixture-password"
|
||||||
|
|
||||||
|
def choose(self, text, options, default=None):
|
||||||
|
self.asked.append(text)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def confirm(self, text, default=False):
|
||||||
|
self.asked.append(text)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def checklist(self, text, options, default=None):
|
||||||
|
self.asked.append(text)
|
||||||
|
return default
|
||||||
|
|
||||||
|
def info(self, text):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def message(self, text):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _addresses(ui, bridge, names, *args):
|
||||||
|
return {name: "dhcp" for name in names}, None
|
||||||
|
|
||||||
|
|
||||||
|
class RequiredGeneratedEnvironmentTests(unittest.TestCase):
|
||||||
|
@patch("proxmenux_oci.i18n.language", return_value="en")
|
||||||
|
@patch("proxmenux_oci.installer.host.storages", return_value=[
|
||||||
|
{"storage": "local-lvm", "type": "lvmthin", "avail": 100 * 2**30}
|
||||||
|
])
|
||||||
|
@patch("proxmenux_oci.installer.host.bridges", return_value=[
|
||||||
|
{"iface": "vmbr0", "cidr": "192.0.2.10/24"}
|
||||||
|
])
|
||||||
|
@patch("proxmenux_oci.installer.host.timezone", return_value="Europe/Madrid")
|
||||||
|
@patch("proxmenux_oci.installer.host.gpus", return_value={"intel": [], "amd": [], "nvidia": False})
|
||||||
|
@patch("proxmenux_oci.installer.access.ask_ipv4", return_value=("dhcp", None))
|
||||||
|
@patch("proxmenux_oci.installer.access.ask_addresses", side_effect=_addresses)
|
||||||
|
@patch("proxmenux_oci.installer.secrets.token_hex", return_value="a1" * 24)
|
||||||
|
def test_database_secrets_are_generated_without_a_prompt_or_static_value(self, token_hex, *_):
|
||||||
|
requirements = {
|
||||||
|
"mysql": "MYSQL_ROOT_PASSWORD",
|
||||||
|
"clickhouse": "CLICKHOUSE_PASSWORD",
|
||||||
|
"typesense": "TYPESENSE_API_KEY",
|
||||||
|
}
|
||||||
|
catalog = Catalog(ROOT)
|
||||||
|
|
||||||
|
for app, name in requirements.items():
|
||||||
|
with self.subTest(app=app):
|
||||||
|
ui = RecordingUI()
|
||||||
|
template = catalog.compose(app)
|
||||||
|
plan = build_deployment(template, ui, DEFAULT_MODE)
|
||||||
|
variable = next(item for item in plan["environment"] if item["name"] == name)
|
||||||
|
|
||||||
|
self.assertTrue(variable["sensitive"])
|
||||||
|
self.assertRegex(variable["value"], re.compile(r"^[0-9a-f]{48}$"))
|
||||||
|
self.assertNotIn(name, " ".join(ui.asked))
|
||||||
|
contract = next(item for item in template["container_contract"]["environment"]
|
||||||
|
if item["name"] == name)
|
||||||
|
self.assertNotIn("prompt_user", contract)
|
||||||
|
token_hex.assert_called_with(24)
|
||||||
|
token_hex.reset_mock()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user