diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 77a6c7ce46..b0752ef0ec 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -17,8 +17,6 @@ func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([ return svc.ds.ListPoliciesForHost(ctx, host) } -const refetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute - // TriggerMigrateMDMDevice triggers the webhook associated with the MDM // migration to Fleet configuration. It is located in the ee package instead of // the server/webhooks one because it is a Fleet Premium only feature and for @@ -88,7 +86,7 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos // if the webhook was successfully triggered, we update the host to // constantly run the query to check if it has been unenrolled from its // existing third-party MDM. - refetchUntil := svc.clock.Now().Add(refetchMDMUnenrollCriticalQueryDuration) + refetchUntil := svc.clock.Now().Add(fleet.RefetchMDMUnenrollCriticalQueryDuration) host.RefetchCriticalQueriesUntil = &refetchUntil if err := svc.ds.UpdateHostRefetchCriticalQueriesUntil(ctx, host.ID, &refetchUntil); err != nil { return ctxerr.Wrap(ctx, err, "save host with refetch critical queries timestamp") diff --git a/orbit/changes/22898-support-windows-mdm-migration b/orbit/changes/22898-support-windows-mdm-migration new file mode 100644 index 0000000000..e4e0401d2b --- /dev/null +++ b/orbit/changes/22898-support-windows-mdm-migration @@ -0,0 +1 @@ +* Added support to migrate the MDM provider of Windows devices to Fleet. diff --git a/orbit/pkg/update/execwinapi_windows.go b/orbit/pkg/update/execwinapi_windows.go index 3c0988a0fc..d07e50192f 100644 --- a/orbit/pkg/update/execwinapi_windows.go +++ b/orbit/pkg/update/execwinapi_windows.go @@ -175,7 +175,8 @@ func generateWindowsMDMAccessTokenPayload(args WindowsMDMEnrollmentArgs) ([]byte return json.Marshal(pld) } -// IsRunningOnWindowsServer determines if the process is running on a Windows server. Exported so it can be used across packages. +// IsRunningOnWindowsServer determines if the process is running on a Windows +// server. Exported so it can be used across packages. func IsRunningOnWindowsServer() (bool, error) { installType, err := readInstallationType() if err != nil { diff --git a/orbit/pkg/update/notifications.go b/orbit/pkg/update/notifications.go index cd5f256458..8e1b1003a6 100644 --- a/orbit/pkg/update/notifications.go +++ b/orbit/pkg/update/notifications.go @@ -165,14 +165,22 @@ func ApplyWindowsMDMEnrollmentFetcherMiddleware( var errIsWindowsServer = errors.New("device is a Windows Server") -// GetConfig calls the wrapped Fetcher's GetConfig method, and if the fleet -// server set the "needs windows enrollment" flag to true, executes the command -// to enroll into Windows MDM (or not, if the device is a Windows Server). +// Run checks if the fleet server set the "needs windows {un}enrollment" flag +// to true, and executes the command to {un}enroll into Windows MDM (or not, if +// the device is a Windows Server). It also unenrolls the device if the flag +// "needs MDM migration" is set to true, so that the device can then be +// enrolled in Fleet MDM. func (w *windowsMDMEnrollmentConfigReceiver) Run(cfg *fleet.OrbitConfig) error { - if cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment { + switch { + case cfg.Notifications.NeedsProgrammaticWindowsMDMEnrollment: w.attemptEnrollment(cfg.Notifications) - } else if cfg.Notifications.NeedsProgrammaticWindowsMDMUnenrollment { - w.attemptUnenrollment() + case cfg.Notifications.NeedsProgrammaticWindowsMDMUnenrollment, + cfg.Notifications.NeedsMDMMigration: + label := "unenroll" + if cfg.Notifications.NeedsMDMMigration { + label = "migrate" + } + w.attemptUnenrollment(label) } return nil } @@ -227,18 +235,18 @@ func (w *windowsMDMEnrollmentConfigReceiver) attemptEnrollment(notifs fleet.Orbi } } -func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment() { +func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment(actionLabel string) { if w.mu.TryLock() { defer w.mu.Unlock() // do not unenroll Windows Servers, and do not attempt unenrollment if the // last run is not at least Frequency ago. if w.isWindowsServer { - log.Debug().Msg("skipped calling UnregisterDeviceWithManagement to unenroll Windows device, device is a server") + log.Debug().Msgf("skipped calling UnregisterDeviceWithManagement to %s Windows device, device is a server", actionLabel) return } if time.Since(w.lastUnenrollRun) <= w.Frequency { - log.Debug().Msg("skipped calling UnregisterDeviceWithManagement to unenroll Windows device, last run was too recent") + log.Debug().Msgf("skipped calling UnregisterDeviceWithManagement to %s Windows device, last run was too recent", actionLabel) return } @@ -252,15 +260,15 @@ func (w *windowsMDMEnrollmentConfigReceiver) attemptUnenrollment() { if err := fn(args); err != nil { if errors.Is(err, errIsWindowsServer) { w.isWindowsServer = true - log.Info().Msg("device is a Windows Server, skipping unenrollment") + log.Info().Msgf("device is a Windows Server, skipping %s", actionLabel) } else { - log.Info().Err(err).Msg("calling UnregisterDeviceWithManagement to unenroll Windows device failed") + log.Info().Err(err).Msgf("calling UnregisterDeviceWithManagement to %s Windows device failed", actionLabel) } return } w.lastUnenrollRun = time.Now() - log.Info().Msg("successfully called UnregisterDeviceWithManagement to unenroll Windows device") + log.Info().Msgf("successfully called UnregisterDeviceWithManagement to %s Windows device", actionLabel) } } diff --git a/orbit/pkg/update/notifications_test.go b/orbit/pkg/update/notifications_test.go index 1455d8fc7e..a16b9dfbf1 100644 --- a/orbit/pkg/update/notifications_test.go +++ b/orbit/pkg/update/notifications_test.go @@ -191,21 +191,27 @@ func TestWindowsMDMEnrollment(t *testing.T) { desc string enrollFlag *bool unenrollFlag *bool + migrateFlag *bool discoveryURL string apiErr error wantAPICalled bool wantLog string }{ - {"enroll=false", ptr.Bool(false), nil, "", nil, false, ""}, - {"enroll=true,discovery=''", ptr.Bool(true), nil, "", nil, false, "discovery endpoint is empty"}, - {"enroll=true,discovery!='',success", ptr.Bool(true), nil, "http://example.com", nil, true, "successfully called RegisterDeviceWithManagement"}, - {"enroll=true,discovery!='',fail", ptr.Bool(true), nil, "http://example.com", io.ErrUnexpectedEOF, true, "enroll Windows device failed"}, - {"enroll=true,discovery!='',server", ptr.Bool(true), nil, "http://example.com", errIsWindowsServer, true, "device is a Windows Server, skipping enrollment"}, + {"enroll=false", ptr.Bool(false), nil, nil, "", nil, false, ""}, + {"enroll=true,discovery=''", ptr.Bool(true), nil, nil, "", nil, false, "discovery endpoint is empty"}, + {"enroll=true,discovery!='',success", ptr.Bool(true), nil, nil, "http://example.com", nil, true, "successfully called RegisterDeviceWithManagement"}, + {"enroll=true,discovery!='',fail", ptr.Bool(true), nil, nil, "http://example.com", io.ErrUnexpectedEOF, true, "enroll Windows device failed"}, + {"enroll=true,discovery!='',server", ptr.Bool(true), nil, nil, "http://example.com", errIsWindowsServer, true, "device is a Windows Server, skipping enrollment"}, - {"unenroll=false", nil, ptr.Bool(false), "", nil, false, ""}, - {"unenroll=true,success", nil, ptr.Bool(true), "", nil, true, "successfully called UnregisterDeviceWithManagement"}, - {"unenroll=true,fail", nil, ptr.Bool(true), "", io.ErrUnexpectedEOF, true, "unenroll Windows device failed"}, - {"unenroll=true,server", nil, ptr.Bool(true), "", errIsWindowsServer, true, "device is a Windows Server, skipping unenrollment"}, + {"unenroll=false", nil, ptr.Bool(false), nil, "", nil, false, ""}, + {"unenroll=true,success", nil, ptr.Bool(true), nil, "", nil, true, "successfully called UnregisterDeviceWithManagement to unenroll"}, + {"unenroll=true,fail", nil, ptr.Bool(true), nil, "", io.ErrUnexpectedEOF, true, "unenroll Windows device failed"}, + {"unenroll=true,server", nil, ptr.Bool(true), nil, "", errIsWindowsServer, true, "device is a Windows Server, skipping unenroll"}, + + {"migrate=false", nil, nil, ptr.Bool(false), "", nil, false, ""}, + {"migrate=true,success", nil, nil, ptr.Bool(true), "", nil, true, "successfully called UnregisterDeviceWithManagement to migrate"}, + {"migrate=true,fail", nil, nil, ptr.Bool(true), "", io.ErrUnexpectedEOF, true, "migrate Windows device failed"}, + {"migrate=true,server", nil, nil, ptr.Bool(true), "", errIsWindowsServer, true, "device is a Windows Server, skipping migrate"}, } for _, c := range cases { @@ -215,12 +221,14 @@ func TestWindowsMDMEnrollment(t *testing.T) { var ( enroll = c.enrollFlag != nil && *c.enrollFlag unenroll = c.unenrollFlag != nil && *c.unenrollFlag + migrate = c.migrateFlag != nil && *c.migrateFlag isUnenroll = c.unenrollFlag != nil ) testConfig := &fleet.OrbitConfig{Notifications: fleet.OrbitConfigNotifications{ NeedsProgrammaticWindowsMDMEnrollment: enroll, NeedsProgrammaticWindowsMDMUnenrollment: unenroll, + NeedsMDMMigration: migrate, WindowsMDMDiscoveryEndpoint: c.discoveryURL, }} @@ -241,7 +249,7 @@ func TestWindowsMDMEnrollment(t *testing.T) { err := enrollReceiver.Run(testConfig) require.NoError(t, err) // the dummy receiver never returns an error - if isUnenroll { + if isUnenroll || migrate { require.Equal(t, c.wantAPICalled, unenrollGotCalled) require.False(t, enrollGotCalled) } else { diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index 95ce9ee268..d55e4ff3ce 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -344,7 +344,7 @@ type Host struct { // is that the latter is a one-time request, while this one is a persistent // until the timestamp expires. The initial use-case is to check for a host // to be unenrolled from its old MDM solution, in the "migrate to Fleet MDM" - // workflow. + // workflow (both Apple and Windows). // // In the future, if we want to use it for more than one use-case, we could // add a "reason" field with well-known labels so we know what condition(s) diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 4d3e09f68e..b441067398 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -18,6 +18,11 @@ const ( MDMAppleDeclarationUUIDPrefix = "d" MDMAppleProfileUUIDPrefix = "a" MDMWindowsProfileUUIDPrefix = "w" + + // RefetchMDMUnenrollCriticalQueryDuration is the duration to set the + // RefetchCriticalQueriesUntil field when migrating a device from a + // third-party MDM solution to Fleet. + RefetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute ) type AppleMDM struct { diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index 6c06a963e2..357af033a6 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -8,7 +8,12 @@ import "encoding/json" type OrbitConfigNotifications struct { RenewEnrollmentProfile bool `json:"renew_enrollment_profile,omitempty"` RotateDiskEncryptionKey bool `json:"rotate_disk_encryption_key,omitempty"` - NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"` + + // NeedsMDMMigration is set to true if MDM is enabled for the host's + // platform, MDM migration is enabled for that platform, and the host is + // eligible for such a migration (e.g. it is enrolled in a third-party MDM + // solution). + NeedsMDMMigration bool `json:"needs_mdm_migration,omitempty"` // NeedsProgrammaticWindowsMDMEnrollment is sent as true if Windows MDM is // enabled and the device should be enrolled as far as the server knows (e.g. diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 14b26700d0..4a4230cd31 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -5926,7 +5926,6 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { err = s.ds.SaveAppConfig(context.Background(), appConf) require.NoError(s.T(), err) - // the feature flag is enabled for the MDM test suite var acResp appConfigResponse s.DoJSON("GET", "/api/latest/fleet/config", nil, http.StatusOK, &acResp) assert.False(t, acResp.MDM.WindowsEnabledAndConfigured) @@ -5937,47 +5936,66 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { tm2, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name() + "2"}) require.NoError(t, err) - // create some hosts - a Windows workstation in each team and no-team, - // Windows server in no team, Windows workstation enrolled in a 3rd-party in - // team 2, Windows workstation already enrolled in Fleet in no team, and a - // macOS host in no team. - metadataHosts := []struct { - os string - suffix string - isServer bool - teamID *uint - enrolledName string - shouldEnroll bool - }{ - {"windows", "win-no-team", false, nil, "", true}, - {"windows", "win-team-1", false, &tm1.ID, "", true}, - {"windows", "win-team-2", false, &tm2.ID, "", true}, - {"windows", "win-server", true, nil, "", false}, // is a server - {"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false}, // is enrolled in 3rd-party - {"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false}, // is already Fleet-enrolled - {"darwin", "macos-no-team", false, nil, "", false}, // is not Windows - } - hostsBySuffix := make(map[string]*fleet.Host, len(metadataHosts)) - for _, meta := range metadataHosts { - h := createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds) - createDeviceTokenForHost(t, s.ds, h.ID, meta.suffix) - err := s.ds.SetOrUpdateMDMData(ctx, h.ID, meta.isServer, meta.enrolledName != "", "https://example.com", false, meta.enrolledName, "") - require.NoError(t, err) - if meta.teamID != nil { - err = s.ds.AddHostsToTeam(ctx, meta.teamID, []uint{h.ID}) - require.NoError(t, err) - } - hostsBySuffix[meta.suffix] = h - } - // enable Windows MDM acResp = appConfigResponse{} s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ "mdm": { "windows_enabled_and_configured": true } }`), http.StatusOK, &acResp) assert.True(t, acResp.MDM.WindowsEnabledAndConfigured) + assert.False(t, acResp.MDM.WindowsMigrationEnabled) s.lastActivityOfTypeMatches(fleet.ActivityTypeEnabledWindowsMDM{}.ActivityName(), `{}`, 0) + // create some hosts - a Windows workstation in each team and no-team, + // Windows server in no team, Windows workstation enrolled in a 3rd-party in + // team 2, Windows workstation already enrolled in Fleet in no team, and a + // macOS host in no team. + metadataHosts := []struct { + os string + suffix string + isServer bool + teamID *uint + enrolledName string + shouldEnroll bool + shouldMigrate bool + }{ + {"windows", "win-no-team", false, nil, "", true, false}, + {"windows", "win-team-1", false, &tm1.ID, "", true, false}, + {"windows", "win-team-2", false, &tm2.ID, "", true, false}, + {"windows", "win-server", true, nil, "", false, false}, // is a server + {"windows", "win-third-party", false, &tm2.ID, fleet.WellKnownMDMSimpleMDM, false, true}, // is enrolled in 3rd-party + {"windows", "win-fleet", false, nil, fleet.WellKnownMDMFleet, false, false}, // is already Fleet-enrolled + {"darwin", "macos-no-team", false, nil, "", false, false}, // is not Windows + {"windows", "win-server-third-party", true, nil, fleet.WellKnownMDMSimpleMDM, false, false}, // is enrolled in 3rd-party, but is a server + } + hostsBySuffix := make(map[string]*fleet.Host, len(metadataHosts)) + for _, meta := range metadataHosts { + var host *fleet.Host + if meta.os == "windows" && meta.enrolledName == fleet.WellKnownMDMFleet { + // special-case to create a properly MDM-enrolled into Fleet host + host = createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds) + mdmDevice := mdmtest.NewTestMDMClientWindowsProgramatic(s.server.URL, *host.OrbitNodeKey) + err := mdmDevice.Enroll() + require.NoError(t, err) + err = s.ds.UpdateMDMWindowsEnrollmentsHostUUID(ctx, host.UUID, mdmDevice.DeviceID) + require.NoError(t, err) + err = s.ds.SetOrUpdateMDMData(ctx, host.ID, meta.isServer, true, s.server.URL, false, fleet.WellKnownMDMFleet, "") + require.NoError(t, err) + } else { + host = createOrbitEnrolledHost(t, meta.os, meta.suffix, s.ds) + createDeviceTokenForHost(t, s.ds, host.ID, meta.suffix) + + serverURL := "https://example.com" + err := s.ds.SetOrUpdateMDMData(ctx, host.ID, meta.isServer, meta.enrolledName != "", serverURL, false, meta.enrolledName, "") + require.NoError(t, err) + } + + if meta.teamID != nil { + err = s.ds.AddHostsToTeam(ctx, meta.teamID, []uint{host.ID}) + require.NoError(t, err) + } + hostsBySuffix[meta.suffix] = host + } + // get the orbit config for each host, verify that only the expected ones // receive the "needs enrollment to Windows MDM" notification. for _, meta := range metadataHosts { @@ -5987,6 +6005,7 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { http.StatusOK, &resp) require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + require.False(t, resp.Notifications.NeedsMDMMigration) if meta.shouldEnroll { require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath) } else { @@ -5994,7 +6013,34 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { } } - // turn on MDM for a host + // enable Windows MDM migration + acResp = appConfigResponse{} + s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{ + "mdm": { "windows_migration_enabled": true } + }`), http.StatusOK, &acResp) + assert.True(t, acResp.MDM.WindowsEnabledAndConfigured) + assert.True(t, acResp.MDM.WindowsMigrationEnabled) + s.lastActivityMatches(fleet.ActivityTypeEnabledWindowsMDMMigration{}.ActivityName(), `{}`, 0) + + // get the orbit config for each host, verify that only the expected ones + // receive the "needs enrollment to Windows MDM" and "needs migration" notifications. + // They still get enrollment notifications as we have not proceeded with enrollment. + for _, meta := range metadataHosts { + var resp orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", + json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)), + http.StatusOK, &resp) + require.Equal(t, meta.shouldEnroll, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.Equal(t, meta.shouldMigrate, resp.Notifications.NeedsMDMMigration) + require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + if meta.shouldEnroll { + require.Contains(t, resp.Notifications.WindowsMDMDiscoveryEndpoint, microsoft_mdm.MDE2DiscoveryPath) + } else { + require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) + } + } + + // turn on MDM for another host orbitHost, _ := createWindowsHostThenEnrollMDM(s.ds, s.server.URL, t) // disable Microsoft MDM @@ -6002,9 +6048,10 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { "mdm": { "windows_enabled_and_configured": false } }`), http.StatusOK, &acResp) assert.False(t, acResp.MDM.WindowsEnabledAndConfigured) + assert.False(t, acResp.MDM.WindowsMigrationEnabled) s.lastActivityOfTypeMatches(fleet.ActivityTypeDisabledWindowsMDM{}.ActivityName(), `{}`, 0) - // get the orbit config for win-no-team should return true for the + // get the orbit config for that MDM-enrolled host returns true for the // unenrollment notification var resp orbitGetConfigResponse s.DoJSON("POST", "/api/fleet/orbit/config", @@ -6012,7 +6059,25 @@ func (s *integrationMDMTestSuite) TestAppConfigWindowsMDM() { http.StatusOK, &resp) require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.False(t, resp.Notifications.NeedsMDMMigration) require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) + + // get the orbit config for each host, only the fleet-enrolled ones get the unenrollment, + // and none get enrollment/migration (because MDM is now off). + for _, meta := range metadataHosts { + var resp orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", + json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *hostsBySuffix[meta.suffix].OrbitNodeKey)), + http.StatusOK, &resp) + require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMEnrollment) + require.False(t, resp.Notifications.NeedsMDMMigration) + if meta.enrolledName == fleet.WellKnownMDMFleet { + require.True(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + } else { + require.False(t, resp.Notifications.NeedsProgrammaticWindowsMDMUnenrollment) + } + require.Empty(t, resp.Notifications.WindowsMDMDiscoveryEndpoint) + } } func (s *integrationMDMTestSuite) TestOrbitConfigNudgeSettings() { diff --git a/server/service/microsoft_mdm.go b/server/service/microsoft_mdm.go index 78fde975ff..4d79d45ce8 100644 --- a/server/service/microsoft_mdm.go +++ b/server/service/microsoft_mdm.go @@ -615,14 +615,22 @@ func NewCertStoreProvisioningData(enrollmentType string, identityFingerprint str return certStore } -// IsEligibleForWindowsMDMEnrollment returns true if the host can be enrolled +// isEligibleForWindowsMDMEnrollment returns true if the host can be enrolled // in Fleet's Windows MDM (if it was enabled). -func IsEligibleForWindowsMDMEnrollment(host *fleet.Host, mdmInfo *fleet.HostMDM) bool { +func isEligibleForWindowsMDMEnrollment(host *fleet.Host, mdmInfo *fleet.HostMDM) bool { return host.FleetPlatform() == "windows" && host.IsOsqueryEnrolled() && (mdmInfo == nil || (!mdmInfo.IsServer && !mdmInfo.Enrolled)) } +// isEligibleForWindowsMDMMigration returns true if the host can be migrated to +// Fleet's Windows MDM (if it was enabled). +func isEligibleForWindowsMDMMigration(host *fleet.Host, mdmInfo *fleet.HostMDM) bool { + return host.FleetPlatform() == "windows" && + host.IsOsqueryEnrolled() && + (mdmInfo != nil && !mdmInfo.IsServer && mdmInfo.Enrolled && mdmInfo.Name != fleet.WellKnownMDMFleet) +} + // NewApplicationProvisioningData returns a new ApplicationProvisioningData Characteristic // The Application Provisioning configuration is used for bootstrapping a device with an OMA DM account // The paramenters here maps to the W7 application CSP @@ -976,7 +984,7 @@ func (svc *Service) authBinarySecurityToken(ctx context.Context, authToken *flee } // This ensures that only hosts that are eligible for Windows enrollment can be enrolled - if !IsEligibleForWindowsMDMEnrollment(host, mdmInfo) { + if !isEligibleForWindowsMDMEnrollment(host, mdmInfo) { return "", "", errors.New("host is not elegible for Windows MDM enrollment") } diff --git a/server/service/orbit.go b/server/service/orbit.go index df75ce21c5..1be699ad47 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -268,13 +268,26 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro // set the host's orbit notifications for Windows MDM if appConfig.MDM.WindowsEnabledAndConfigured { - if IsEligibleForWindowsMDMEnrollment(host, mdmInfo) { + if isEligibleForWindowsMDMEnrollment(host, mdmInfo) { discoURL, err := microsoft_mdm.ResolveWindowsMDMDiscovery(appConfig.ServerSettings.ServerURL) if err != nil { return fleet.OrbitConfig{}, err } notifs.WindowsMDMDiscoveryEndpoint = discoURL notifs.NeedsProgrammaticWindowsMDMEnrollment = true + } else if appConfig.MDM.WindowsMigrationEnabled && isEligibleForWindowsMDMMigration(host, mdmInfo) { + notifs.NeedsMDMMigration = true + + // Set the host to refetch the "critical queries" quickly for some time, + // to improve ingestion time of the unenroll and make the host eligible to + // enroll into Fleet faster. + if host.RefetchCriticalQueriesUntil == nil { + refetchUntil := svc.clock.Now().Add(fleet.RefetchMDMUnenrollCriticalQueryDuration) + host.RefetchCriticalQueriesUntil = &refetchUntil + if err := svc.ds.UpdateHostRefetchCriticalQueriesUntil(ctx, host.ID, &refetchUntil); err != nil { + return fleet.OrbitConfig{}, err + } + } } } if !appConfig.MDM.WindowsEnabledAndConfigured { diff --git a/server/service/osquery.go b/server/service/osquery.go index 21555df372..ce88aa6250 100644 --- a/server/service/osquery.go +++ b/server/service/osquery.go @@ -691,7 +691,8 @@ const alwaysTrueQuery = "SELECT 1" // list of detail queries that are returned when only the critical queries // should be returned (due to RefetchCriticalQueriesUntil timestamp being set). var criticalDetailQueries = map[string]bool{ - "mdm": true, + "mdm": true, + "mdm_windows": true, } // detailQueriesForHost returns the map of detail+additional queries that should be executed by diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go index 5be2974a12..d0f37e1e90 100644 --- a/server/service/osquery_test.go +++ b/server/service/osquery_test.go @@ -179,7 +179,7 @@ func TestGetClientConfig(t *testing.T) { // Check scheduled queries are loaded properly conf, err = svc.GetClientConfig(ctx3) require.NoError(t, err) - assert.JSONEq(t, `{ + assert.JSONEq(t, `{ "pack_by_label": { "queries":{ "time":{"query":"select * from time","interval":30,"removed":false} @@ -208,7 +208,7 @@ func TestGetClientConfig(t *testing.T) { "version": "" } } - } + } }`, string(conf["packs"].(json.RawMessage)), ) @@ -1165,8 +1165,12 @@ func TestHostDetailQueries(t *testing.T) { host.RefetchCriticalQueriesUntil = ptr.Time(mockClock.Now().Add(1 * time.Minute)) queries, discovery, err = svc.detailQueriesForHost(ctx, &host) require.NoError(t, err) - require.Equal(t, len(criticalDetailQueries), len(queries), distQueriesMapKeys(queries)) + // host is darwin so it gets only the darwin critical query + require.Equal(t, 1, len(queries), distQueriesMapKeys(queries)) for name := range criticalDetailQueries { + if strings.HasSuffix(name, "_windows") { + continue + } assert.Contains(t, queries, hostDetailQueryPrefix+name) } verifyDiscovery(t, queries, discovery) diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go index da62dfef17..8f7600c16c 100644 --- a/server/service/osquery_utils/queries.go +++ b/server/service/osquery_utils/queries.go @@ -1885,6 +1885,11 @@ func directIngestMDMWindows(ctx context.Context, logger log.Logger, host *fleet. return nil } + if host.RefetchCriticalQueriesUntil != nil { + level.Debug(logger).Log("msg", "ingesting Windows mdm data during refetch critical queries window", "host_id", host.ID, + "data", fmt.Sprintf("%+v", rows)) + } + data := rows[0] var enrolled bool var automatic bool @@ -1900,13 +1905,20 @@ func directIngestMDMWindows(ctx context.Context, logger log.Logger, host *fleet. } isServer := strings.Contains(strings.ToLower(data["installation_type"]), "server") + mdmSolutionName := deduceMDMNameWindows(data) + if !enrolled && mdmSolutionName != fleet.WellKnownMDMFleet && host.RefetchCriticalQueriesUntil != nil { + // the host was unenrolled from a non-Fleet MDM solution, and the refetch + // critical queries timestamp was set, so clear it. + host.RefetchCriticalQueriesUntil = nil + } + return ds.SetOrUpdateMDMData(ctx, host.ID, isServer, enrolled, serverURL, automatic, - deduceMDMNameWindows(data), + mdmSolutionName, "", ) } diff --git a/tools/mdm/windows/poc-mdm-server/README.md b/tools/mdm/windows/poc-mdm-server/README.md index 74760b2de2..eebc49061a 100644 --- a/tools/mdm/windows/poc-mdm-server/README.md +++ b/tools/mdm/windows/poc-mdm-server/README.md @@ -23,6 +23,17 @@ This code is MIT licensed and it was forked from [here](https://github.com/oscar ## Usage On the server side, you just need to run the project using the already provided cert and keys. The certificate is in `.pfx` file format, so you need to extract the certificate and key first, see https://stackoverflow.com/a/59120388/1094941. +The "Import password" is "testpassword", and the names of the output files matter, on Linux something like this works (assuming you are in the certs/ directory): + +``` +# for the cert +$ openssl pkcs12 -in dev_cert_mdmwindows_com.pfx -clcerts -nokeys -out dev_cert_mdmwindows_com_cert.pem + +# for the key +$ openssl pkcs12 -in dev_cert_mdmwindows_com.pfx -out dev_cert_mdmwindows_com.key -nocerts -nodes +``` + +Note that an asn1 error might occur when running the server, if that's the case you need to patch your local Go toolchain by running `$ go run ./patch/patch.go` (`GOROOT` env var must be set to point to your `go env GOROOT` directory). It may require `sudo` depending on where your `go` installation is (due to https://github.com/golang/go/issues/14017). Next go to the project folder and run. @@ -30,7 +41,9 @@ Next go to the project folder and run. go run . ``` -On the Windows client side, you need to import a custom CA certificate to the certificate store, and populate the `hosts` file before running the Windows Enrollment. The certificate to import is on the certs directory and it is called `dev_cert_mdmwindows_com.pfx`. You need to copy this certificate to the client machine and run the powershell command below. This is required because the project uses a local dev https endpoint. +Note that the server binds to the standard and usually firewall-protected `443` port, so you may need to configure your firewall to allow connections to it for the duration of your test. + +On the Windows client side, you need to import the custom CA certificate to the certificate store, and populate the `hosts` file before running the Windows Enrollment. The certificate to import is on the certs directory and it is called `dev_cert_mdmwindows_com.pfx`. You need to copy this certificate to the client machine and run the powershell command below (in the console, not in a powershell terminal). This is required because the project uses a local dev https endpoint. 1) Import certificate to Trusted CAs repository (be sure to update the path to the pfx certificate) @@ -42,6 +55,8 @@ On the Windows client side, you need to import a custom CA certificate to the ce echo autodiscovery.mdmwindows.com >> %SystemRoot%\System32\drivers\etc\hosts echo enterpriseenrollment.mdmwindows.com >> %SystemRoot%\System32\drivers\etc\hosts +To enroll the device into this MDM server, go to `Settings > Accounts > Access work or school` and click the connect button, enter the email provided to the server when you ran `go run .` (default: `demo@mdmwindows.com`) and it should automatically detect the server and proceed with enrollment. This is why the server must run on port `:443`, because it uses automatic discovery and will not attempt a custom port. + ## Protocol Details Below is the raw https exchange of the MS-MDE and MS-MDM protocols when run using the -verbose mode: