diff --git a/changes/9459-install-fleetd b/changes/9459-install-fleetd new file mode 100644 index 0000000000..5424d5091a --- /dev/null +++ b/changes/9459-install-fleetd @@ -0,0 +1 @@ +* MDM: automatically install `fleetd` for DEP enrolled hosts. diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go index b93819f4af..96070469f6 100644 --- a/cmd/fleet/serve.go +++ b/cmd/fleet/serve.go @@ -535,7 +535,8 @@ the way that the Fleet server works. nanoMDMLogger := service.NewNanoMDMLogger(kitlog.With(logger, "component", "apple-mdm-push")) pushProviderFactory := buford.NewPushProviderFactory() mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger) - mdmCheckinAndCommandService = service.NewMDMAppleCheckinAndCommandService(ds) + commander := apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPushService) + mdmCheckinAndCommandService = service.NewMDMAppleCheckinAndCommandService(ds, commander) appCfg.MDM.EnabledAndConfigured = true } diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go index d6ac99df0e..e16294fab3 100644 --- a/server/fleet/apple_mdm.go +++ b/server/fleet/apple_mdm.go @@ -18,6 +18,7 @@ type MDMAppleCommandIssuer interface { RemoveProfile(ctx context.Context, hostUUIDs []string, identifier string, uuid string) error DeviceLock(ctx context.Context, hostUUIDs []string, uuid string) error EraseDevice(ctx context.Context, hostUUIDs []string, uuid string) error + InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error } // MDMAppleEnrollmentType is the type for Apple MDM enrollments. diff --git a/server/mdm/apple/apple_mdm.go b/server/mdm/apple/apple_mdm.go index 6095ae9eab..cd9abaf736 100644 --- a/server/mdm/apple/apple_mdm.go +++ b/server/mdm/apple/apple_mdm.go @@ -47,6 +47,11 @@ const ( // FleetPayloadIdentifier is the value for the "PayloadIdentifier" // used by Fleet MDM on the enrollment profile. FleetPayloadIdentifier = "com.fleetdm.fleet.mdm.apple" + + // FleetdPublicManifestURL contains a valid manifest that can be used + // by InstallEnterpriseApplication to install `fleetd` in a host. + // TODO: update this URL with the value TBD in #10960 + FleetdPublicManifestURL = "https://download.fleetdm.com/fleetd-base.plist" ) func ResolveAppleMDMURL(serverURL string) (string, error) { diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go index dfb1bdbd7b..b759965a90 100644 --- a/server/mdm/apple/commander.go +++ b/server/mdm/apple/commander.go @@ -116,6 +116,26 @@ func (svc *MDMAppleCommander) EraseDevice(ctx context.Context, hostUUIDs []strin return svc.EnqueueCommand(ctx, hostUUIDs, raw) } +func (svc *MDMAppleCommander) InstallEnterpriseApplication(ctx context.Context, hostUUIDs []string, uuid string, manifestURL string) error { + raw := fmt.Sprintf(` + + + + Command + + ManifestURL + %s + RequestType + InstallEnterpriseApplication + + + CommandUUID + %s + +`, manifestURL, uuid) + return svc.EnqueueCommand(ctx, hostUUIDs, raw) +} + // EnqueueCommand takes care of enqueuing the commands and sending push // notifications to the devices. // diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index cb36a606ee..de5e851aa3 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -1692,11 +1692,12 @@ func (svc *Service) MDMAppleDisableFileVaultAndEscrow(ctx context.Context, teamI //////////////////////////////////////////////////////////////////////////////// type MDMAppleCheckinAndCommandService struct { - ds fleet.Datastore + ds fleet.Datastore + commander *apple_mdm.MDMAppleCommander } -func NewMDMAppleCheckinAndCommandService(ds fleet.Datastore) *MDMAppleCheckinAndCommandService { - return &MDMAppleCheckinAndCommandService{ds: ds} +func NewMDMAppleCheckinAndCommandService(ds fleet.Datastore, commander *apple_mdm.MDMAppleCommander) *MDMAppleCheckinAndCommandService { + return &MDMAppleCheckinAndCommandService{ds: ds, commander: commander} } // Authenticate handles MDM [Authenticate][1] requests. @@ -1744,6 +1745,17 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm. if err := svc.ds.BulkSetPendingMDMAppleHostProfiles(r.Context, nil, nil, nil, []string{r.ID}); err != nil { return err } + + info, err := svc.ds.GetHostMDMCheckinInfo(r.Context, m.Enrollment.UDID) + if err != nil { + return err + } + if info.InstalledFromDEP { + uuid := uuid.New().String() + if err := svc.commander.InstallEnterpriseApplication(r.Context, []string{m.Enrollment.UDID}, uuid, apple_mdm.FleetdPublicManifestURL); err != nil { + return err + } + } } return nil } diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go index 284a7bcbaa..548ba90e4e 100644 --- a/server/service/apple_mdm_test.go +++ b/server/service/apple_mdm_test.go @@ -970,6 +970,76 @@ func TestMDMAuthenticate(t *testing.T) { require.True(t, ds.NewActivityFuncInvoked) } +func TestMDMTokenUpdate(t *testing.T) { + ctx := context.Background() + ds := new(mock.Store) + mdmStorage := &nanomdm_mock.Storage{} + pushFactory, _ := newMockAPNSPushProviderFactory() + pusher := nanomdm_pushsvc.New( + mdmStorage, + mdmStorage, + pushFactory, + NewNanoMDMLogger(kitlog.NewJSONLogger(os.Stdout)), + ) + cmdr := apple_mdm.NewMDMAppleCommander(mdmStorage, pusher) + svc := MDMAppleCheckinAndCommandService{ds: ds, commander: cmdr} + uuid, serial, model := "ABC-DEF-GHI", "XYZABC", "MacBookPro 16,1" + + mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) { + require.NotNil(t, cmd) + require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType) + require.Contains(t, string(cmd.Raw), apple_mdm.FleetdPublicManifestURL) + return nil, nil + } + + mdmStorage.RetrievePushInfoFunc = func(p0 context.Context, targetUUIDs []string) (map[string]*mdm.Push, error) { + require.ElementsMatch(t, []string{uuid}, targetUUIDs) + pushes := make(map[string]*mdm.Push, len(targetUUIDs)) + for _, uuid := range targetUUIDs { + pushes[uuid] = &mdm.Push{ + PushMagic: "magic" + uuid, + Token: []byte("token" + uuid), + Topic: "topic" + uuid, + } + } + + return pushes, nil + } + + mdmStorage.RetrievePushCertFunc = func(ctx context.Context, topic string) (*tls.Certificate, string, error) { + cert, err := tls.LoadX509KeyPair("testdata/server.pem", "testdata/server.key") + return &cert, "", err + } + + ds.GetNanoMDMEnrollmentFunc = func(ctx context.Context, hostUUID string) (*fleet.NanoEnrollment, error) { + return &fleet.NanoEnrollment{Enabled: true, Type: "Device", TokenUpdateTally: 1}, nil + } + + ds.GetHostMDMCheckinInfoFunc = func(ct context.Context, hostUUID string) (*fleet.HostMDMCheckinInfo, error) { + require.Equal(t, uuid, hostUUID) + return &fleet.HostMDMCheckinInfo{ + HardwareSerial: serial, + DisplayName: model, + InstalledFromDEP: true, + }, nil + } + ds.BulkSetPendingMDMAppleHostProfilesFunc = func(ctx context.Context, hids, tids, pids []uint, uuids []string) error { + return nil + } + + err := svc.TokenUpdate( + &mdm.Request{Context: ctx, EnrollID: &mdm.EnrollID{ID: uuid}}, + &mdm.TokenUpdate{ + Enrollment: mdm.Enrollment{ + UDID: uuid, + }, + }, + ) + require.NoError(t, err) + require.True(t, ds.BulkSetPendingMDMAppleHostProfilesFuncInvoked) + require.True(t, ds.GetHostMDMCheckinInfoFuncInvoked) +} + func TestMDMCheckout(t *testing.T) { ds := new(mock.Store) svc := MDMAppleCheckinAndCommandService{ds: ds} @@ -1606,7 +1676,6 @@ func TestMDMAppleCommander(t *testing.T) { cmdUUID := uuid.New().String() err := cmdr.InstallProfile(ctx, hostUUIDs, mc, cmdUUID) - require.NotEmpty(t, cmdUUID) require.NoError(t, err) require.True(t, mdmStorage.EnqueueCommandFuncInvoked) mdmStorage.EnqueueCommandFuncInvoked = false @@ -1625,8 +1694,22 @@ func TestMDMAppleCommander(t *testing.T) { mdmStorage.EnqueueCommandFuncInvoked = false require.True(t, mdmStorage.RetrievePushInfoFuncInvoked) mdmStorage.RetrievePushInfoFuncInvoked = false - require.NotEmpty(t, cmdUUID) require.NoError(t, err) + + cmdUUID = uuid.New().String() + mdmStorage.EnqueueCommandFunc = func(ctx context.Context, id []string, cmd *mdm.Command) (map[string]error, error) { + require.NotNil(t, cmd) + require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType) + require.Contains(t, string(cmd.Raw), "http://test.example.com") + require.Contains(t, string(cmd.Raw), cmdUUID) + return nil, nil + } + err = cmdr.InstallEnterpriseApplication(ctx, hostUUIDs, "http://test.example.com", cmdUUID) + require.NoError(t, err) + require.True(t, mdmStorage.EnqueueCommandFuncInvoked) + mdmStorage.EnqueueCommandFuncInvoked = false + require.True(t, mdmStorage.RetrievePushInfoFuncInvoked) + mdmStorage.RetrievePushInfoFuncInvoked = false } func TestMDMAppleReconcileProfiles(t *testing.T) { diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go index 979717343b..b98b133206 100644 --- a/server/service/integration_mdm_test.go +++ b/server/service/integration_mdm_test.go @@ -555,11 +555,20 @@ func (s *integrationMDMTestSuite) TestDEPProfileAssignment() { s.DoJSON("GET", "/api/latest/fleet/hosts?mdm_enrollment_status=pending", nil, http.StatusOK, &listHostsRes) require.Len(t, listHostsRes.Hosts, 2) - // enroll one of the hosts d := newDevice(s) + s.pushProvider.PushFunc = func(pushes []*mdm.Push) (map[string]*push.Response, error) { + return map[string]*push.Response{}, nil + } + + // enroll one of the hosts d.serial = devices[0].SerialNumber d.mdmEnroll(s) + // make sure the host gets a request to install fleetd + cmd := d.idle() + require.Equal(t, "InstallEnterpriseApplication", cmd.Command.RequestType) + require.Contains(t, *cmd.Command.InstallEnterpriseApplication.ManifestURL, apple_mdm.FleetdPublicManifestURL) + // only one shows up as pending listHostsRes = listHostsResponse{} s.DoJSON("GET", "/api/latest/fleet/hosts?mdm_enrollment_status=pending", nil, http.StatusOK, &listHostsRes) diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index e71e7f40c7..8a65b0a04e 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -282,6 +282,10 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ if len(opts) > 0 && opts[0].Logger != nil { logger = opts[0].Logger } + var mdmPusher nanomdm_push.Pusher + if len(opts) > 0 && opts[0].MDMPusher != nil { + mdmPusher = opts[0].MDMPusher + } limitStore, _ := memstore.New(0) rootMux := http.NewServeMux() @@ -295,7 +299,7 @@ func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServ mdmStorage, scepStorage, logger, - &MDMAppleCheckinAndCommandService{ds: ds}, + &MDMAppleCheckinAndCommandService{ds: ds, commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mdmPusher)}, ) require.NoError(t, err) }