Files
Netbox-Store/host_agent/README.md
T
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

178 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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:
```python
from 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:
```text
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}/`:
```json
{
"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:
```json
{
"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":{...}}`.
```json
{"protocol_version":1,"method":"GET","path":"/v1/capabilities"}
```
```json
{"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"}}
```
```json
{"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:
```text
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.