# 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`: ```text netbox-plugin-store==0.1.3 ``` Add the plugin to `configuration.py`: ```python 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//` - `GET /api/v1/plugins//releases//` (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. ### `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): ```python "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/`. 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. ```python "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: ```bash python -m unittest discover -s tests -v ```