From b59fbd1fa8d5c19b1b5259bd2eddb9009e4c90d3 Mon Sep 17 00:00:00 2001 From: Bryan Joshua Pedini Date: Wed, 5 Aug 2026 05:38:01 +0200 Subject: [PATCH] secured the plugin a bit for 0.1.1 --- README.md | 32 +++++++++++++++---- proxmox_power_button/__init__.py | 8 +++-- .../vm_power_buttons.html | 2 ++ proxmox_power_button/views.py | 15 +++++++-- pyproject.toml | 2 +- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c9a4a71..759c832 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ PLUGINS = ["proxmox_power_button"] PLUGINS_CONFIG = { "proxmox_power_button": { - "verify_ssl": False, + "verify_ssl": True, # set False only for self-signed lab certs "stop_mode": "shutdown", "reboot_mode": "reboot", }, @@ -82,15 +82,35 @@ for both QEMU and LXC. | key | default | meaning | |---------------|--------------|------------------------------------------| -| `verify_ssl` | `False` | verify Proxmox TLS cert | +| `verify_ssl` | `True` | verify Proxmox TLS cert (set `False` only for self-signed lab certs) | | `stop_mode` | `"shutdown"` | `shutdown` (graceful ACPI) or `stop` (hard) | | `reboot_mode` | `"reboot"` | `reboot` (graceful) or `reset` (hard) | -## Notes / caveats +## Security -- **Token is stored in a plain-text custom field** and is visible to anyone who - can view the cluster. Use a scoped, least-privilege Proxmox API token and - restrict cluster view permissions. NetBox has no "secret" custom-field type. +- **Token is stored in a plain-text custom field.** It is visible to anyone who + can *view* the cluster — in the UI, in REST API responses for the cluster, + and in change-log data. NetBox has no "secret" custom-field type, so: + - restrict Cluster **view** permissions to administrators only; + - use a least-privilege Proxmox API token: only `VM.Audit` + `VM.PowerMgmt`, + scoped to the specific VMs or pool this plugin should manage — never a + `root@pam` token with datacenter-wide rights. Anyone who can read the + field can extract the token and drive the hypervisor directly. +- **Permissions.** The buttons render only for users with + `virtualization.change_virtualmachine`, and the action endpoint enforces + NetBox's **object-scoped** permissions: a user whose change permission is + constrained (e.g. to one tenant's VMs) gets a 404 on any VM outside that + scope — the same behaviour as core NetBox object views. +- **TLS verification is on by default.** The Proxmox API token crosses this + connection on every power action; leaving `verify_ssl` at `True` prevents + token theft via MitM. Only set it to `False` for self-signed lab + certificates. +- **Error detail goes to the log, not the browser.** Failed actions show a + generic message; the full Proxmox/network error (which may contain internal + hostnames, URLs, or response bodies) is written to the + `proxmox_power_button` logger. + +## Notes / caveats - **Requires NetBox ≥ 4.5.0.** The data migration depends on `extras.0134_owner` and `virtualization.0052_gfk_indexes`, which first appear in 4.5.0; on 4.4 or older `migrate` fails with `NodeNotFoundError`. Bump diff --git a/proxmox_power_button/__init__.py b/proxmox_power_button/__init__.py index 6ca9e91..69b1841 100644 --- a/proxmox_power_button/__init__.py +++ b/proxmox_power_button/__init__.py @@ -7,7 +7,7 @@ class ProxmoxPowerButtonConfig(PluginConfig): name = "proxmox_power_button" verbose_name = "Proxmox Power Button" description = "Start/stop/reboot Proxmox VMs from the NetBox VM detail page" - version = "0.1.0" + version = "0.1.1" author = "Bryan Pedini" base_url = "proxmox-power-button" # The data migration pins extras.0134_owner / virtualization.0052_gfk_indexes, @@ -16,11 +16,13 @@ class ProxmoxPowerButtonConfig(PluginConfig): min_version = "4.5.0" # Optional behaviour, overridable via PLUGINS_CONFIG["proxmox_power_button"]: - # verify_ssl : verify the Proxmox TLS cert (default False) + # verify_ssl : verify the Proxmox TLS cert (default True — set False + # only for self-signed lab certs; the API token crosses + # this connection, so disabling verification invites MitM) # stop_mode : "shutdown" (graceful ACPI, default) or "stop" (hard kill) # reboot_mode : "reboot" (graceful, default) or "reset" (hard) default_settings = { - "verify_ssl": False, + "verify_ssl": True, "stop_mode": "shutdown", "reboot_mode": "reboot", } diff --git a/proxmox_power_button/templates/proxmox_power_button/vm_power_buttons.html b/proxmox_power_button/templates/proxmox_power_button/vm_power_buttons.html index 92df723..2990277 100644 --- a/proxmox_power_button/templates/proxmox_power_button/vm_power_buttons.html +++ b/proxmox_power_button/templates/proxmox_power_button/vm_power_buttons.html @@ -1,4 +1,5 @@ {# SPDX-License-Identifier: GPL-2.0-or-later — Copyright (C) 2026 Bryan Joshua Pedini #} +{% if perms.virtualization.change_virtualmachine %} {% if object.status == 'active' %} {# Powered on: reboot (left) + stop. #}
@@ -22,3 +23,4 @@
{% endif %} +{% endif %} diff --git a/proxmox_power_button/views.py b/proxmox_power_button/views.py index 35a7519..95f56b9 100644 --- a/proxmox_power_button/views.py +++ b/proxmox_power_button/views.py @@ -61,7 +61,12 @@ class VMPowerActionView(PermissionRequiredMixin, View): return redirect("virtualization:virtualmachine", pk=pk) def post(self, request, pk, action): - vm = get_object_or_404(VirtualMachine, pk=pk) + # Scope the lookup through NetBox's object-permission system: a user + # whose change_virtualmachine permission is constraint-scoped gets a + # 404 on VMs outside their scope, exactly like core NetBox views. + vm = get_object_or_404( + VirtualMachine.objects.restrict(request.user, "change"), pk=pk + ) if action not in VALID_ACTIONS: logger.error("user=%s vm=%s: unknown power action '%s'", request.user, vm.name, action) @@ -72,7 +77,13 @@ class VMPowerActionView(PermissionRequiredMixin, View): result = power_action(vm, action) except ProxmoxError as exc: logger.error("user=%s vm=%s: %s failed: %s", request.user, vm.name, action, exc) - messages.error(request, f"Proxmox action failed: {exc}") + # Don't echo raw Proxmox/network errors to the browser — they can + # leak internal hostnames, URLs, and response bodies. Full detail + # is in the log line above. + messages.error( + request, + f"Proxmox {action} failed for {vm.name} — see the NetBox log for details.", + ) return redirect("virtualization:virtualmachine", pk=vm.pk) if action == "start": diff --git a/pyproject.toml b/pyproject.toml index bfe673e..9cc720c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta" # name that goes into NetBox's PLUGINS list — stays `proxmox_power_button`, # matching the ecosystem convention (cf. netbox-plugin-dns / netbox_dns). name = "netbox-proxmox-power-button" -version = "0.1.0" +version = "0.1.1" description = "NetBox plugin adding Start/Stop/Reboot buttons that drive Proxmox VE VMs" readme = "README.md" license = "GPL-2.0-or-later"