Files
MrBlake 26aea40e6a
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
feat: install plugins from approved source commits
2026-08-24 21:37:17 +02:00
..
2026-08-24 20:51:25 +02:00
2026-08-24 20:51:25 +02:00
2026-08-24 20:51:25 +02:00
2026-08-24 20:51:25 +02:00
2026-08-24 20:51:25 +02:00
2026-08-24 20:51:25 +02:00

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-index by default and runs python -m pip check after install/update.
  • configuration.py and local_requirements.txt are changed via same-directory temporary files plus os.replace(). Existing files receive timestamped 0600 backups. 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, and releases
  • release fields: version, download_url, sha256, artifact_size, commit_sha, min_netbox_version, max_netbox_version, published_at, approved, status, immutable, and opaque approved_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.

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