6.9 KiB
NetBox Plugin Store
netbox-plugin-store is the NetBox-side client for an approved internal plugin catalog. It supports NetBox 4.6.5 through 4.6.8 and provides catalog, status, confirmation, and redacted audit pages.
The safe default is intentionally non-mutating. Installing the UI alone never grants the NetBox web process permission to modify its Python environment.
Security model
- Catalog, status, audit, and dry-run access require an authenticated user with
netbox_plugin_store.manage_plugin(superusers implicitly have it). - Every state-changing POST is checked again in the view and requires a superuser. A menu permission alone is never sufficient.
- Lifecycle endpoints accept POST only, use Django CSRF protection, and require the user to type the exact plugin slug on a separate confirmation page.
- Package, import, slug, version, URL, and SHA-256 fields are validated. The Store plugin cannot update, disable, or uninstall itself.
- Installation/update requires an explicitly approved plugin and an explicitly approved, immutable release with a 64-character artifact SHA-256. A Git commit SHA is not an artifact hash and is rejected.
- Downloads go to a private temporary directory and are SHA-256 verified before pip sees the file. Redirects and pagination remain inside configured URL allowlists.
- Pip and maintenance commands use argv arrays with
shell=False; direct mode uses--no-indexby default and runspython -m pip checkafter install/update. configuration.pyandlocal_requirements.txtare changed via same-directory temporary files plusos.replace(). Existing files receive timestamped0600backups. A cross-process file lock serializes operations.- Audit input/output is bounded and redacted for common token, password, authorization, and credential patterns.
- Install only installs and pins a package; it remains disabled. Enable is a separate action which edits
PLUGINS, runs migrations/collectstatic, and requires restart. Uninstall is accepted only after an explicit disable.
Installation
Install the wheel into NetBox's virtual environment and persist it in /opt/netbox/local_requirements.txt:
netbox-plugin-store==0.1.0
Add the plugin to configuration.py:
PLUGINS = [
"netbox_plugin_store",
]
PLUGINS_CONFIG = {
"netbox_plugin_store": {
# Required: this is the separate Store service, not the Forgejo host.
"store_url": "https://store.example.internal",
"allowed_store_urls": ["https://store.example.internal"],
# Include every origin from which approved immutable artifacts are served.
"allowed_artifact_urls": ["https://store.example.internal", "https://git.mrblake.cc"],
"api_token": "", # Prefer a read-only catalog token if authentication is required.
"execution_mode": "dry_run",
}
}
Then use NetBox's supported upgrade flow (normally /opt/netbox/upgrade.sh) or run migrate/collectstatic and restart the web and RQ services. The service user needs read access to the Store and configuration. Dry-run mode needs no venv/config write permission.
The Store API contract is:
GET /api/v1/plugins/(plain list or{ "results": [...] }pagination)GET /api/v1/plugins/<slug>/GET /api/v1/plugins/<slug>/releases/<version>/(used by the host agent; returns one release object)- plugin fields:
slug,name,summary,description,repository_url,latest_version,package_name,import_name,min_netbox_version,max_netbox_version,approved/status, andreleases - release fields:
version,download_url,sha256,artifact_size,commit_sha,min_netbox_version,max_netbox_version,published_at,approved,status,immutable, and opaqueapproved_payload_sha256
Execution modes
dry_run (default)
Only validates and plans. default_dry_run also defaults to True. Dry-runs may use NetBox's default RQ queue; real operations never run inside netbox-rq, because restarting the same worker would leave its job state inconsistent.
agent (recommended for production)
Use a separately privileged host agent. NetBox connects to an absolute Unix socket and sends one JSON line per connection (maximum 64 KiB):
"execution_mode": "agent",
"agent_socket_path": "/run/netbox-store-agent/agent.sock",
"agent_timeout": 30,
Protocol version 1 uses GET /v1/capabilities, POST /v1/operations, and GET /v1/operations/<uuid>. The POST includes a UUID idempotency key and only the action, slug, version, actor, request ID, and the Store's opaque approved_payload_sha256. The agent re-fetches the approved catalog record itself.
The mutating HTTP request stops immediately after the agent accepts the operation. It does not poll while the agent might restart NetBox. Opening the audit detail page performs one status query and reconciles a completed/failed operation.
The Unix socket should be owned by the agent group, writable only by the NetBox service account/group, and placed in a non-world-writable directory.
direct (development/controlled installations only)
Direct mode additionally requires allow_lifecycle_mutations=True. The NetBox service account must be able to write the venv, configuration.py, local_requirements.txt, backup directory, and lock file. This is often inappropriate for production.
"execution_mode": "direct",
"allow_lifecycle_mutations": True,
"configuration_path": "/opt/netbox/netbox/netbox/configuration.py",
"requirements_path": "/opt/netbox/local_requirements.txt",
"manage_path": "/opt/netbox/netbox/manage.py",
"allow_package_index": False,
With allow_package_index=False, pip receives --no-index; therefore all transitive dependencies must already be installed or available in the artifact. Enabling allow_package_index is an explicit supply-chain policy decision.
Direct mode never performs automatic restart. Enable, disable, and updating an enabled plugin are marked restart-required; restart NetBox web services and workers out of band. auto_restart=True is rejected in direct mode.
For agent-managed restarts, restart_commands must match restart_allowlist token-for-token. Do not use a shell command string. The host agent must enforce its own independent command policy.
Persistence and rollback
Install/update writes an immutable PEP 508 direct URL plus #sha256=... into local_requirements.txt; uninstall removes it. This keeps plugins present across NetBox's supported upgrade flow. Config/requirements writes are atomic and backed up under plugin-store-backups by default. The direct executor performs compensating rollback where safe; an interrupted pip upgrade or already-applied database migration can still require operator recovery, which is recorded as failed/restart-required.
Test
The core tests use fake downloads, subprocesses, repositories, and sockets; they never invoke real pip, NetBox restarts, or maintenance commands:
python -m unittest discover -s tests -v