2.1
Group client traffic policies
The client traffic policy decides whether users may route all of their traffic through the VPN, are prevented from doing so, or are forced to do so. In previous Defguard versions this was a single instance-wide setting stored in the enterprise settings. Defguard 2.1 allows administrators to override it for selected groups, while every group without an explicit assignment keeps following the instance-wide policy.
Requirements
Administrators should be able to assign a traffic policy to selected groups, and a group should have at most one assigned policy.
A user may belong to several groups, so the effective policy has to be resolved from all applicable assignments.
Groups without an assignment must keep using the instance-wide policy.
The client configuration and enrollment flows must receive the effective policy for the user, not the raw settings.
The feature must be limited to installations with an active Business license, and installations without one must behave exactly as they did before.
Losing the license must disable the feature without destroying the assignments the administrator saved.
Existing clients that do not know about the new field must keep working.
Considered options
1. A dedicated assignment table, resolved in Core
Store one row per assigned group in a new table, with the group id as the primary key.
Return the assignments as part of the enterprise settings API.
Resolve the effective policy in Core, while building the client configuration.
Pros
The primary key alone guarantees that a group has at most one policy.
The core group model stays independent of an optional enterprise feature - a group without an assignment simply has no row.
Deleting a group removes its assignment through a foreign key cascade.
Resolution happens in one place, on the side that the client cannot influence.
Cons
Building the client configuration performs an additional lookup for the user's group policies.
The enterprise settings response grows, and the UI has to combine it with group metadata to show a policy per group.
2. A policy column in the group table
Add the policy directly to the group table.
Deal-breaking issue: this puts an optional enterprise field into the core group model, which every group row then carries, and it turns "no assignment" into a value in that column instead of the absence of a row.
3. Exposing the assignments through the group API
Return the assigned policy together with the rest of the group information from /group-info.
Deal-breaking issue: the enterprise settings response already carries the policy-to-group mapping, and /group-info is a widely used contract. Coupling it to enterprise policy storage would change a response that many callers depend on, for an unrelated reason.
4. Resolving the effective policy in the client
Send the assignments and the user's groups to the desktop client and let it work out which policy applies.
Deal-breaking issue: client behaviour has to be enforced by Core. This would move the decision to the side that the policy is meant to constrain, and make it depend on client-side state.
5. A dedicated endpoint for policy assignments
Manage the assignments through their own API instead of the enterprise settings API.
Deal-breaking issue: assignments are edited as part of the client settings form. Keeping them in the same request gives one transactional update and one audit entry for the whole form, which a separate endpoint would have to reproduce.
Decision
Option 1, the dedicated assignment table with resolution in Core, is selected.
Rationale
Assignments are optional, sparse and enterprise-only, which is exactly what a separate table expresses well: the absence of a row is the absence of a policy, the primary key enforces the "one policy per group" rule without any application logic, and the core group model does not change at all. The same argument decides the API surface. The mapping is already part of the enterprise settings, so putting it into the settings request keeps validation, persistence and audit for the whole form in one transaction, instead of spreading them across two endpoints that would have to agree with each other.
Resolving the effective policy in Core follows from what the policy is for. A traffic policy constrains what the client may do, so the client cannot be the component that decides which policy applies to it. Doing the resolution while the client configuration is built also means there is a single place where group membership, group assignments, the instance-wide setting and the license state come together, and every configuration fetch reflects the current state of all four.
Database model changes
The migration adds a group_client_traffic_policy table:
group_idis the primary key and referencesgroup(id)withON DELETE CASCADE.client_traffic_policyreuses the existing database enum, so a group assignment cannot express anything the instance-wide setting could not.
Existing installations start with no rows, so after the migration every group follows the instance-wide policy and behaviour is unchanged.
Policy resolution
Resolution takes the instance-wide policy and the policies of all groups the user belongs to, and applies them in a fixed order:
Disable all traffic wins over everything else.
Otherwise Force all traffic wins over the instance-wide policy.
Otherwise an explicit No limitation assignment wins over the instance-wide policy. This is what allows a group to opt out of a restrictive global setting.
A user with no applicable assignment gets the instance-wide policy.
Resolution itself is a pure function - loading the assignments and checking the license happen outside it, so the precedence rules can be tested on their own.
The effective policy is calculated while building the client configuration, which the client receives during enrollment and while polling. As a result, a change to the assignments or to a user's group membership takes effect on the next configuration fetch, rather than being pushed to a connected client. Clients that predate the policy field are still served the deprecated disable_all_traffic flag, which is now set from the resolved policy and is true only when that policy disables all traffic.
API changes
The assignments are part of the enterprise settings API:
GETreturns them regardless of license state, so a saved configuration is never hidden from the API.PATCHtakes them as an optional field. Including it replaces all assignments in one transaction, omitting it leaves them untouched. This distinction matters for partial updates and for license transitions.Before anything is replaced, Core rejects a group that appears under more than one policy, and a group that does not exist. If validation or persistence fails, the transaction rolls back and the previous assignments remain in place.
Enterprise settings audit events record the complete assignment state before and after the change, not only the instance-wide settings.
License behaviour
Group policies require an active Business license. Without one:
resolution ignores the assignments and the instance-wide policy falls back to the unlicensed default;
the settings page shows no assigned groups, locks the traffic policy to No limitation and disables the policy controls;
Edit groups opens the upgrade or expired-license modal instead of the group selection modal;
the UI stops sending the assignment field, so an unlicensed instance cannot overwrite stored assignments with empty arrays.
Nothing is deleted. When a valid license is restored, the stored assignments are read again and become effective without any reconfiguration. We decided against clearing them on expiry, because license state should disable a feature, not destroy configuration.
Linux service locations
Service locations are WireGuard connections managed in the background by the client daemon, used to reach services without a user-controlled tunnel. They have been available on Windows since the 1.6 desktop client, in the Pre-logon and Always-on modes. Defguard 2.1 introduces service locations on Linux.
Requirements
Always-on service locations must reconnect after the host or the daemon restarts, without user action and before a desktop session exists.
The connection must be managed by the privileged daemon, not by the desktop application or the CLI.
Service locations must not be exposed as ordinary user-controlled locations, and must not react to normal connect and disconnect actions.
The daemon must have everything it needs to bring a service location up - the configuration and the instance private key - before any user logs in.
Configuration changes have to reach the daemon, but unchanged configuration must not disturb working connections.
Failed interface setup must not leave a partially configured WireGuard interface behind.
Existing Windows behaviour must be preserved, and the shared service location manager should not diverge between platforms more than necessary.
Considered options
1. Reuse the tunnel manager used for ordinary locations
Treat a service location as another location in the client database and connect it through the same code path as a user location.
Deal-breaking issue: ordinary tunnels are owned by the desktop application and follow user actions. Service locations have to survive UI restarts, come up before anyone logs in, and stay out of the normal connect and disconnect controls. Reusing that path would tie background service access to desktop state.
2. Daemon-managed locations with client-side persistence
Keep the lifecycle in the daemon, but store the service location configuration in the client database, the way ordinary locations are stored.
Deal-breaking issue: that database belongs to the desktop user and is not available to the daemon early enough. The daemon has to reconnect Always-on locations at startup, and it needs the instance private key to do so.
3. Daemon-managed locations with daemon-owned persistence
Keep the lifecycle in the daemon and give the daemon its own state directory, readable and writable only by root.
Use the existing daemon RPC operations for saving and deleting service location configuration on both Windows and Linux.
Keep platform-specific behaviour in the
ServiceLocationManagerimplementations.
Pros
Startup recovery does not depend on the desktop database or on a logged-in session.
The private key and the interface lifecycle stay on the privileged side of the boundary.
Windows and Linux share one RPC surface, so the client does not need platform-specific synchronization logic.
Cons
The daemon owns sensitive files and has to maintain their permissions itself.
Service location state exists both in the client database and in the daemon's directory, and the client is responsible for keeping them in agreement.
Decision
Option 3, daemon-managed locations with daemon-owned persistence, is selected. On Linux, only Always-on service locations are supported.
Rationale
One requirement decides the design: an Always-on location has to be up before anyone logs in. That rules out any state the daemon cannot read on its own, and it also decides where the instance private key lives, because whoever brings the interface up needs that key. Once the daemon owns the key and the interface, it may as well own the configuration that describes them, and the desktop application is left with the job it can actually do - noticing that the configuration changed and telling the daemon about it.
Pre-logon is a different matter. It is built on the Windows service lifecycle, and there is no Linux equivalent that behaves the same way. We preferred to leave the mode Windows-only and filter it out on Linux, rather than ship something that looks like Pre-logon but has different guarantees. Unsupported modes are therefore removed before the daemon persists anything or brings up an interface.
Platform and version gating
Because the feature depends on both the platform and the client version, Core advertises it through per-platform rules and filters service location configuration out of a device configuration response when the client cannot use it. Service locations are supported on Windows from client version 1.6.0, and on Linux from 2.1.0. Older Linux clients keep working unchanged, because they never receive the configuration in the first place.
Daemon-owned state
The daemon keeps one file per enrolled Defguard instance:
Each file holds the instance identifier, the instance WireGuard private key used by its service location interfaces, and the supported service location definitions. The directory is created with mode 0700 and the files with mode 0600, and private keys are kept out of debug output.
Saving writes the complete current set of Always-on locations for that instance. An empty set is stored as an empty list, while deleting an instance's service locations removes the file entirely. Every operation is scoped to a single instance, so a configuration change for one enrolled Defguard instance cannot disturb the service locations of another.
At startup the daemon loads every persisted file and connects the locations it finds. Already-connected locations are skipped, which makes the attempt idempotent and lets the daemon simply repeat it a bounded number of times when the network, WireGuard or DNS is not ready yet. The retry policy belongs to the daemon and is passed into the shared auto-connect task, rather than being fixed inside the service location manager.
Configuration synchronization
The client pushes service location state to the daemon only after a configuration change has actually been applied, not on every configuration poll. Saving reconciles the instance: removed locations are disconnected, and the remaining ones are reset by disconnecting the existing interface and reconnecting it with the new configuration. Calling that on every poll would repeatedly tear down healthy connections, so a poll that changes nothing calls nothing. All resets are attempted before an aggregate error is returned, so one failing location does not leave the remaining ones unreconciled. Enrollment and manual instance updates use the same path, so the daemon does not have to wait for the next poll to become correct.
WireGuard interface handling
Linux WireGuard interfaces are named dynamically as wgN, and the helper that produces a name returns the next free one. This makes the name useless as an identity for a service location: recomputing it on disconnect can return a different free name, leave the old interface in place, and create a duplicate interface on the next reconnect.
The daemon therefore identifies a managed interface by matching the service location peer public key against the peer data of the interfaces it holds. The key is already part of the service location contract, and an interface configured for a service location has precisely that location as its peer.
We considered two alternatives and chose neither. Deterministic interface names would remove the lookup, but Linux interface names have a strict length limit, so this would require a naming scheme, collision handling and a migration for existing installations. Storing the interface name in the manager's entry for a connected location would add another piece of platform-specific runtime state, while the peer public key is a more meaningful identity than an ephemeral interface name.
The lookup is deliberately limited to the interfaces tracked by the running daemon, and it returns one match. Several interfaces sharing one service location peer indicate an unhealthy state rather than normal operation, so the daemon returns the matching interface instead of treating the situation as a general cleanup task for interfaces left behind by other processes or by manual administration.
If configuring the interface, the routing or DNS fails after the interface has already been created, the daemon removes the interface before returning the error, so a failed attempt cannot leave an unusable interface that would interfere with later retries. A missing runtime interface during disconnect is treated as an idempotent cleanup condition and logged, rather than being an error.
Consequences
DNS configuration is part of interface setup, so a host whose resolver backend cannot accept the requested configuration fails the whole setup. On systemd-based distributions this means systemd-resolved has to be installed and active when the WireGuard library uses that backend.
Startup recovery is bounded. A location that is still unreachable after the last attempt stays down until the daemon restarts or another configuration change triggers reconciliation. Reconciliation itself resets active interfaces when their configuration changes, so a configuration update is not transparent to an established service location connection.
Directory synchronization scope
In previous Defguard versions, directory synchronization covered every user the configured application could see in the provider's directory. In a large directory this means creating and updating accounts that have no reason to exist in Defguard at all. A field restricting synchronization to selected groups first appeared for Microsoft Entra ID only. Defguard 2.1 makes it part of the common directory synchronization configuration, so it works the same way for Microsoft Entra ID, Google Workspace, Okta and JumpCloud.
An administrator lists one or more directory groups, and only their members are then considered. Defguard fetches the listed groups, resolves their members, and applies the resulting list both to regular state synchronization and to the import that creates accounts ahead of the first login. An empty list means no restriction, which keeps existing configurations behaving as they did.
Two properties of the filter are deliberate. A group that Defguard cannot see in the directory is skipped with a warning in the Core log instead of failing the whole synchronization run, because one renamed or misspelled group should not stop everybody else from being synchronized. And the filter decides who has a Defguard account, not what that account may do - group membership mapping and access rights are configured separately, so restricting the scope of synchronization is not a substitute for access control.
Device posture checks
This record is being prepared and will be published here.
SMTP with OAuth2
This record is being prepared and will be published here.
Disabling password management
This record is being prepared and will be published here.
Generating AllowedIPs from ACL rules
This record is being prepared and will be published here.
Last updated
Was this helpful?