Files
Netbox-Store/host_agent
MrBlake eb6be0e5fa
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run
Fix host agent socket activation
2026-08-24 23:13:10 +02:00
..
2026-08-24 23:13:10 +02:00
2026-08-24 23:13:10 +02:00
2026-08-24 20:51:25 +02:00

NetBox Store host agent (MVP)

This package is the deliberately small privileged boundary between the NetBox Store plugin and a NetBox 4.6.54.6.8 Linux host. It does not import the Store implementation or assume Django: every decision is revalidated against the configured JSON API immediately before a lifecycle action.

The daemon is dry-run by default. It serializes operations, persists idempotency and state in SQLite, accepts one bounded JSON line per Unix-stream connection, verifies Linux peer credentials, downloads only an approved immutable wheel or commit-bound source archive, and invokes subprocesses only as fixed argument arrays with shell=False.

Install and one-time operator setup

Build/install into a dedicated administrative environment, copy examples/agent.toml to /etc/netbox-store-agent/agent.toml, replace all site-specific paths, hosts, UIDs/GIDs and versions, then make the file root-owned and mode 0600. The optional bearer-token file must also be root-owned 0600. Store credentials are sent only to the exact scheme/host/port origin of store.base_url, never to a separately allow-listed artifact host.

allow_private_addresses = false is intentionally fail-closed. If the Store or an approved artifact host consciously runs on RFC1918/ULA infrastructure, set it to true only after pinning every expected hostname in allowed_hosts and deploying correctly verified TLS (including the private CA via ca_file when needed). The host allow-list and exact-origin credential rule remain active; this switch only permits private/reserved DNS results.

The agent owns only these two configured files:

  • paths.include_path, which contains only STORE_PLUGINS = [...];
  • paths.requirements_path, which contains locked wheel references. Locally built wheels are cached below /opt/netbox/.netbox-store-wheels.

In the operator-owned NetBox configuration.py, add once, after the normal PLUGINS declaration:

from netbox.store_plugins import STORE_PLUGINS

PLUGINS += STORE_PLUGINS

Place store_plugins.py where that import resolves in your deployment. Optionally add a one-time -r /opt/netbox/local_requirements_store.txt line to the operator-owned /opt/netbox/local_requirements.txt for reproducible maintenance installs. The agent never edits either operator-owned file.

Install the example systemd units, review their ReadWritePaths, SocketGroup, and ExecStart, then:

systemctl daemon-reload
systemctl enable --now netbox-store-agent.socket
netbox-store-agent capabilities

The canonical socket path used by the unit, example config, CLI default, and NetBox Store plugin is /run/netbox-store-agent/agent.sock.

With peer checks enabled (the production default), Linux SO_PEERCRED must be available and either the peer's effective UID must appear in allowed_peer_uids or its effective GID in allowed_peer_gids; Unix-socket filesystem permissions still apply. Prefer allow-listing the exact NetBox service UID, especially when socket access is granted through a supplementary group.

Keep dry_run = true through catalog and lifecycle acceptance tests. Enabling real mutation is an explicit operator configuration change.

Store JSON contract

Both endpoint templates may be configured with or without a trailing slash. A 404 is retried once using the alternate form. Path parameters are URL-quoted. The agent accepts exactly these fields.

GET /api/v1/plugins/{slug}/:

{
  "api_version": "v1",
  "slug": "netbox-example",
  "name": "Example",
  "summary": "Example plugin",
  "description": "Description",
  "repository_url": "https://git.example/repo",
  "latest_version": "1.2.3",
  "package_name": "netbox-example",
  "import_name": "netbox_example",
  "min_netbox_version": "4.6.5",
  "max_netbox_version": "4.6.8",
  "approved": true,
  "status": "approved",
  "releases": []
}

GET /api/v1/plugins/{slug}/releases/{version}/ returns a release object directly:

{
  "version": "1.2.3",
  "download_url": "https://store.example/artifacts/netbox_example-1.2.3-py3-none-any.whl",
  "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "artifact_size": 12345,
  "commit_sha": "",
  "min_netbox_version": "4.6.5",
  "max_netbox_version": "4.6.8",
  "published_at": "2026-08-24T12:00:00Z",
  "approved": true,
  "status": "approved",
  "immutable": true,
  "approved_payload_sha256": "abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd"
}

If release detail returns 404, exactly one matching object in plugin releases[] is accepted. The historic field approved_payload_sha256 is protocol-v1 naming: clients treat its lowercase 64-hex value as an opaque Store marker, and the agent compares it exactly with the freshly fetched value. Artifact sha256, in contrast, is always a lowercase 64-character SHA-256.

The plugin and release must be approved, the release immutable and compatible with the configured NetBox version. A wheel's filename distribution and version must match the catalog, and its byte count, digest, host, scheme and DNS addresses are checked while streaming. Redirects, source distributions, private/reserved DNS targets (unless explicitly enabled for a test environment), and catalog additions outside the v1 schema fail closed.

Client protocol

Transport is Unix SOCK_STREAM, UTF-8 JSON-lines, exactly one request and one response per connection, maximum 64 KiB. Response shape is always {"protocol_version":1,"status":...,"body":{...}}.

{"protocol_version":1,"method":"GET","path":"/v1/capabilities"}
{"protocol_version":1,"method":"POST","path":"/v1/operations","idempotency_key":"20f4274f-d4e5-42bf-9164-967b1a774481","body":{"request_id":"eea17d87-8944-4ee2-a076-363338ab746d","action":"install","plugin_slug":"netbox-example","version":"1.2.3","approved_payload_sha256":"abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd","requested_by":"netbox:alice"}}
{"protocol_version":1,"method":"GET","path":"/v1/operations/20f4274f-d4e5-42bf-9164-967b1a774481"}

POST returns 202. Repeating the same idempotency UUID with the identical body returns the existing operation (created:false); a different body returns 409. States are queued, running, dry_run, succeeded, failed, or manual_recovery.

CLI examples:

netbox-store-agent submit install netbox-example --version 1.2.3 \
  --approved-payload-sha256 abcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcdefabcd
netbox-store-agent status 20f4274f-d4e5-42bf-9164-967b1a774481

Lifecycle and fail-closed boundaries

  • install and update re-fetch plugin and release, exactly match the approval token, download and validate the wheel, then use pip --no-index --no-deps --only-binary=:all: --require-hashes.
  • Source archives must be tied to an exact commit. The agent rejects unsafe archive members and requires configured unshare/setpriv executables plus a dedicated build UID/GID. It builds without network access and with dropped privileges via pip wheel --no-index --no-deps --no-build-isolation, validates the result, and caches only that wheel for future NetBox upgrades. Reviewing source code remains a privileged trust decision because Python build backends execute code.
  • New installs remain disabled. Updating an enabled plugin runs NetBox migrate, collectstatic, and restarts every configured service (netbox and netbox-rq in the example).
  • enable revalidates its installed release, writes the include, migrates, collects static files, and restarts both services. disable writes the include and restarts both. uninstall is allowed only after disable and revalidates current approved plugin identity first.
  • Self-management slugs are denied for every action. Package/import names and all executable paths, service units, managed paths, Store origins and NetBox compatibility are configuration/catalog policy—not caller-controlled command fragments.
  • A root-owned global file lock prevents simultaneous host mutations. Interrupted running operations become manual_recovery at startup. Managed files are backed up per operation and atomically replaced; best-effort restoration does not claim package/service rollback.

This MVP intentionally has no TUF metadata, dependency-wheel set, transactional virtualenv switch, or reliable rollback for package installation/database migrations/service restarts. --no-deps means an approved plugin's dependencies must already be provisioned by the operator/base image. Any failure after host mutation is marked manual_recovery; inspect the journal, backup directory, installed distributions, migrations, both services and both managed files before retrying. If the Store is unavailable or an entry is no longer approved, lifecycle calls fail closed; emergency manual recovery remains an operator procedure outside this API.

The security boundary still depends on root ownership and permissions of the daemon executable, configuration, token, journal/state directories, socket and NetBox paths; TLS/CA integrity; Store approval operations; artifact build provenance; and a correctly restricted NetBox service account.