Windows MDM migration: implement fleetd notification and migration (#24185)
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
* Added support to migrate the MDM provider of Windows devices to Fleet.
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
+14
-1
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 <server_ip> autodiscovery.mdmwindows.com >> %SystemRoot%\System32\drivers\etc\hosts
|
||||
echo <server_ip> 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:
|
||||
|
||||
Reference in New Issue
Block a user