[management,client] Gate remote jobs behind an admin opt-in with MDM support (#7153)

This introduces a disabled-by-default allow-remote-jobs setting that
controls whether the management server may run jobs (such as debug
bundles) on a peer. The flag propagates end to end: through client
configuration, the daemon SetConfig and Login requests, authentication,
and system info, up to management, where it is stored on the peer and
exposed on the peers API as remote_jobs_allowed. The client refuses any
management-requested job unless the peer has opted in. Because enabling
remote jobs crosses the user-to-root boundary, turning it on requires
privilege, mirroring the SSH-server gate. Administrators can enforce the
setting through MDM policy on both macOS and Windows, and MDM can also
override the debug-bundle upload URL. The change ships policy
documentation and generated profile templates, and adds configuration,
conflict, and enforcement tests covering the opt-in, privilege, and MDM
paths.
This commit is contained in:
Maycon Santos
2026-09-01 17:53:41 +02:00
committed by GitHub
parent 3027130f0f
commit ebc259e30b
39 changed files with 1770 additions and 1025 deletions
+25 -18
View File
@@ -137,6 +137,7 @@ type EngineConfig struct {
RosenpassPermissive bool
ServerSSHAllowed bool
RemoteJobsAllowed bool
EnableSSHRoot *bool
EnableSSHSFTP *bool
EnableSSHLocalPortForwarding *bool
@@ -1259,6 +1260,7 @@ func (e *Engine) applyInfoFlags(info *system.Info) {
e.config.EnableSSHLocalPortForwarding,
e.config.EnableSSHRemotePortForwarding,
e.config.DisableSSHAuth,
&e.config.RemoteJobsAllowed,
)
}
@@ -1344,6 +1346,13 @@ func (e *Engine) receiveJobEvents() {
ID: msg.ID,
Status: mgmProto.JobStatus_failed,
}
// Remote jobs are an explicit opt-in. When not enabled on this
// peer, every job is refused before any work is done.
if !e.config.RemoteJobsAllowed {
log.Warnf("refusing remote job: remote jobs are not enabled on this peer (enable with --allow-remote-jobs)")
resp.Reason = []byte("remote jobs are not enabled on this peer")
return &resp
}
switch params := msg.WorkloadParameters.(type) {
case *mgmProto.JobRequest_Bundle:
bundleResult, err := e.handleBundle(params.Bundle)
@@ -1380,7 +1389,15 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
params.GetAnonymize(), params.GetAnonymizeLevel(), params.GetLogFileCount(), params.GetBundleFor(), params.GetBundleForTime())
log.Debugf("remote debug bundle request parameters: %s", params.String())
if err := validateBundleUploadURL(params.GetUploadUrl()); err != nil {
// Resolve the upload destination: an MDM override, when set, takes
// precedence over the management-supplied URL. Both are validated the same
// way; an empty result falls back to the default upload server downstream.
uploadURL := params.GetUploadUrl()
if override := e.config.ProfileConfig.DebugBundleUploadURL; override != "" {
log.Infof("using MDM debug bundle upload URL override instead of the management-supplied value")
uploadURL = override
}
if err := validateBundleUploadURL(uploadURL); err != nil {
return nil, err
}
@@ -1411,7 +1428,7 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
waitFor := time.Duration(params.BundleForTime) * time.Minute
uploadKey, err := e.jobExecutor.BundleJob(e.ctx, bundleDeps, bundleJobParams, waitFor, e.config.ProfileConfig.ManagementURL.String(), params.GetUploadUrl())
uploadKey, err := e.jobExecutor.BundleJob(e.ctx, bundleDeps, bundleJobParams, waitFor, e.config.ProfileConfig.ManagementURL.String(), uploadURL)
if err != nil {
return nil, err
}
@@ -1425,23 +1442,13 @@ func (e *Engine) handleBundle(params *mgmProto.BundleParameters) (*mgmProto.JobR
}
// validateBundleUploadURL sanity-checks a management-supplied upload URL for a
// remote debug bundle job. An empty value is accepted — the executor falls back
// to the default upload service. A non-empty value must be a well-formed https
// URL with a host; a malformed value or a plaintext scheme is rejected. This
// deliberately does not constrain which host may receive the bundle; that
// policy is left open pending a decision on management-directed uploads.
// remote debug bundle job. It delegates to profilemanager.ValidateBundleUploadURL
// so the executor and the MDM policy override share one definition of the rule
// (empty accepted; otherwise a well-formed https URL with a host) and cannot
// drift. The host is deliberately left unconstrained pending a decision on
// management-directed uploads.
func validateBundleUploadURL(raw string) error {
if raw == "" {
return nil
}
parsed, err := url.Parse(raw)
if err != nil {
return fmt.Errorf("parse upload URL: %w", err)
}
if parsed.Scheme != "https" || parsed.Host == "" {
return fmt.Errorf("upload URL must be an https URL with a host")
}
return nil
return profilemanager.ValidateBundleUploadURL(raw)
}
// receiveManagementEvents connects to the Management Service event stream to receive updates from the management service