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.
Authoring a policy
Section titled “Authoring a policy”You don’t need to touch JSON to build one. NetDefense exposes three imperative verbs and a waive inverse:
# Create an empty policyndcli software create monitoring-tools
# Populate itndcli software require-package monitoring-tools os-zabbix72-agent bashndcli software block-package monitoring-tools os-zabbix6-agent os-zabbix74-agent
# Change your mindndcli software waive-package monitoring-tools bash # was required; we stop caringndcli software waive-package monitoring-tools os-zabbix6-agent # was blocked; we stop caring
# Inspect — also lists every template this policy is attached tondcli software describe monitoring-toolsEach 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"]}ndcli software create monitoring-tools --file ./monitoring-tools.json# or inlinendcli 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.
Package names
Section titled “Package names”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:
- Unioning every required package across every policy that applies to the device.
- Unioning every blocked package across every policy that applies to the device.
- 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.
How reconciliation runs
Section titled “How reconciliation runs”When a sync task reaches a device, the package reconciliation step:
- Refreshes the local repository catalog (
pkg update -q) once. - Processes every blocked name. If the package is installed, NetDefense removes it. If not, it’s recorded as
ALREADY_ABSENT. - 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:
| Action | Meaning |
|---|---|
INSTALLED | The package was missing; NetDefense installed it. |
ALREADY_PRESENT | The package was already installed; no change. |
REMOVED | The package was installed; NetDefense removed it. |
ALREADY_ABSENT | The package was already missing; no change. |
NOT_FOUND | No configured repository has a package with that name. Task fails. |
INVALID_NAME | The name failed the on-device safety regex. Task fails. |
ERROR | pkg 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.
What software policies don’t do
Section titled “What software policies don’t do”- Plugin activation. Some OPNsense plugins need a follow-up
pluginctlor “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-installtask instead.
End-to-end example
Section titled “End-to-end example”# 1. Build the policy without writing any JSONndcli software create monitoring-toolsndcli software require-package monitoring-tools os-zabbix72-agent bashndcli software block-package monitoring-tools os-zabbix6-agent os-zabbix74-agent
# 2. Attach it to the same template that carries your Zabbix snippetsndcli template add-software network-monitoring monitoring-tools
# 3. Push the change to every device the template coversndcli sync apply
# 4. Inspect the resultndcli software describe monitoring-toolsndcli task describe <task-code> # shows per-package INSTALLED / REMOVED / etc.Open Software Policies in the sidebar, click New policy, give it a
name, and save. The detail page shows two structured lists — Required
and Blocked — each with an input + Add button at the bottom and an
× on every entry to drop it. Each click is saved immediately; there’s
no separate Save step. The page also shows every template the policy
is currently attached to, so you can see the policy’s reach without
hopping pages. Adding a package that’s already on the opposite list
moves it across in one step. Then open the template you want to attach
it to and add the software policy from the template’s attachments panel.
Finally, trigger a sync from Synchronize or wait for the next
auto-sync window if the device’s auto_sync flag is on.