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)
}