1 Commits
0.1.0 ... main

Author SHA1 Message Date
b59fbd1fa8 secured the plugin a bit for 0.1.1 2026-08-05 05:38:53 +02:00
5 changed files with 47 additions and 12 deletions

View File

@@ -19,7 +19,7 @@ PLUGINS = ["proxmox_power_button"]
PLUGINS_CONFIG = { PLUGINS_CONFIG = {
"proxmox_power_button": { "proxmox_power_button": {
"verify_ssl": False, "verify_ssl": True, # set False only for self-signed lab certs
"stop_mode": "shutdown", "stop_mode": "shutdown",
"reboot_mode": "reboot", "reboot_mode": "reboot",
}, },
@@ -82,15 +82,35 @@ for both QEMU and LXC.
| key | default | meaning | | 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) | | `stop_mode` | `"shutdown"` | `shutdown` (graceful ACPI) or `stop` (hard) |
| `reboot_mode` | `"reboot"` | `reboot` (graceful) or `reset` (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 - **Token is stored in a plain-text custom field.** It is visible to anyone who
can view the cluster. Use a scoped, least-privilege Proxmox API token and can *view* the cluster — in the UI, in REST API responses for the cluster,
restrict cluster view permissions. NetBox has no "secret" custom-field type. 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 - **Requires NetBox ≥ 4.5.0.** The data migration depends on
`extras.0134_owner` and `virtualization.0052_gfk_indexes`, which first appear `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 in 4.5.0; on 4.4 or older `migrate` fails with `NodeNotFoundError`. Bump

View File

@@ -7,7 +7,7 @@ class ProxmoxPowerButtonConfig(PluginConfig):
name = "proxmox_power_button" name = "proxmox_power_button"
verbose_name = "Proxmox Power Button" verbose_name = "Proxmox Power Button"
description = "Start/stop/reboot Proxmox VMs from the NetBox VM detail page" description = "Start/stop/reboot Proxmox VMs from the NetBox VM detail page"
version = "0.1.0" version = "0.1.1"
author = "Bryan Pedini" author = "Bryan Pedini"
base_url = "proxmox-power-button" base_url = "proxmox-power-button"
# The data migration pins extras.0134_owner / virtualization.0052_gfk_indexes, # The data migration pins extras.0134_owner / virtualization.0052_gfk_indexes,
@@ -16,11 +16,13 @@ class ProxmoxPowerButtonConfig(PluginConfig):
min_version = "4.5.0" min_version = "4.5.0"
# Optional behaviour, overridable via PLUGINS_CONFIG["proxmox_power_button"]: # 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) # stop_mode : "shutdown" (graceful ACPI, default) or "stop" (hard kill)
# reboot_mode : "reboot" (graceful, default) or "reset" (hard) # reboot_mode : "reboot" (graceful, default) or "reset" (hard)
default_settings = { default_settings = {
"verify_ssl": False, "verify_ssl": True,
"stop_mode": "shutdown", "stop_mode": "shutdown",
"reboot_mode": "reboot", "reboot_mode": "reboot",
} }

View File

@@ -1,4 +1,5 @@
{# SPDX-License-Identifier: GPL-2.0-or-later — Copyright (C) 2026 Bryan Joshua Pedini #} {# SPDX-License-Identifier: GPL-2.0-or-later — Copyright (C) 2026 Bryan Joshua Pedini #}
{% if perms.virtualization.change_virtualmachine %}
{% if object.status == 'active' %} {% if object.status == 'active' %}
{# Powered on: reboot (left) + stop. #} {# Powered on: reboot (left) + stop. #}
<form class="d-inline" method="post" action="{% url 'plugins:proxmox_power_button:power' pk=object.pk action='reboot' %}"> <form class="d-inline" method="post" action="{% url 'plugins:proxmox_power_button:power' pk=object.pk action='reboot' %}">
@@ -22,3 +23,4 @@
</button> </button>
</form> </form>
{% endif %} {% endif %}
{% endif %}

View File

@@ -61,7 +61,12 @@ class VMPowerActionView(PermissionRequiredMixin, View):
return redirect("virtualization:virtualmachine", pk=pk) return redirect("virtualization:virtualmachine", pk=pk)
def post(self, request, pk, action): 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: if action not in VALID_ACTIONS:
logger.error("user=%s vm=%s: unknown power action '%s'", request.user, vm.name, action) 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) result = power_action(vm, action)
except ProxmoxError as exc: except ProxmoxError as exc:
logger.error("user=%s vm=%s: %s failed: %s", request.user, vm.name, action, 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) return redirect("virtualization:virtualmachine", pk=vm.pk)
if action == "start": if action == "start":

View File

@@ -11,7 +11,7 @@ build-backend = "setuptools.build_meta"
# name that goes into NetBox's PLUGINS list — stays `proxmox_power_button`, # name that goes into NetBox's PLUGINS list — stays `proxmox_power_button`,
# matching the ecosystem convention (cf. netbox-plugin-dns / netbox_dns). # matching the ecosystem convention (cf. netbox-plugin-dns / netbox_dns).
name = "netbox-proxmox-power-button" 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" description = "NetBox plugin adding Start/Stop/Reboot buttons that drive Proxmox VE VMs"
readme = "README.md" readme = "README.md"
license = "GPL-2.0-or-later" license = "GPL-2.0-or-later"