[client] Gate remote jobs behind an admin opt-in with MDM support

Remote jobs (debug bundles requested by the management server) run on the
peer with no local consent. This makes them an explicit opt-in, mirroring
the SSH-server opt-in: an --allow-remote-jobs flag persisted in the client
config, defaulting off. Enabling it off->on crosses the user-to-root
boundary and is refused for unprivileged IPC callers by the daemon gate,
the same way enabling the SSH server is. When disabled, the job-stream
handler refuses every job before doing any work.

Because the flag is admin-controlled, it is also MDM-managed: the
allowRemoteJobs policy key can enable or lock it, and a user SetConfig that
diverges from an enforced value is rejected like the other managed fields.

A second MDM key, debugBundleUploadURL, overrides the debug-bundle upload
service for remote jobs, taking precedence over the management-supplied
value (MDM > management > default). This lets an operator pin uploads to a
trusted host regardless of what management requests. The override is
validated as an https URL with a host, the same as the management value.

Defaulting the opt-in off is a behavior change: existing deployments that
rely on management-triggered debug bundles must opt in (flag or MDM) before
they work again.
This commit is contained in:
mlsmaycon
2026-08-13 02:12:47 +09:00
committed by Maycon Santos
parent d32573020a
commit 7da4127c7d
15 changed files with 227 additions and 27 deletions
+24 -12
View File
@@ -39,27 +39,30 @@ import (
// user-to-root boundary. Fields are nil or empty when the request leaves them
// untouched.
type privilegedConfigChange struct {
managementURL string
serverSSHAllowed *bool
enableSSHRoot *bool
disableSSHAuth *bool
managementURL string
serverSSHAllowed *bool
remoteJobsAllowed *bool
enableSSHRoot *bool
disableSSHAuth *bool
}
func privilegedChangeFromSetConfig(msg *proto.SetConfigRequest) privilegedConfigChange {
return privilegedConfigChange{
managementURL: msg.GetManagementUrl(),
serverSSHAllowed: msg.ServerSSHAllowed,
enableSSHRoot: msg.EnableSSHRoot,
disableSSHAuth: msg.DisableSSHAuth,
managementURL: msg.GetManagementUrl(),
serverSSHAllowed: msg.ServerSSHAllowed,
remoteJobsAllowed: msg.RemoteJobsAllowed,
enableSSHRoot: msg.EnableSSHRoot,
disableSSHAuth: msg.DisableSSHAuth,
}
}
func privilegedChangeFromLogin(msg *proto.LoginRequest) privilegedConfigChange {
return privilegedConfigChange{
managementURL: msg.GetManagementUrl(),
serverSSHAllowed: msg.ServerSSHAllowed,
enableSSHRoot: msg.EnableSSHRoot,
disableSSHAuth: msg.DisableSSHAuth,
managementURL: msg.GetManagementUrl(),
serverSSHAllowed: msg.ServerSSHAllowed,
remoteJobsAllowed: msg.RemoteJobsAllowed,
enableSSHRoot: msg.EnableSSHRoot,
disableSSHAuth: msg.DisableSSHAuth,
}
}
@@ -83,6 +86,15 @@ func requirePrivilegeForConfigChange(ctx context.Context, stored *profilemanager
return denyPrivileged(ctx, "enabling the NetBird SSH server", ipcauth.UpCommand("--allow-server-ssh"))
}
// Enabling remote jobs lets the management server run jobs (e.g. debug
// bundles) on this host, so turning it on crosses the user-to-root
// boundary the same way enabling the SSH server does. The stored value
// defaults to off (nil = off), so a legacy config is correctly seen as
// off and turning it on requires privilege.
if enables(storedFlag(stored, func(c *profilemanager.Config) *bool { return c.RemoteJobsAllowed }), change.remoteJobsAllowed) {
return denyPrivileged(ctx, "enabling remote jobs", ipcauth.UpCommand("--allow-remote-jobs"))
}
// Only guard the management binding while the SSH server is enabled: that is
// when the management identity decides who may open a shell here.
if !sshServerEnabled(stored) {