Skip to content

Software Policies

A software policy is a reusable, named list of packages NetDefense will keep installed or uninstalled on every device that picks the policy up through its templates. Each policy carries two lists:

  • Required packages: NetDefense installs them if a device is missing them, leaves them alone if already there.
  • Blocked packages: NetDefense uninstalls them if a device has them, leaves them alone if already gone.

Software policies pair naturally with the snippets that configure the same software. A monitoring-tools policy that requires os-zabbix72-agent is typically attached to the same template that carries the Zabbix Agent settings snippet and the firewall rules permitting Zabbix server traffic.

You don’t need to touch JSON to build one. NetDefense exposes three imperative verbs and a waive inverse:

Terminal window
# Create an empty policy
ndcli software create monitoring-tools
# Populate it
ndcli software require-package monitoring-tools os-zabbix72-agent bash
ndcli software block-package monitoring-tools os-zabbix6-agent os-zabbix74-agent
# Change your mind
ndcli software waive-package monitoring-tools bash # was required; we stop caring
ndcli software waive-package monitoring-tools os-zabbix6-agent # was blocked; we stop caring
# Inspect — also lists every template this policy is attached to
ndcli software describe monitoring-tools

Each verb accepts one or more package names. You don’t have to remember which list a package is in when you run waive-package — it removes the package from whichever side it’s on, and it’s a no-op if the package isn’t specified anywhere.

describe includes a Templates line listing every template this policy is currently attached to, so you can see at a glance which fleet slices the policy will reach on the next sync.

For bulk seeding (or migration from an external inventory), you can also pass a JSON document directly:

{
"present": ["os-zabbix72-agent", "bash"],
"absent": ["os-zabbix6-agent", "os-zabbix74-agent"]
}
Terminal window
ndcli software create monitoring-tools --file ./monitoring-tools.json
# or inline
ndcli software create monitoring-tools --content '{"present":["bash"],"absent":[]}'

The wire keys are present (= required) and absent (= blocked) — different vocabulary because the document is a declarative state while the CLI verbs express operator intent, but they describe the same policy.

NetDefense doesn’t distinguish between an OPNsense plugin (names starting with os-) and a plain FreeBSD package — they’re the same to the underlying pkg tooling. Use the exact name as it appears in the OPNsense repository or the FreeBSD ports catalog. NetDefense does not verify that a name resolves to a real package until reconciliation time on the device, where unknown names surface as a NOT_FOUND action and fail the sync task.

For security, package names are restricted to the character set [A-Za-z0-9._+-], must start with a letter or digit, and are capped at 100 characters. Each list is capped at 200 entries. These limits exist so a policy can never inject shell metacharacters or arbitrarily-long arguments into the pkg command line — every package name reaches pkg as a separate argv entry, never through a shell.

How conflicts are resolved across policies

Section titled “How conflicts are resolved across policies”

A single device can pick up many policies — one per template, and many templates per organizational unit. NetDefense computes the final desired state by:

  1. Unioning every required package across every policy that applies to the device.
  2. Unioning every blocked package across every policy that applies to the device.
  3. Removing from the blocked union anything that also appears in the required union. Required wins.

That means if one team’s policy requires os-zabbix72-agent and another’s blocks it, the agent installs it. The blocked list is interpreted as “if no one else explicitly wants this, get rid of it” — never destructive in the face of an explicit require.

Within a single policy document the validator is stricter: you can’t have the same name in both lists, and you can’t repeat a name in one list. The CLI verbs already enforce this — block-package on something currently required just moves it, no conflict to surface.

When a sync task reaches a device, the package reconciliation step:

  1. Refreshes the local repository catalog (pkg update -q) once.
  2. Processes every blocked name. If the package is installed, NetDefense removes it. If not, it’s recorded as ALREADY_ABSENT.
  3. Processes every required name. If the package is missing, NetDefense installs it. If it’s already there, it’s recorded as ALREADY_PRESENT.

Blocked runs before required so an older plugin can step out of the way before a newer one tries to install — for example, removing os-zabbix6-agent before installing os-zabbix72-agent.

The sync task result includes one entry per package with the action that was taken:

ActionMeaning
INSTALLEDThe package was missing; NetDefense installed it.
ALREADY_PRESENTThe package was already installed; no change.
REMOVEDThe package was installed; NetDefense removed it.
ALREADY_ABSENTThe package was already missing; no change.
NOT_FOUNDNo configured repository has a package with that name. Task fails.
INVALID_NAMEThe name failed the on-device safety regex. Task fails.
ERRORpkg refused for another reason (dependency conflict, etc.). Fails.

A sync task fails as a whole if any package ended in NOT_FOUND, INVALID_NAME, or ERROR. This mirrors how a snippet failure fails the sync task — partial success isn’t reported as success.

  • Plugin activation. Some OPNsense plugins need a follow-up pluginctl or “Apply” step in the web UI before they’re actually serving. NetDefense’s software policy step stops at install. The plugin will activate through its normal configuration path — often the very snippet that ships its settings.
  • Service restarts. Installing or removing a package doesn’t automatically reload other services that consume it.
  • Version pinning. The current shape only carries package names; the agent installs whatever version the repository serves. For deliberate plugin version management, use the ndcli run plugin-install task instead.
Terminal window
# 1. Build the policy without writing any JSON
ndcli software create monitoring-tools
ndcli software require-package monitoring-tools os-zabbix72-agent bash
ndcli software block-package monitoring-tools os-zabbix6-agent os-zabbix74-agent
# 2. Attach it to the same template that carries your Zabbix snippets
ndcli template add-software network-monitoring monitoring-tools
# 3. Push the change to every device the template covers
ndcli sync apply
# 4. Inspect the result
ndcli software describe monitoring-tools
ndcli task describe <task-code> # shows per-package INSTALLED / REMOVED / etc.