NetDefense can create and manage external authentication servers — LDAP or Active Directory directories that an OPNsense device checks passwords against — and the login order that decides which sources are tried, and in what sequence, for the web administration interface and password-based console and SSH access.
This is a security-sensitive surface: get the login order wrong and you can lock yourself out of a device, or leave a directory account able to shadow a privileged local one. Read Scope, Prerequisites, and the recovery runbook before you turn this on for a device you can’t reach physically.
ndagent 1.19.2 or later on every device you’ll manage this way.
OPNsense 26.1.6 or later on every device — read locally from the running system. Older releases are refused for every write (creating or updating a server, or writing a login order); deleting is always allowed regardless of version.
ndcli 1.35.0 or later if you author these snippets or set the bind-password variable from the command line (ndcli version; upgrade with brew/scoop).
The “Allow All Snippet Content” device setting must be turned on (it’s off by default on a fresh install) on any device where you want NetDefense to create, update, or write a login order — see The consent switch. A device upgraded from before ndagent v1.14.0 may already have this open — check the setting itself rather than assuming a new device’s default applies.
The version floor exists in part because OPNsense 26.1.6 fixed an LDAP filter-injection flaw in the login username (CVE-2026-34578) that could enumerate directory users and bypass an Extended Query group restriction; releases up to 26.1.5 are affected. Upgrade OPNsense before turning on directory management for a device still on an older release — see Firmware Upgrades.
An authentication server is a snippet, just like a firewall rule or an alias — see Snippets if that concept is new. Content is the same JSON keys OPNsense itself uses:
In the web app, open Snippets → + Create Snippet → Create from scratch, then pick Auth Server (LDAP) or Auth Order from the Type menu. For a server, also fill in Server name (as it will appear on the device) — it becomes the permanent name. Create & Edit saves the snippet immediately with example placeholder content (a Corp AD-style server, or an order of just ["Local Database"]) — open the snippet’s Content editor afterward and replace the placeholders with your own values before attaching it to a template. Auth Server snippets can’t be cloned, since the copy would carry the same frozen name.
A few things worth knowing before you fill this in:
name is permanent. Once created, a server’s name can’t be changed — because the login order and every device it’s delivered to refer to it by that exact name. Changing it in place is refused (AUTH_SERVER_NAME_IMMUTABLE). To rename, create a new server under the new name, add it to every template that held the old one (ndcli template describe lists a template’s snippets), point the login order at it, and then delete the old one.
Local Database and Local API are reserved. You can’t create a server with either name — Local Database is what OPNsense calls its own built-in local account check, and it always has to remain available in the login order (see Local Database comes first).
The bind credential is both or neither. Set ldap_binddn and ldap_bindpw together for an authenticated bind, or leave both out for an anonymous one. ldap_bindpw is never a literal password — see Secret variables for the bind password.
caseInSensitiveUsernames only works with attributes that are already case-insensitive in the directory (sAMAccountName, uid, cn, mail, userPrincipalName). It’s refused with anything else, since that combination would be a way to shadow an account by case alone.
Non-ASCII values are fine in ldap_basedn and ldap_authcn — organizational unit names with accented characters, for example — and round-trip correctly.
ldap_urltype accepts "StartTLS", "SSL - Encrypted", or "TCP - Standard". The last one sends every login’s password to the directory in clear text on the network. NetDefense requires an explicit, human-typed acknowledgement to create a server that way — add "nd_allow_cleartext_ldap": true to the content, or the create is refused. Prefer StartTLS or SSL - Encrypted; see The CA prerequisite for what those need first.
ldap_bindpw must be written as exactly ${a_variable_name} — a whole-value reference to an organization-scope secret variable. A literal password in the snippet content is refused outright; NetDefense never lets a directory bind password sit in a snippet’s stored content.
Create the AUTH_SERVER snippet first, referencing the variable by name, before the variable exists. The snippet can be created, saved, and even attached to a template while the variable is still missing — a build only actually needs the variable to resolve at the device it’s about to sync to.
Then create the secret variable. From the command line, keep the value off the command line entirely — a literal password passed as an argument lands in shell history and is visible to other users on the host via ps, which is exactly what --value-stdin exists to avoid. Redirect it from a file, or pipe it from a password manager’s own CLI:
Exactly one trailing newline is trimmed, so a file created with a normal text editor works without extra care. Leave off --value-stdin and the value argument both, and you’re prompted interactively instead — the input isn’t echoed to the terminal either way.
In the web app, this is just as safe: go to Variables → + Create Variable, turn on Secret, and type the password into Value and Confirm Value (the two must match before the form submits). Nothing about typing it into that dialog lands it in shell history or ps.
Never pipe or type a secret value through the MCP server or an AI assistant tool — the MCP variable tools refuse to write a secret value at all, by design. Use ndcli or the web app’s Variables page instead.
Sync. The password reaches the device the same way any other resolved configuration does — see Where the password lives for what that means at rest.
Rotating the password. If you ever repoint ldap_bindpw at a different variable, or change which server uses which credential, rotate the directory-side password for the one you moved away from. NetDefense has no way to invalidate a bind credential on the directory server itself — see Where the password lives for why a copy can outlive the change on the NetDefense side.
The variable you moved away from also stops being protected the moment you repoint: NetDefense only treats a secret variable as bound to authentication while some ldap_bindpw in the org is actively referencing it, so after a repoint it’s no longer covered by the misuse check described above and could legally be reused somewhere less protected. Delete the old variable or rotate its value too, rather than leaving it around unbound.
An authentication server can mirror directory group membership into NetDefense-managed groups instead of managing membership by hand:
{
"name": "corp-engineering",
"priv": ["page-firewall-rules-edit"],
"external_members": true
}
A GROUP snippet with "external_members": true never carries a members list of its own — the directory is the source of truth for who’s in it. NetDefense still owns the group’s existence, its priv set, and its source_networks; only membership comes from the directory. Turning external_members on for a group that still lists members in its stored content is refused — detach the group and attach an external version instead of flipping the flag in place.
To make an authentication server populate one of these groups, name it in the server’s ldap_sync_memberof_groups (or, with ldap_sync_create_local_users on, add every newly created user to a group with ldap_sync_default_groups):
Every name in ldap_sync_memberof_groups or ldap_sync_default_groups must be an external GROUP snippet delivered to the same device. This is checked when NetDefense builds the configuration for a sync to that device — not when you save the AUTH_SERVER snippet. Saving a snippet that names a group NetDefense doesn’t manage as external, or one that isn’t delivered to the device at all, succeeds; the next sync to every device that would receive it fails outright, with AUTH_GROUP_NOT_ATTACHED (no matching external GROUP in the payload) or AUTH_GROUP_NOT_EXTERNAL (the group is there but still manages its own membership). No configuration at all — firewall rules included — reaches those devices until the mismatch is fixed. Separately, and checked at save time instead: NetDefense refuses to create a server that syncs memberOf without a scoped, non-empty Limit groups list (ldap_sync_memberof_groups). An unscoped sync matches a user’s directory groups against every local group by name — admins included — so a directory group that happens to be named admins would hand out full administrator rights. That is not something to opt into by omission.
A local user OPNsense creates automatically for a directory login (via ldap_sync_create_local_users) is unmanaged — NetDefense didn’t create it, doesn’t tag it, and doesn’t sweep it away later. It survives even after the device is removed from NetDefense.
It’s also WebGUI-only. OPNsense creates this account with no login shell, and it skips shell-less accounts when it builds the system’s local login database — so the account can sign in to the web interface but can never complete a password login over SSH, at the console, or through su/sudo, no matter where its directory sits in the webadmin order. Password SSH, the console, and su/sudo only ever work for a local account that has a shell, and an auto-created directory account isn’t one.
Make each identity either NetDefense-managed or directory-provisioned, not both; mixing the two on the same username is not something NetDefense reconciles for you.
The login order — which authentication server wins, and in what sequence — is a second snippet type, AUTH_ORDER, kept deliberately separate from the servers themselves:
The facility is webadmin. Its order is a list of up to 8 names, tried first to last — the first one that successfully authenticates the WebGUI login wins. Password SSH, the console, and su/sudo consult the same order, but only ever grant access to a local account that has a login shell — see Scope for what that leaves out. HQ-LDAP above is a normal string; it doesn’t have to be an AUTH_SERVER snippet at all — see Order policies can name servers you didn’t create.
* Local accounts with a login shell only. A directory identity only ever reaches the WebGUI through this order. See Local users the directory creates for why password SSH, the console, and su/sudo are out of reach for it no matter where it sits in the order.
A dead directory server in the order costs real time on every login attempt — OPNsense waits out a connection timeout (on the order of 30 seconds) before trying the next entry. Every order NetDefense writes always leads with Local Database (see below), so a local account keeps working even when every directory is down — this isn’t something to remember to do, it’s enforced on write. It matters most for an order edited by hand directly on the device: a hand-edited webadmin order made up entirely of unreachable servers, with no Local Database entry, denies password login outright.
Every webadmin order must list Local Database first. This isn’t optional and can’t be turned off: it’s the one thing standing between a directory outage (or a misconfigured order) and being completely locked out of password-based access. A policy that doesn’t lead with it is refused before it’s ever written.
NetDefense can’t check, when you save an order policy, whether every name in it actually exists — a name might refer to a server made by hand that only exists on the device, which NetDefense has no visibility into at save time. So the check happens later, when the policy actually reaches the device:
every name must be an auth server delivered to the same device by any of its templates, or already configured on the device; names match exactly (case-sensitive); “Local Database” is always valid
Because of the rule above, a typo in an order — HQ-LADP instead of HQ-LDAP — passes validation when you save the snippet. It’s caught on the device during the next sync: the sync fails, the error names the entry that didn’t resolve and lists the server names that do exist on that device, and the login order is not written at all — the device keeps whatever order it already had. Every other part of that sync (firewall rules, aliases, everything else in the same push) still applies; only the login-order write is skipped.
Detaching an AUTH_ORDER snippet’s template, or removing the device from the OU that delivered it, does not restore anything. The webadmin facility simply stops being defined for that device — the device keeps whatever order was last written, and NetDefense stops touching it. This mirrors how NetDefense already treats a handful of other singleton settings; see Removing snippets from templates for the general pattern.
To deliberately put a device back to just the local account, push an order of exactly ["Local Database"] and sync.
Removing a device does something similar automatically, but only conditionally: decommission resets the order only if the live value still matches what NetDefense last wrote. If you (or someone else) edited the order by hand after NetDefense wrote it, decommission leaves it exactly as it is and logs the fact instead of overwriting your edit. See NDAgent Removal for the rest of what decommission does to authentication servers and the login order.
Detaching or deleting an AUTH_SERVER snippet that a written login order still names is accepted on the NetDefense side, but the device keeps the server — retained, not deleted, with the login order named as the reference — and that sync (and every one after it) fails until the reference is gone, because removing the server would otherwise pull an active login path out from under the device. Rewrite the order to drop the name first, sync, and only then delete the server. If the device has “Allow All Snippet Content” off, NetDefense can’t write that reordered policy either without you temporarily allowing it — the fallback path is a remote session at full access, or an edit on the device itself (see Hand edits and sync apply --force).
Hand-made servers get none of NetDefense’s guardrails
An order policy can name a server that NetDefense never created — one configured by hand on the device through OPNsense’s own Authentication Servers page. NetDefense will happily include it in the order (subject to the consistency rule), but it never modifies, adopts, or manages that server, and none of the following protections apply to it:
No reserved-name exclusion — a directory account named root or matching the agent’s own service account can shadow a privileged local identity.
No requirement to scope group sync with Limit groups.
No refusal of a protected group name.
No cleartext acknowledgement, even over "TCP - Standard".
When a written order names a hand-made server, NetDefense reports it as a warning listing exactly which of the above apply — cleartext, unscoped group sync, a protected group name, or no reserved-name exclusion — but checking and fixing any of it is on you, not NetDefense.
A server made by hand with the exact same name (case-sensitive, byte for byte) as one NetDefense is about to create is a collision, not an adoption: NetDefense never touches it, and the create fails naming the collision. Rename one of the two, or remove the hand-made one, to proceed. (Two NetDefense-authored snippets whose names differ only by case are a separate, build-time conflict — AUTH_SERVER_NAME_CONFLICT — because OPNsense’s own name comparisons are case-sensitive but NetDefense’s own snippet-naming check isn’t.)
Authoring authentication-server content needs the Superuser role: creating, updating, renaming, or reprioritizing an AUTH_SERVER or AUTH_ORDER snippet, deleting either, or flipping a group’s external_members flag on. See Organization Roles for what that means generally.
A Read-Write residual worth knowing about. Deleting or detaching an externalGROUP (one with external_members: true), or deleting or detaching a secret variable that an AUTH_SERVER’s ldap_bindpw references, only needs Read-Write — and either one can stop every sync to a device, firewall rules included, until it’s fixed. This is a deliberate tradeoff, the same one as the attach residual below, not a bug to route around.
Assigning already-authored content is a lower bar. Adding an authentication-carrying template to an OU, adding an authentication snippet to a template, or moving a device into an OU that carries one, only needs Read-Write — the same as any other template or OU change. In practice this means a Read-Write member can put a Superuser-authored policy — including one that resets a device’s login order down to ["Local Database"] — onto more devices, without being able to author that content themselves. Treat authentication-carrying templates the same way you’d treat any other high-impact template when deciding who has Read-Write on an organization.
NetDefense doesn’t need a database record to know which servers and login orders it owns — it marks them directly on the device, the same general idea as the markers used for other snippet types, just stored differently because these two settings have no equivalent OPNsense API to tag through:
Object
Marker
Authentication server (AUTH_SERVER)
A hidden child element, <netdefense_owner>, on the server’s own configuration entry, holding the device’s identity
Login order (AUTH_ORDER), per facility
A hidden value, system/webgui/netdefense_authmode_owner, recording the device’s identity together with a fingerprint of the value NetDefense wrote
Don’t remove any netdefense_* element by hand. What happens if you do depends on which marker it is:
On a server: NetDefense no longer recognizes it as its own, and treats it as made by hand instead — it stops managing it, which shows up later as an unexpected collision if a same-named server is created, and it’s never deleted as a side effect.
On a login order: the order itself is untouched either way. The practical effect is narrower and shows up later, at decommission: without the marker, decommission can no longer tell whether the live order is still what NetDefense wrote, so it leaves that facility alone instead of resetting it.
Detaching an AUTH_SERVER snippet’s template, or deleting the snippet, is accepted on the NetDefense side even while something else on the device still points at the server — a written login order (see Removing a server from login), an OpenVPN instance’s authentication source, an IPsec user source, a Captive Portal zone, or the web proxy’s authentication method. What happens on the device is different: the server is retained, not deleted, reported with CONSUMER_REFERENCED and the specific path still referencing it — and because the sync couldn’t do what it was asked to, that sync fails. Every later sync to that device fails the same way until the reference is removed. A plain text mention elsewhere on the device — the server’s name showing up inside a host alias or an override, for example — is reported only as an informational warning and never blocks anything; those are common enough (dc01 is a very ordinary hostname fragment) that treating every text match as a hard block would be its own hazard.
Creating or updating an authentication server, and writing a login order, are gated by the same device-side setting that gates every other privilege-sensitive push: “Allow All Snippet Content” in the OPNsense plugin’s advanced settings. It’s off by default on a fresh install, and while it’s off, those specific pushes are refused at the device — even from a fully authenticated, correctly signed sync. Turn it on to allow them. A device upgraded from before ndagent v1.14.0 keeps its pre-upgrade behavior, which may already be the permissive state — check the setting rather than assuming it’s off.
The agent’s own name for this setting, which its refusal messages use, is reject_dangerous_snippets — with the boolean flipped. Its default, true, is the same protective state as “Allow All Snippet Content” being off. Change it on the plugin’s settings page, not in the agent’s configuration file, which the plugin rewrites from those settings. See Device-Side Protection for the rest of what it covers, including the grandfathering behavior for upgraded devices.
Deleting a server is never gated — removal is always allowed, regardless of the setting.
One narrow exception. NetDefense maintains a reserved-name exclusion on every server it manages, keeping directory accounts named like root or a NetDefense service account out of the effective query, and that exclusion is allowed to grow — but never to widen anything else in the same query — even while Allow All Snippet Content is off (reject_dangerous_snippets = true). It can only ever make a managed server more restrictive, never less, which is why it’s exempt from the gate that otherwise blocks every other server write in that state.
See Prerequisites. Below the OPNsense floor, every write to an authentication server or a login order is refused; deletes are always allowed. Re-test logins after a major OPNsense upgrade.
OPNsense validates SSL - Encrypted and StartTLS connections against the device’s own trust store, not a field you set per server. If your directory’s certificate isn’t signed by something already in that trust store — your own internal CA, most commonly — the CA has to be there first, or the connection fails validation regardless of how correctly everything else is configured. Sync it to every device with a TRUST_CA snippet (agent 1.21.0 or later), or import it by hand in System > Trust > Authorities. Sync the root CA: the system trust store leaves intermediate CAs out unless OPNsense’s Store intermediate setting (System > Trust > Settings) is on.
ldap_bindpw’s resolved value — the actual directory password — reaches the device the same way the rest of a resolved configuration does, and lands in the same places any other configuration value would: in plain text, inside the device’s config.xml and inside its own local configuration backups (/conf/backup). This isn’t unique to authentication servers; it’s the same baseline every other credential NetDefense delivers to a device already has. It means a few things worth planning around:
Anyone with a way to read the device’s configuration — a config backup download, a root shell, a remote session at full access — can read the bind password in the clear.
NetDefense’s own configuration backups carry it too. A backup is an encrypted snapshot of the device’s full configuration uploaded to your organization’s S3 bucket, and config.xml (bind password included) is part of what gets snapshotted. Anyone who can decrypt one of those backups — anyone holding the org or device backup encryption key — can read the bind password from it the same way they could from the device directly.
Rotate the directory-side bind password whenever a device that used it is removed, decommissioned, or restored from an old backup — and if a NetDefense configuration backup or its encryption key is ever exposed. NetDefense has no way to invalidate a credential on the directory server itself, so a copy of the old password can keep working there long after NetDefense has stopped using it.
If a pair of OPNsense devices runs CARP/HA with configuration synchronization enabled for the “Authentication Servers” category, servers NetDefense writes on the primary are copied to the secondary — the login order is not part of that category and isn’t replicated this way. The copy includes NetDefense’s own marker, still naming the primary device — but the secondary’s own agent doesn’t recognize a marker naming a different device as its own, so it treats the copied server as made by hand and never manages or removes it. If the secondary is itself managed by NetDefense and would receive a server with the same name, that create fails as NAME_COLLISION_UNMANAGED. Removing the primary device from NetDefense does not remove what HA already copied to its peer.
Leave “Authentication Servers” out of HA configuration synchronization on any pair you want NetDefense to fully manage, and configure each half separately through NetDefense instead.
A hand edit to an authentication server or the login order directly on the device — through OPNsense’s own pages — isn’t detected as drift, because NetDefense’s sync-hash comparison is based on what it intends to deliver, not a live diff against the device. But it isn’t left alone either: any sync that reaches the device — a scheduled one, one triggered by an unrelated change, or sync apply --force — recomputes the server and order content and writes it back over a hand edit, because NetDefense always evaluates what it should look like and compares that to what’s actually there, regardless of what triggered the sync.
On a device with “Allow All Snippet Content” off, that write-back is itself a gated mutation: it’s refused (AUTH_REJECTED_DANGEROUS) and the sync task fails, repeatedly, until the hand edit is reverted or the setting is turned on — --force does not bypass this gate.
If you edit an authentication page by hand and want to keep NetDefense from reverting it on the next sync, update the snippet or order policy to match — there’s no way to “adopt” a hand edit otherwise. Running ndcli sync apply --force is only useful for forcing an otherwise-unwilling schedule to re-push sooner; it doesn’t change what gets written.
A build-time failure fails the whole device build, not just the authentication content — no configuration of any kind reaches that device until it’s fixed. Most of these surface as AUTH_BUILD_INVALID, with one or more auth_issues entries naming the code, snippet, template, and (for group problems) server or facility:
Code
Meaning
AUTH_SERVER_NAME_CONFLICT
Two AUTH_SERVER snippets delivered to the same device have the same name, case-insensitively.
AUTH_ORDER_FACILITY_CONFLICT
Two AUTH_ORDER snippets delivered to the same device define the same facility.
SECRET_VARIABLE_REQUIRED
ldap_bindpw resolved to a variable that isn’t marked secret.
SECRET_VARIABLE_MISUSE
An AUTH-bound secret variable is referenced somewhere other than ldap_bindpw.
AUTH_CONTENT_INVALID
The resolved content failed validation after variable substitution (for example, the substituted value made ldap_bindpw too long).
Two more build-time failures aren’t auth_issues entries, since neither is about a specific AUTH_SERVER/AUTH_ORDER snippet having something wrong with it:
ldap_bindpw naming a variable that doesn’t exist yet fails the build the same way any other unresolved variable reference does — UNDEFINED_VARIABLES, naming the missing variable. This is what you’ll see if you sync a device after step 1 (create the snippet) but before finishing step 2 (create the variable) in Secret variables for the bind password.
AUTH_HASH_KEY_MISSING is its own top-level error code, not an auth_issues entry — it means an operational prerequisite the build needed wasn’t available on NetDefense’s side. It isn’t something authoring a snippet differently fixes; contact support if you see it.
A device-side (sync) failure fails only that sync, and only the parts of it that touch authentication — everything else in the same push (firewall rules, aliases, everything else) still applies:
A Limit or default group name on this server matches, case-insensitively, a live group on the device that NetDefense doesn’t manage as an external GROUP — OPNsense would link that other group instead. The server write is refused; rename or remove the ambiguous local group, or bring it under NetDefense as an external GROUP with the matching name.
LOCAL_DATABASE_SHADOWED
A server made by hand on the device is already named “Local Database”, so OPNsense would use it instead of the built-in local connector. The login-order write is refused until that hand-made server is renamed or removed.
Creating this server would activate a name already sitting in a live login order or another authentication consumer on the device (OpenVPN, IPsec, Captive Portal, web proxy); the create is refused and the blocking consumer is named. A webadmin order that this same sync also writes, or re-asserts unchanged, through an AUTH_ORDER policy never blocks the create — only a hand-edited order, or a non-facility consumer (OpenVPN/IPsec/Captive Portal/proxy), does. Remove the stale reference, or ship an AUTH_ORDER policy naming the server in the same sync, then retry.
ORDER_NAMES_LOCAL_SERVER
Informational only — the written order includes a server made by hand; see Hand-made servers for the risks it’s reported with.
USER_DEFERRED_EXCLUSION_STALE
A new privileged local identity (a user, or a member added to a group NetDefense manages) was deferred to the next sync because the reserved-name exclusion on the authentication servers couldn’t be confirmed current this pass — for example, the on-device helper didn’t run. It’s created on the sync after the exclusion catches up.
A few other faults can surface here that aren’t specific to your content at all — the on-device helper timing out while it waits for OPNsense’s own configuration lock, or the helper failing to run or respond in the first place. These are transient on-device faults — retry the sync, and contact support if one keeps recurring.
Agents older than ndagent 1.19.2 silently skip authentication content. They don’t recognize AUTH_SERVER/AUTH_ORDER at all, so a sync to one of these still reports IN_SYNC even though nothing authentication-related was ever applied — check the agent version if a server or order policy doesn’t seem to be taking effect.
If a login-order change leaves WebGUI or password SSH unreachable, in order of how likely each is to still work:
Key-based SSH. Unaffected by the login order entirely — it authenticates independently of webadmin.
The physical or serial console, with a local account’s password. This works as long as the live order still leads with Local Database — every order NetDefense writes does (see Local Database comes first), but a hand-edited order might not. If the console refuses a known-good local password, the order in front of you is a hand edit without Local Database first — skip to the remote session or the reset below.
A NetDefense remote session, if the device’s Remote Access Policy is “Full access” and your account is Read-Write or Superuser. A Read-Only session is clamped to deny configuration writes and can’t fix this from inside NetDefense.
Push a reset. If none of the above are available and every server in the order is genuinely unreachable — this scenario is only possible with a hand-edited order, since NetDefense never writes an order containing a name it couldn’t resolve — push an AUTH_ORDER snippet with "order": ["Local Database"] and sync. Writing the order is a gated mutation, so this only works if “Allow All Snippet Content” is (temporarily) turned on for that device first — see The consent switch.