diff --git a/ee/server/service/teams.go b/ee/server/service/teams.go index 20a526197bf5..eded9b4788ac 100644 --- a/ee/server/service/teams.go +++ b/ee/server/service/teams.go @@ -1516,15 +1516,15 @@ func unmarshalWithGlobalDefaults(b *json.RawMessage) (fleet.Features, error) { } func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.Team, enable *bool) error { - var didUpdate, didUpdateMacOSDiskEncryption bool + var didUpdate bool if enable != nil { - if svc.config.Server.PrivateKey == "" { - return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") - } if tm.Config.MDM.EnableDiskEncryption != *enable { + if *enable && svc.config.Server.PrivateKey == "" { + return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + tm.Config.MDM.EnableDiskEncryption = *enable didUpdate = true - didUpdateMacOSDiskEncryption = true } } @@ -1537,13 +1537,7 @@ func (svc *Service) updateTeamMDMDiskEncryption(ctx context.Context, tm *fleet.T if err != nil { return err } - - // macOS-specific stuff. For legacy reasons we check if apple is configured - // via `appCfg.MDM.EnabledAndConfigured` - // - // TODO: is there a missing bitlocker activity feed item? (see same TODO on - // other methods that deal with disk encryption) - if appCfg.MDM.EnabledAndConfigured && didUpdateMacOSDiskEncryption { + if appCfg.MDM.EnabledAndConfigured { var act fleet.ActivityDetails if tm.Config.MDM.EnableDiskEncryption { act = fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &tm.ID, TeamName: &tm.Name} diff --git a/frontend/services/entities/mdm.ts b/frontend/services/entities/mdm.ts index ec7499390b40..aab412772ae7 100644 --- a/frontend/services/entities/mdm.ts +++ b/frontend/services/entities/mdm.ts @@ -168,7 +168,7 @@ const mdmService = { }, getProfilesStatusSummary: (teamId: number) => { - let { MDM_PROFILES_STATUS_SUMMARY: path } = endpoints; + let { PROFILES_STATUS_SUMMARY: path } = endpoints; if (teamId) { path = `${path}?${buildQueryStringFromParams({ team_id: teamId })}`; diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 196345bf2bbe..a1acd94adee5 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -114,7 +114,7 @@ export default { MDM_PROFILE: (id: string) => `/${API_VERSION}/fleet/mdm/profiles/${id}`, MDM_UPDATE_APPLE_SETTINGS: `/${API_VERSION}/fleet/mdm/apple/settings`, - MDM_PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/mdm/profiles/summary`, + PROFILES_STATUS_SUMMARY: `/${API_VERSION}/fleet/configuration_profiles/summary`, MDM_DISK_ENCRYPTION_SUMMARY: `/${API_VERSION}/fleet/mdm/disk_encryption/summary`, MDM_APPLE_SSO: `/${API_VERSION}/fleet/mdm/sso`, MDM_APPLE_ENROLLMENT_PROFILE: (token: string, ref?: string) => { diff --git a/server/datastore/mysql/app_configs.go b/server/datastore/mysql/app_configs.go index 8f8d708eb908..5de25d4d0006 100644 --- a/server/datastore/mysql/app_configs.go +++ b/server/datastore/mysql/app_configs.go @@ -274,7 +274,7 @@ func (ds *Datastore) AggregateEnrollSecretPerTeam(ctx context.Context) ([]*fleet return secrets, nil } -func (ds *Datastore) getConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { +func (ds *Datastore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { if teamID != nil && *teamID > 0 { tc, err := ds.TeamMDMConfig(ctx, *teamID) if err != nil { diff --git a/server/datastore/mysql/app_configs_test.go b/server/datastore/mysql/app_configs_test.go index dc0d4b1c9d9d..46df41010be8 100644 --- a/server/datastore/mysql/app_configs_test.go +++ b/server/datastore/mysql/app_configs_test.go @@ -449,7 +449,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { require.NoError(t, err) require.False(t, ac.MDM.EnableDiskEncryption.Value) - enabled, err := ds.getConfigEnableDiskEncryption(ctx, nil) + enabled, err := ds.GetConfigEnableDiskEncryption(ctx, nil) require.NoError(t, err) require.False(t, enabled) @@ -461,7 +461,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { require.NoError(t, err) require.True(t, ac.MDM.EnableDiskEncryption.Value) - enabled, err = ds.getConfigEnableDiskEncryption(ctx, nil) + enabled, err = ds.GetConfigEnableDiskEncryption(ctx, nil) require.NoError(t, err) require.True(t, enabled) @@ -474,7 +474,7 @@ func testGetConfigEnableDiskEncryption(t *testing.T, ds *Datastore) { require.NotNil(t, tm) require.False(t, tm.Config.MDM.EnableDiskEncryption) - enabled, err = ds.getConfigEnableDiskEncryption(ctx, &team1.ID) + enabled, err = ds.GetConfigEnableDiskEncryption(ctx, &team1.ID) require.NoError(t, err) require.False(t, enabled) diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index b2f7f3a650ed..0de58ba918a0 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -1218,7 +1218,7 @@ func (ds *Datastore) applyHostFilters( return "", nil, ctxerr.Wrap(ctx, err, "building query to filter macOS settings status") } sqlStmt, whereParams = filterHostsByMacOSDiskEncryptionStatus(sqlStmt, opt, whereParams) - if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { + if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { if errors.Is(err, sql.ErrNoRows) { return "", nil, ctxerr.Wrap( ctx, &fleet.BadRequestError{ @@ -4547,6 +4547,7 @@ func (ds *Datastore) HostLite(ctx context.Context, id uint) (*fleet.Host, error) "hardware_model", "computer_name", "platform", + "os_version", "team_id", "distributed_interval", "logger_tls_period", diff --git a/server/datastore/mysql/labels.go b/server/datastore/mysql/labels.go index 21c4e0eb9402..4111c81b6ee0 100644 --- a/server/datastore/mysql/labels.go +++ b/server/datastore/mysql/labels.go @@ -662,7 +662,7 @@ func (ds *Datastore) applyHostLabelFilters(ctx context.Context, filter fleet.Tea } query, whereParams = filterHostsByMacOSDiskEncryptionStatus(query, opt, whereParams) query, whereParams = filterHostsByMDMBootstrapPackageStatus(query, opt, whereParams) - if enableDiskEncryption, err := ds.getConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { + if enableDiskEncryption, err := ds.GetConfigEnableDiskEncryption(ctx, opt.TeamFilter); err != nil { return "", nil, err } else if opt.OSSettingsFilter.IsValid() { query, whereParams, err = ds.filterHostsByOSSettingsStatus(query, opt, whereParams, enableDiskEncryption) diff --git a/server/datastore/mysql/microsoft_mdm.go b/server/datastore/mysql/microsoft_mdm.go index 9e773afddb06..e846ef8af79e 100644 --- a/server/datastore/mysql/microsoft_mdm.go +++ b/server/datastore/mysql/microsoft_mdm.go @@ -585,7 +585,7 @@ AND ( } func (ds *Datastore) GetMDMWindowsBitLockerSummary(ctx context.Context, teamID *uint) (*fleet.MDMWindowsBitLockerSummary, error) { - enabled, err := ds.getConfigEnableDiskEncryption(ctx, teamID) + enabled, err := ds.GetConfigEnableDiskEncryption(ctx, teamID) if err != nil { return nil, err } @@ -655,7 +655,7 @@ func (ds *Datastore) GetMDMWindowsBitLockerStatus(ctx context.Context, host *fle return nil, nil } - enabled, err := ds.getConfigEnableDiskEncryption(ctx, host.TeamID) + enabled, err := ds.GetConfigEnableDiskEncryption(ctx, host.TeamID) if err != nil { return nil, err } @@ -887,7 +887,7 @@ func subqueryHostsMDMWindowsOSSettingsStatusVerified() (string, []interface{}, e } func (ds *Datastore) GetMDMWindowsProfilesSummary(ctx context.Context, teamID *uint) (*fleet.MDMProfilesSummary, error) { - includeBitLocker, err := ds.getConfigEnableDiskEncryption(ctx, teamID) + includeBitLocker, err := ds.GetConfigEnableDiskEncryption(ctx, teamID) if err != nil { return nil, err } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 5979c7ade843..2a536c8a8dde 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -899,6 +899,7 @@ type Datastore interface { GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error + GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for // a host diff --git a/server/fleet/mdm.go b/server/fleet/mdm.go index 55e28bc7b945..4d3e09f68ec8 100644 --- a/server/fleet/mdm.go +++ b/server/fleet/mdm.go @@ -310,8 +310,8 @@ type MDMDiskEncryptionSummary struct { RemovingEnforcement MDMPlatformsCounts `db:"removing_enforcement" json:"removing_enforcement"` } -// MDMProfilesSummary reports the number of hosts being managed with MDM configuration -// profiles. Each host may be counted in only one of four mutually-exclusive categories: +// MDMProfilesSummary reports the number of hosts being managed with configuration +// profiles and/or disk encryption. Each host may be counted in only one of four mutually-exclusive categories: // Failed, Pending, Verifying, or Verified. type MDMProfilesSummary struct { // Verified includes each host where Fleet has verified the installation of all of the diff --git a/server/fleet/service.go b/server/fleet/service.go index ad12547a1d5f..7e9f7c973cbb 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -1059,6 +1059,11 @@ type Service interface { // Returns empty status if the host is not a supported Linux host LinuxHostDiskEncryptionStatus(ctx context.Context, host Host) (HostMDMDiskEncryption, error) + // GetMDMLinuxProfilesSummary summarizes the current status of Linux disk encryption for + // the provided team (or hosts without a team if teamId is nil), or returns zeroes if disk + // encryption is not enforced on the selected team + GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (MDMProfilesSummary, error) + /////////////////////////////////////////////////////////////////////////////// // Common MDM diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index 0490ed54740e..7c1f909f0ba6 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -635,6 +635,8 @@ type GetHostEmailsFunc func(ctx context.Context, hostUUID string, source string) type SetOrUpdateHostDisksSpaceFunc func(ctx context.Context, hostID uint, gigsAvailable float64, percentAvailable float64, gigsTotal float64) error +type GetConfigEnableDiskEncryptionFunc func(ctx context.Context, teamID *uint) (bool, error) + type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, encrypted bool) error type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error @@ -2085,6 +2087,9 @@ type DataStore struct { SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFunc SetOrUpdateHostDisksSpaceFuncInvoked bool + GetConfigEnableDiskEncryptionFunc GetConfigEnableDiskEncryptionFunc + GetConfigEnableDiskEncryptionFuncInvoked bool + SetOrUpdateHostDisksEncryptionFunc SetOrUpdateHostDisksEncryptionFunc SetOrUpdateHostDisksEncryptionFuncInvoked bool @@ -5029,6 +5034,13 @@ func (s *DataStore) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, return s.SetOrUpdateHostDisksSpaceFunc(ctx, hostID, gigsAvailable, percentAvailable, gigsTotal) } +func (s *DataStore) GetConfigEnableDiskEncryption(ctx context.Context, teamID *uint) (bool, error) { + s.mu.Lock() + s.GetConfigEnableDiskEncryptionFuncInvoked = true + s.mu.Unlock() + return s.GetConfigEnableDiskEncryptionFunc(ctx, teamID) +} + func (s *DataStore) SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error { s.mu.Lock() s.SetOrUpdateHostDisksEncryptionFuncInvoked = true diff --git a/server/service/appconfig.go b/server/service/appconfig.go index ad8778b57035..030c2c650202 100644 --- a/server/service/appconfig.go +++ b/server/service/appconfig.go @@ -416,7 +416,7 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle // 1. To get the JSON value from the database // 2. To update fields with the incoming values if newAppConfig.MDM.EnableDiskEncryption.Valid { - if svc.config.Server.PrivateKey == "" { + if newAppConfig.MDM.EnableDiskEncryption.Value && svc.config.Server.PrivateKey == "" { return nil, ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") } appConfig.MDM.EnableDiskEncryption = newAppConfig.MDM.EnableDiskEncryption diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go index da6d28be49bc..6ad19bb1eb97 100644 --- a/server/service/apple_mdm.go +++ b/server/service/apple_mdm.go @@ -2143,12 +2143,15 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable return err } - var didUpdate, didUpdateMacOSDiskEncryption bool + var didUpdate bool if enabled != nil { if ac.MDM.EnableDiskEncryption.Value != *enabled { + if *enabled && svc.config.Server.PrivateKey == "" { + return ctxerr.New(ctx, "Missing required private key. Learn how to configure the private key here: https://fleetdm.com/learn-more-about/fleet-server-private-key") + } + ac.MDM.EnableDiskEncryption = optjson.SetBool(*enabled) didUpdate = true - didUpdateMacOSDiskEncryption = true } } @@ -2156,7 +2159,7 @@ func (svc *Service) updateAppConfigMDMDiskEncryption(ctx context.Context, enable if err := svc.ds.SaveAppConfig(ctx, ac); err != nil { return err } - if didUpdateMacOSDiskEncryption { + if ac.MDM.EnabledAndConfigured { // if macOS MDM is configured, set up FileVault escrow var act fleet.ActivityDetails if ac.MDM.EnableDiskEncryption.Value { act = fleet.ActivityTypeEnabledMacosDiskEncryption{} diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go index 999b6bfb34d6..0dfab1f02f1c 100644 --- a/server/service/integration_core_test.go +++ b/server/service/integration_core_test.go @@ -8638,6 +8638,11 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() { require.Equal(t, hostLin.ID, getHostResp.Host.ID) require.True(t, *getHostResp.Host.DiskEncryptionEnabled) + // should succeed as we no longer require MDM to access this endpoint, as Linux encryption doesn't require MDM + var profiles getMDMProfilesSummaryResponse + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles) + s.DoJSON("GET", "/api/latest/fleet/mdm/profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profiles) + // set unencrypted for all hosts require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostWin.ID, false)) require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), hostMac.ID, false)) @@ -8653,7 +8658,7 @@ func (s *integrationTestSuite) TestGetHostDiskEncryption() { require.Equal(t, hostMac.ID, getHostResp.Host.ID) require.False(t, *getHostResp.Host.DiskEncryptionEnabled) - // Linux does not return false, it omits the field when false + // Linux may omit the field when false getHostResp = getHostResponse{} s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostLin.ID), nil, http.StatusOK, &getHostResp) require.Equal(t, hostLin.ID, getHostResp.Host.ID) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 649d0b5912d0..93bac5241052 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -2881,6 +2881,168 @@ func (s *integrationEnterpriseTestSuite) TestAppleOSUpdatesTeamConfig() { }, http.StatusUnprocessableEntity, &tmResp) } +func (s *integrationEnterpriseTestSuite) TestLinuxDiskEncryption() { + t := s.T() + + // create a Linux host + noTeamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"), + OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "3"), + UUID: t.Name() + "3", + Hostname: t.Name() + "foo3.local", + PrimaryIP: "192.168.1.3", + PrimaryMac: "30-65-EC-6F-C4-60", + Platform: "ubuntu", + OSVersion: "Ubuntu 22.04", + }) + require.NoError(t, err) + orbitKey := setOrbitEnrollment(t, noTeamHost, s.ds) + noTeamHost.OrbitNodeKey = &orbitKey + + team, err := s.ds.NewTeam(context.Background(), &fleet.Team{Name: "A team"}) + require.NoError(t, err) + teamID := ptr.Uint(team.ID) + teamHost, err := s.ds.NewHost(context.Background(), &fleet.Host{ + DetailUpdatedAt: time.Now(), + LabelUpdatedAt: time.Now(), + PolicyUpdatedAt: time.Now(), + SeenTime: time.Now(), + NodeKey: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"), + OsqueryHostID: ptr.String(strings.ReplaceAll(t.Name(), "/", "_") + "2"), + UUID: t.Name() + "2", + Hostname: t.Name() + "foo2.local", + PrimaryIP: "192.168.1.2", + PrimaryMac: "30-65-EC-6F-C4-59", + Platform: "rhel", + OSVersion: "Fedora 38.0", // this check is why HostLite now includes os_version in the data it's selecting + TeamID: teamID, + }) + require.NoError(t, err) + teamOrbitKey := setOrbitEnrollment(t, teamHost, s.ds) + teamHost.OrbitNodeKey = &teamOrbitKey + + // NO TEAM // + + // config profiles endpoint should work but show all zeroes + var profileSummary getMDMProfilesSummaryResponse + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // set encrypted for host + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), noTeamHost.ID, true)) + + // should still show zeroes + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // turn on disk encryption enforcement + s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{EnableDiskEncryption: true}, http.StatusNoContent) + + // should show the Linux host as pending + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary) + + // encryption summary should succeed (Linux encryption doesn't require MDM) + var summary getMDMDiskEncryptionSummaryResponse + s.DoJSON("GET", "/api/latest/fleet/mdm/disk_encryption/summary", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary) + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary) + // disk is encrypted but key hasn't been escrowed yet + require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // trigger escrow process from device + token := "much_valid" + mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error { + _, err := db.ExecContext(context.Background(), `INSERT INTO host_device_auth (host_id, token) VALUES (?, ?)`, noTeamHost.ID, token) + return err + }) + // should fail because default Orbit version is too old + res := s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusBadRequest) + res.Body.Close() + + // should succeed now that Orbit version isn't too old + require.NoError(t, s.ds.SetOrUpdateHostOrbitInfo(context.Background(), noTeamHost.ID, fleet.MinOrbitLUKSVersion, sql.NullString{}, sql.NullBool{})) + res = s.DoRawNoAuth("POST", fmt.Sprintf("/api/latest/fleet/device/%s/mdm/linux/trigger_escrow", token), nil, http.StatusNoContent) + res.Body.Close() + + // confirm that Orbit endpoint shows notification flag + var orbitResponse orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &orbitResponse) + require.True(t, orbitResponse.Notifications.RunDiskEncryptionEscrow) + + // confirm that second Orbit pull doesn't show notification flag + var secondOrbitResponse orbitGetConfigResponse + s.DoJSON("POST", "/api/fleet/orbit/config", orbitGetConfigRequest{OrbitNodeKey: orbitKey}, http.StatusOK, &secondOrbitResponse) + require.False(t, secondOrbitResponse.Notifications.RunDiskEncryptionEscrow) + + // set an error first; the successful write should overwrite that + s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{ + OrbitNodeKey: *noTeamHost.OrbitNodeKey, + ClientError: "Houston, we had a problem", + }, http.StatusNoContent) + + // upload LUKS data + keySlot := ptr.Uint(1) + s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{ + OrbitNodeKey: *noTeamHost.OrbitNodeKey, + Passphrase: "whale makes pail rise", + Salt: "the team i like lost", + KeySlot: keySlot, + }, http.StatusNoContent) + + // confirm verified + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{}, http.StatusOK, &summary) + require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // get passphrase back + var keyResponse getHostEncryptionKeyResponse + s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/mdm/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse) + s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, noTeamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse) + require.Equal(t, "whale makes pail rise", keyResponse.EncryptionKey.DecryptedValue) + + // TEAM // + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // set encrypted for host + require.NoError(t, s.ds.SetOrUpdateHostDisksEncryption(context.Background(), teamHost.ID, true)) + + // should still show zeroes + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{}, profileSummary.MDMProfilesSummary) + + // turn on disk encryption enforcement for team + s.Do("POST", "/api/latest/fleet/disk_encryption", updateDiskEncryptionRequest{TeamID: teamID, EnableDiskEncryption: true}, http.StatusNoContent) + + // should show the Linux host as pending + s.DoJSON("GET", "/api/latest/fleet/configuration_profiles/summary", getMDMProfilesSummaryRequest{TeamID: teamID}, http.StatusOK, &profileSummary) + require.Equal(t, fleet.MDMProfilesSummary{Pending: 1}, profileSummary.MDMProfilesSummary) + + // encryption summary should show host as action required + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary) + require.Equal(t, fleet.MDMDiskEncryptionSummary{ActionRequired: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // upload LUKS data (no error, and no trigger, first this time) + keySlot = ptr.Uint(3) + s.Do("POST", "/api/fleet/orbit/luks_data", orbitPostLUKSRequest{ + OrbitNodeKey: *teamHost.OrbitNodeKey, + Passphrase: "the mome raths outgrabe", + Salt: "jabberwocky, but salty", + KeySlot: keySlot, + }, http.StatusNoContent) + + // confirm verified + s.DoJSON("GET", "/api/latest/fleet/disk_encryption", getMDMDiskEncryptionSummaryRequest{TeamID: teamID}, http.StatusOK, &summary) + require.Equal(t, fleet.MDMDiskEncryptionSummary{Verified: fleet.MDMPlatformsCounts{Linux: 1}}, *summary.MDMDiskEncryptionSummary) + + // get passphrase back + s.DoJSON("GET", fmt.Sprintf(`/api/latest/fleet/hosts/%d/encryption_key`, teamHost.ID), getHostEncryptionKeyRequest{}, http.StatusOK, &keyResponse) + require.Equal(t, "the mome raths outgrabe", keyResponse.EncryptionKey.DecryptedValue) +} + func (s *integrationEnterpriseTestSuite) TestListDevicePolicies() { t := s.T() ctx := context.Background() diff --git a/server/service/linux_mdm.go b/server/service/linux_mdm.go index d4ae8da27e2a..815e2bf04d7f 100644 --- a/server/service/linux_mdm.go +++ b/server/service/linux_mdm.go @@ -3,6 +3,7 @@ package service import ( "context" + "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" ) @@ -42,3 +43,28 @@ func (svc *Service) LinuxHostDiskEncryptionStatus(ctx context.Context, host flee Status: &verified, }, nil } + +func (svc *Service) GetMDMLinuxProfilesSummary(ctx context.Context, teamId *uint) (summary fleet.MDMProfilesSummary, err error) { + if err = svc.authz.Authorize(ctx, fleet.MDMConfigProfileAuthz{TeamID: teamId}, fleet.ActionRead); err != nil { + return summary, ctxerr.Wrap(ctx, err) + } + + // Linux doesn't have configuration profiles, so if we aren't enforcing disk encryption we have nothing to report + includeDiskEncryptionStats, err := svc.ds.GetConfigEnableDiskEncryption(ctx, teamId) + if err != nil { + return summary, ctxerr.Wrap(ctx, err) + } else if !includeDiskEncryptionStats { + return summary, nil + } + + counts, err := svc.ds.GetLinuxDiskEncryptionSummary(ctx, teamId) + if err != nil { + return summary, ctxerr.Wrap(ctx, err) + } + + return fleet.MDMProfilesSummary{ + Verified: counts.Verified, + Pending: counts.ActionRequired, + Failed: counts.Failed, + }, nil +} diff --git a/server/service/mdm.go b/server/service/mdm.go index 12876b95ddd7..41fc1f789e40 100644 --- a/server/service/mdm.go +++ b/server/service/mdm.go @@ -907,7 +907,8 @@ func (svc *Service) GetMDMDiskEncryptionSummary(ctx context.Context, teamID *uin } //////////////////////////////////////////////////////////////////////////////// -// GET /mdm/profiles/summary +// GET /mdm/profiles/summary (deprecated) +// GET /configuration_profiles/summary //////////////////////////////////////////////////////////////////////////////// type getMDMProfilesSummaryRequest struct { @@ -935,10 +936,15 @@ func getMDMProfilesSummaryEndpoint(ctx context.Context, request interface{}, svc return &getMDMProfilesSummaryResponse{Err: err}, nil } - res.Verified = as.Verified + ws.Verified + ls, err := svc.GetMDMLinuxProfilesSummary(ctx, req.TeamID) + if err != nil { + return &getMDMProfilesSummaryResponse{Err: err}, nil + } + + res.Verified = as.Verified + ws.Verified + ls.Verified res.Verifying = as.Verifying + ws.Verifying - res.Failed = as.Failed + ws.Failed - res.Pending = as.Pending + ws.Pending + res.Failed = as.Failed + ws.Failed + ls.Failed + res.Pending = as.Pending + ws.Pending + ls.Pending return &res, nil } @@ -2606,9 +2612,52 @@ func (svc *Service) UploadMDMAppleAPNSCert(ctx context.Context, cert io.ReadSeek return ctxerr.Wrap(ctx, err, "retrieving app config") } + wasEnabledAndConfigured := appCfg.MDM.EnabledAndConfigured appCfg.MDM.EnabledAndConfigured = true + err = svc.ds.SaveAppConfig(ctx, appCfg) + if err != nil { + return ctxerr.Wrap(ctx, err, "saving app config") + } - return svc.ds.SaveAppConfig(ctx, appCfg) + // Disk encryption can be enabled prior to Apple MDM being configured, but we need MDM to be set up to escrow + // FileVault keys. We handle the other order of operations elsewhere (on encryption enable, after checking to see + // if Mac MDM is already enabled). We skip this step if we were just re-uploading an APNs cert when MDM was already + // enabled. + if wasEnabledAndConfigured { + return nil + } + + // Enable FileVault escrow if no-team already has disk encryption enforced + if appCfg.MDM.EnableDiskEncryption.Value { + if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, nil); err != nil { + return ctxerr.Wrap(ctx, err, "enable no-team FileVault escrow") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{}); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for enabling no-team macOS disk encryption") + } + } + // Enable FileVault escrow for teams that already have disk encryption enforced + // For later: add a data store method to avoid making an extra query per team to check whether encryption is enforced + teams, err := svc.ds.TeamsSummary(ctx) + if err != nil { + return ctxerr.Wrap(ctx, err, "listing teams") + } + for _, team := range teams { + isEncryptionEnforced, err := svc.ds.GetConfigEnableDiskEncryption(ctx, &team.ID) + if err != nil { + return ctxerr.Wrap(ctx, err, "retrieving encryption enforcement status for team") + } + if isEncryptionEnforced { + if err := svc.EnterpriseOverrides.MDMAppleEnableFileVaultAndEscrow(ctx, &team.ID); err != nil { + return ctxerr.Wrap(ctx, err, "enable FileVault escrow for team") + } + if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), fleet.ActivityTypeEnabledMacosDiskEncryption{TeamID: &team.ID, TeamName: &team.Name}); err != nil { + return ctxerr.Wrap(ctx, err, "create activity for enabling macOS disk encryption for team") + } + } + } + + return nil } ////////////////////////////////////////////////////////////////////////////////