use Escrow Buddy to rotate FileVault keys on macOS (#20842)

back-end and agent part of #13157

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [x] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [x] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [x] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
This commit is contained in:
Roberto Dip 2024-07-31 16:59:30 -03:00 committed by GitHub
parent fbf1f55399
commit 7a080a9b36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 537 additions and 199 deletions

View file

@ -424,6 +424,14 @@ endif
tar czf $(out-path)/swiftDialog.app.tar.gz -C $(TMP_DIR)/swiftDialog_pkg_payload_expanded/Library/Application\ Support/Dialog/ Dialog.app
rm -rf $(TMP_DIR)
# Generate escrowBuddy.pkg bundle from the Escrow Buddy repo.
#
# Usage:
# make escrow-buddy-pkg version=1.0.0 out-path=.
escrow-buddy-pkg:
curl -L https://github.com/macadmins/escrow-buddy/releases/download/v$(version)/Escrow.Buddy-$(version).pkg --output $(out-path)/escrowBuddy.pkg
# Build and generate desktop.app.tar.gz bundle.
#
# Usage:

1
changes/13157-fv-escrow Normal file
View file

@ -0,0 +1 @@
* `fleetd` now uses Escrow Buddy to rotate FileVault keys. Internal API endpoints documented in the API for contributors have been modified and/or removed.

View file

@ -270,6 +270,8 @@ func updatesAddFunc(c *cli.Context) error {
dstPath += ".exe"
case strings.HasSuffix(target, ".app.tar.gz"):
dstPath += ".app.tar.gz"
case strings.HasSuffix(target, ".pkg"):
dstPath += ".pkg"
// osquery extensions require the .ext suffix
case strings.HasSuffix(target, ".ext"):
dstPath += ".ext"

View file

@ -17,10 +17,6 @@ func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([
return svc.ds.ListPoliciesForHost(ctx, host)
}
func (svc *Service) RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error {
return svc.ds.SetDiskEncryptionResetStatus(ctx, hostID, true)
}
const refetchMDMUnenrollCriticalQueryDuration = 3 * time.Minute
// TriggerMigrateMDMDevice triggers the webhook associated with the MDM

View file

@ -0,0 +1 @@
* Use Escrow Buddy to rotate FileVault keys on macOS

View file

@ -870,7 +870,11 @@ func main() {
orbitClient.RegisterConfigReceiver(update.ApplyNudgeConfigReceiverMiddleware(update.NudgeConfigFetcherOptions{
UpdateRunner: updateRunner, RootDir: c.String("root-dir"), Interval: nudgeLaunchInterval,
}))
orbitClient.RegisterConfigReceiver(update.ApplyDiskEncryptionRunnerMiddleware())
if orbitClient.GetServerCapabilities().Has(fleet.CapabilityEscrowBuddy) {
orbitClient.RegisterConfigReceiver(update.NewEscrowBuddyRunner(updateRunner, 5*time.Minute))
} else {
orbitClient.RegisterConfigReceiver(update.ApplyDiskEncryptionRunnerMiddleware())
}
orbitClient.RegisterConfigReceiver(update.ApplySwiftDialogDownloaderMiddleware(updateRunner))
case "windows":
orbitClient.RegisterConfigReceiver(update.ApplyWindowsMDMEnrollmentFetcherMiddleware(windowsMDMEnrollmentCommandFrequency, orbitHostInfo.HardwareUUID, orbitClient))

View file

@ -0,0 +1,120 @@
package update
import (
"fmt"
"sync"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/rs/zerolog/log"
)
// EscrowBuddyRunner sets up [Escrow Buddy][1] to rotate FileVault keys on
// macOS without user interaction. This runner:
//
// - Ensures Escrow Buddy is added as a target for the update runner, so the
// authorization plugin is downloaded and installed.
// - Shells out to call `defaults` to configure Escrow Buddy according to
// server instructions provided via notifications.
//
// [1]: https://github.com/macadmins/escrow-buddy
type EscrowBuddyRunner struct {
// updateRunner is the wrapped Runner where Escrow Buddy will be set as
// a target.
updateRunner *Runner
// runCmdFunc can be set in tests to mock the command executed to
// configure Escrow Buddy
runCmdFunc func(cmd string, args ...string) error
// runMu guards runs to prevent multiple Run calls happening at the
// same time.
runMu sync.Mutex
// lastRun is used to guarantee that the run interval is enforced
lastRun time.Time
// interval defines how often Run is allowed to perform work
interval time.Duration
}
// NewEscrowBuddyRunner returns a new instance configured with the provided values
func NewEscrowBuddyRunner(runner *Runner, interval time.Duration) fleet.OrbitConfigReceiver {
return &EscrowBuddyRunner{updateRunner: runner, interval: interval}
}
func (e *EscrowBuddyRunner) Run(cfg *fleet.OrbitConfig) error {
log.Debug().Msgf("EscrowBuddyRunner: notification: %t", cfg.Notifications.RotateDiskEncryptionKey)
if e.updateRunner == nil {
log.Debug().Msg("EscrowBuddyRunner: received nil UpdateRunner, this probably indicates that updates are turned off. Skipping any actions related to Disk encryption")
return nil
}
if !e.runMu.TryLock() {
log.Debug().Msg("EscrowBuddyRunner: a previous instance is currently running, returning early")
return nil
}
defer e.runMu.Unlock()
if time.Since(e.lastRun) < e.interval {
log.Debug().Msgf("EscrowBuddyRunner: last run (%v) is less than the configured interval (%v), returning early", e.lastRun, e.interval)
return nil
}
updaterHasTarget := e.updateRunner.HasRunnerOptTarget("escrowBuddy")
// if the notification is false, it could mean that we shouldn't do
// anything at all (eg: MDM is not configured) or that this host
// doesn't need to rotate the key.
//
// if Escrow Buddy is a TUF target, it means that we tried to rotate
// the key before, and we must disable it to keep the local state as
// instructed by the server.
if !cfg.Notifications.RotateDiskEncryptionKey {
if updaterHasTarget {
log.Debug().Msg("EscrowBuddyRunner: disabling disk encryption rotation")
e.lastRun = time.Now()
return e.setGenerateNewKeyTo(false)
}
log.Debug().Msg("EscrowBuddyRunner: skipping any actions related to disk encryption")
return nil
}
runnerHasLocalHash := e.updateRunner.HasLocalHash("escrowBuddy")
if !updaterHasTarget || !runnerHasLocalHash {
log.Info().Msg("refreshing the update runner config with Escrow Buddy targets and hashes")
log.Debug().Msgf("updater has target: %t, runner has local hash: %t", updaterHasTarget, runnerHasLocalHash)
if err := e.setTargetsAndHashes(); err != nil {
return fmt.Errorf("setting Escrow Buddy targets and hashes: %w", err)
}
}
log.Debug().Msg("EscrowBuddyRunner: enabling disk encryption rotation")
if err := e.setGenerateNewKeyTo(true); err != nil {
return fmt.Errorf("enabling disk encryption rotation: %w", err)
}
e.lastRun = time.Now()
return nil
}
func (e *EscrowBuddyRunner) setTargetsAndHashes() error {
e.updateRunner.AddRunnerOptTarget("escrowBuddy")
e.updateRunner.updater.SetTargetInfo("escrowBuddy", EscrowBuddyMacOSTarget)
// we don't want to keep escrowBuddy as a target if we failed to update the
// cached hashes in the runner.
if err := e.updateRunner.StoreLocalHash("escrowBuddy"); err != nil {
log.Debug().Msgf("removing escrowBuddy from target options, error updating local hashes: %s", err)
e.updateRunner.RemoveRunnerOptTarget("escrowBuddy")
e.updateRunner.updater.RemoveTargetInfo("escrowBuddy")
return err
}
return nil
}
func (e *EscrowBuddyRunner) setGenerateNewKeyTo(enabled bool) error {
log.Debug().Msgf("running defaults write to configure Escrow Buddy with value %t", enabled)
cmd := fmt.Sprintf("defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool %t", enabled)
fn := e.runCmdFunc
if fn == nil {
fn = runCmdCollectErr
}
return fn("sh", "-c", cmd)
}

View file

@ -0,0 +1,86 @@
package update
import (
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
func TestEscrowBuddy(t *testing.T) {
testingSuite := new(escrowBuddyTestSuite)
testingSuite.s = &testingSuite.Suite
suite.Run(t, testingSuite)
}
type escrowBuddyTestSuite struct {
suite.Suite
withTUF
}
func (s *escrowBuddyTestSuite) TestUpdatesDisabled() {
t := s.T()
cfg := &fleet.OrbitConfig{}
cfg.Notifications.RotateDiskEncryptionKey = true
r := NewEscrowBuddyRunner(nil, time.Second)
err := r.Run(cfg)
require.NoError(t, err)
}
func (s *escrowBuddyTestSuite) TestEscrowBuddyRotatesKey() {
t := s.T()
updater := &Updater{
client: s.client,
opt: Options{Targets: make(map[string]TargetInfo), RootDirectory: t.TempDir()},
}
runner := &Runner{updater: updater, localHashes: make(map[string][]byte)}
escrowBuddyPath := "escrowBuddy/macos/stable/escrowBuddy.pkg"
cfg := &fleet.OrbitConfig{}
r := &EscrowBuddyRunner{updateRunner: runner, interval: time.Millisecond}
// mock the command to run the defaults cli
cmdCalls := []map[string]any{}
r.runCmdFunc = func(cmd string, args ...string) error {
cmdCalls = append(cmdCalls, map[string]any{"cmd": cmd, "args": args})
return nil
}
// no new target added if the notification is not set
err := r.Run(cfg)
require.NoError(t, err)
targets := runner.updater.opt.Targets
require.Len(t, targets, 0)
require.Empty(t, cmdCalls)
// there's an error when the remote repo doesn't have the target yet
cfg.Notifications.RotateDiskEncryptionKey = true
err = r.Run(cfg)
require.ErrorContains(t, err, "tuf: file not found")
require.Empty(t, cmdCalls)
// add escrow buddy to the remote
s.addRemoteTarget(escrowBuddyPath)
err = r.Run(cfg)
require.NoError(t, err)
require.Len(t, cmdCalls, 1)
require.Equal(t, cmdCalls[0]["cmd"], "sh")
require.Equal(t, cmdCalls[0]["args"], []string{"-c", "defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool true"})
targets = runner.updater.opt.Targets
require.Len(t, targets, 1)
ti, ok := targets["escrowBuddy"]
require.True(t, ok)
require.EqualValues(t, EscrowBuddyMacOSTarget, ti)
time.Sleep(3 * time.Millisecond)
cfg.Notifications.RotateDiskEncryptionKey = false
err = r.Run(cfg)
require.NoError(t, err)
require.Len(t, cmdCalls, 2)
require.Equal(t, cmdCalls[1]["cmd"], "sh")
require.Equal(t, cmdCalls[1]["args"], []string{"-c", "defaults write /Library/Preferences/com.netflix.Escrow-Buddy.plist GenerateNewKey -bool false"})
}

View file

@ -122,4 +122,10 @@ var (
TargetFile: "swiftDialog.app.tar.gz",
ExtractedExecSubPath: []string{"Dialog.app", "Contents", "MacOS", "Dialog"},
}
EscrowBuddyMacOSTarget = TargetInfo{
Platform: "macos",
Channel: "stable",
TargetFile: "escrowBuddy.pkg",
}
)

View file

@ -85,6 +85,7 @@ func (ts *withTUF) SetupSuite() {
ts.mockFiles = map[string][]byte{
"nudge/macos/stable/nudge.app.tar.gz": ts.memTarGz("/Nudge.app/Contents/MacOS/Nudge", "nudge"),
"osqueryd/macos/stable/osqueryd.app.tar.gz": ts.memTarGz("osqueryd", "osqueryd"),
"escrowBuddy/macos/stable/escrowBuddy.pkg": {},
}
ts.store = tuf.MemoryStore(nil, ts.mockFiles)

View file

@ -384,6 +384,12 @@ func (u *Updater) get(target string) (*LocalTarget, error) {
return nil, fmt.Errorf("failed to remove old extracted dir: %q: %w", localTarget.DirPath, err)
}
}
if strings.HasSuffix(localTarget.Path, ".pkg") {
cmd := exec.Command("installer", "-pkg", localTarget.Path, "-target", "/")
if out, err := cmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("running pkgutil to install %s: %s: %w", localTarget.Path, string(out), err)
}
}
} else {
log.Debug().Str("path", localTarget.Path).Str("target", target).Msg("found expected target locally")
}
@ -558,6 +564,14 @@ func (u *Updater) checkExec(target, tmpPath string, customCheckExec func(execPat
tmpPath = filepath.Join(append([]string{filepath.Dir(tmpPath)}, localTarget.Info.ExtractedExecSubPath...)...)
}
if strings.HasSuffix(tmpPath, ".pkg") && runtime.GOOS == "darwin" {
cmd := exec.Command("pkgutil", "--payload-files", tmpPath)
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("running pkgutil to verify %s: %s: %w", tmpPath, string(out), err)
}
return nil
}
if customCheckExec != nil {
if err := customCheckExec(tmpPath); err != nil {
return fmt.Errorf("custom exec new version failed: %w", err)

View file

@ -2271,7 +2271,6 @@ func (ds *Datastore) LoadHostByOrbitNodeKey(ctx context.Context, nodeKey string)
h.policy_updated_at,
h.public_ip,
h.orbit_node_key,
COALESCE(hdek.reset_requested, false) AS disk_encryption_reset_requested,
IF(hdep.host_id AND ISNULL(hdep.deleted_at), true, false) AS dep_assigned_to_fleet,
hd.encrypted as disk_encryption_enabled,
COALESCE(hdek.decryptable, false) as encryption_key_available,
@ -4955,20 +4954,6 @@ func (ds *Datastore) ListUpcomingHostMaintenanceWindows(ctx context.Context, hid
return mws, nil
}
func (ds *Datastore) SetDiskEncryptionResetStatus(ctx context.Context, hostID uint, status bool) error {
const stmt = `
INSERT INTO host_disk_encryption_keys (host_id, reset_requested, base64_encrypted)
VALUES (?, ?, '')
ON DUPLICATE KEY UPDATE
reset_requested = VALUES(reset_requested)`
_, err := ds.writer(ctx).ExecContext(ctx, stmt, hostID, status)
if err != nil {
return ctxerr.Wrap(ctx, err, "upsert disk encryption reset status")
}
return nil
}
// countHostNotResponding counts the hosts that haven't been submitting results for sent queries.
//
// Notes:

View file

@ -7616,7 +7616,6 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
// the returned host by LoadHostByOrbitNodeKey will have the orbit key stored
h.OrbitNodeKey = &orbitKey
h.DiskEncryptionResetRequested = ptr.Bool(false)
returned, err := ds.LoadHostByOrbitNodeKey(ctx, orbitKey)
require.NoError(t, err)
@ -7696,8 +7695,8 @@ func testHostsLoadHostByOrbitNodeKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
loadFleet, err = ds.LoadHostByOrbitNodeKey(ctx, *hFleet.OrbitNodeKey)
require.NoError(t, err)
require.True(t, loadFleet.MDM.EncryptionKeyAvailable)
require.NoError(t, err)
require.True(t, loadFleet.MDM.EncryptionKeyAvailable)
require.NotNil(t, loadFleet.DiskEncryptionEnabled)
require.True(t, *loadFleet.DiskEncryptionEnabled)
@ -8361,8 +8360,8 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) {
// no disk encryption key information
got, err := ds.Host(ctx, host.ID)
require.NoError(t, err)
require.False(t, got.MDM.EncryptionKeyAvailable)
require.NotNil(t, got.MDM.TestGetRawDecryptable())
require.False(t, got.MDM.EncryptionKeyAvailable)
require.Equal(t, -1, *got.MDM.TestGetRawDecryptable())
// create the encryption key row, but unknown decryptable
@ -8380,8 +8379,8 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) {
got, err = ds.Host(ctx, host.ID)
require.NoError(t, err)
require.False(t, got.MDM.EncryptionKeyAvailable)
require.NotNil(t, got.MDM.TestGetRawDecryptable())
require.False(t, got.MDM.EncryptionKeyAvailable)
require.Equal(t, 0, *got.MDM.TestGetRawDecryptable())
// mark the key as decryptable
@ -8390,8 +8389,8 @@ func testHostsEncryptionKeyRawDecryption(t *testing.T, ds *Datastore) {
got, err = ds.Host(ctx, host.ID)
require.NoError(t, err)
require.True(t, got.MDM.EncryptionKeyAvailable)
require.NotNil(t, got.MDM.TestGetRawDecryptable())
require.True(t, got.MDM.EncryptionKeyAvailable)
require.Equal(t, 1, *got.MDM.TestGetRawDecryptable())
}

View file

@ -78,6 +78,8 @@ const (
// CapabilityEndUserEmail denotes the ability of the server to support
// receiving the end-user email from orbit.
CapabilityEndUserEmail Capability = "end_user_email"
// CapabilityEscrowBuddy allows to use Escrow Buddy to rotate FileVault keys
CapabilityEscrowBuddy Capability = "escrow_buddy"
)
func GetServerOrbitCapabilities() CapabilityMap {
@ -85,6 +87,7 @@ func GetServerOrbitCapabilities() CapabilityMap {
CapabilityOrbitEndpoints: {},
CapabilityTokenRotation: {},
CapabilityEndUserEmail: {},
CapabilityEscrowBuddy: {},
}
}

View file

@ -883,8 +883,6 @@ type Datastore interface {
// GetHostDiskEncryptionKey returns the encryption key information for a given host
GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*HostDiskEncryptionKey, error)
SetDiskEncryptionResetStatus(ctx context.Context, hostID uint, status bool) error
// GetHostCertAssociationsToExpire retrieves host certificate
// associations that are close to expire and don't have a renewal in
// progress based on the provided arguments.

View file

@ -321,10 +321,6 @@ type Host struct {
// omitted if we don't have encryption information yet.
DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled" csv:"-"`
// DiskEncryptionResetRequested is only fetched when loading a host by
// orbit_node_key, and so it's not used in the UI.
DiskEncryptionResetRequested *bool `json:"disk_encryption_reset_requested,omitempty" db:"disk_encryption_reset_requested" csv:"-"`
HostIssues `json:"issues,omitempty" csv:"-"`
// DeviceMapping is in fact included in the CSV export, but it is not directly
@ -574,11 +570,7 @@ func (d *MDMHostData) PopulateOSSettingsAndMacOSSettings(profiles []HostMDMApple
// but either we didn't get an encryption key or we're not able to
// decrypt the key we've got
settings.DiskEncryption = DiskEncryptionActionRequired.addrOf()
if *d.rawDecryptable == 0 {
settings.ActionRequired = ActionRequiredRotateKey.addrOf()
} else {
settings.ActionRequired = ActionRequiredLogOut.addrOf()
}
settings.ActionRequired = ActionRequiredRotateKey.addrOf()
} else {
// if [a FileVault profile is pending to be installed or] the
// matching row in host_disk_encryption_keys has a field decryptable
@ -1230,19 +1222,3 @@ func IsEligibleForDEPMigration(host *Host, mdmInfo *HostMDM, isConnectedToFleetM
// the checkout message from the host.
(!isConnectedToFleetMDM || mdmInfo.Name != WellKnownMDMFleet)
}
// IsEligibleForBitLockerEncryption checks if the host needs to enforce disk
// encryption using Fleet MDM features.
func IsEligibleForBitLockerEncryption(h *Host, mdmInfo *HostMDM, isConnectedToFleetMDM bool) bool {
isServer := mdmInfo != nil && mdmInfo.IsServer
isWindows := h.FleetPlatform() == "windows"
needsEncryption := h.DiskEncryptionEnabled != nil && !*h.DiskEncryptionEnabled
encryptedWithoutKey := h.DiskEncryptionEnabled != nil && *h.DiskEncryptionEnabled && !h.MDM.EncryptionKeyAvailable
return isWindows &&
h.IsOsqueryEnrolled() &&
isConnectedToFleetMDM &&
!isServer &&
mdmInfo != nil &&
(needsEncryption || encryptedWithoutKey)
}

View file

@ -214,50 +214,6 @@ func TestMDMEnrollmentStatus(t *testing.T) {
}
}
func TestIsEligibleForBitLockerEncryption(t *testing.T) {
require.False(t, IsEligibleForBitLockerEncryption(&Host{}, &HostMDM{}, false))
hostThatNeedsEnforcement := &Host{
Platform: "windows",
OsqueryHostID: ptr.String("test"),
MDM: MDMHostData{
EncryptionKeyAvailable: false,
},
DiskEncryptionEnabled: ptr.Bool(false),
}
hostThatNeedsEnforcementMdmInfo := &HostMDM{
Name: WellKnownMDMFleet,
Enrolled: true,
IsServer: false,
InstalledFromDep: true,
}
require.True(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
// macOS hosts are not elegible
hostThatNeedsEnforcement.Platform = "darwin"
require.False(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
hostThatNeedsEnforcement.Platform = "windows"
require.True(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
// hosts with disk encryption already enabled are elegible only if we
// can't decrypt the key
hostThatNeedsEnforcement.DiskEncryptionEnabled = ptr.Bool(true)
require.True(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
hostThatNeedsEnforcement.MDM.EncryptionKeyAvailable = true
require.False(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
hostThatNeedsEnforcement.DiskEncryptionEnabled = ptr.Bool(false)
hostThatNeedsEnforcement.MDM.EncryptionKeyAvailable = false
require.True(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
// hosts without MDMinfo are not elegible
require.False(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, nil, true))
require.True(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, true))
// hosts that are not enrolled in MDM are not elegible
require.False(t, IsEligibleForBitLockerEncryption(hostThatNeedsEnforcement, hostThatNeedsEnforcementMdmInfo, false))
}
func TestIsEligibleForDEPMigration(t *testing.T) {
testCases := []struct {
name string

View file

@ -928,8 +928,6 @@ type Service interface {
// for all hosts that are already marked as failing.
ResetAutomation(ctx context.Context, teamIDs, policyIDs []uint) error
RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error
///////////////////////////////////////////////////////////////////////////////
// Windows MDM

View file

@ -618,8 +618,6 @@ type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uin
type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error)
type SetDiskEncryptionResetStatusFunc func(ctx context.Context, hostID uint, status bool) error
type GetHostCertAssociationsToExpireFunc func(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error)
type SetCommandForPendingSCEPRenewalFunc func(ctx context.Context, assocs []fleet.SCEPIdentityAssociation, cmdUUID string) error
@ -1910,9 +1908,6 @@ type DataStore struct {
GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFunc
GetHostDiskEncryptionKeyFuncInvoked bool
SetDiskEncryptionResetStatusFunc SetDiskEncryptionResetStatusFunc
SetDiskEncryptionResetStatusFuncInvoked bool
GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFunc
GetHostCertAssociationsToExpireFuncInvoked bool
@ -4597,13 +4592,6 @@ func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (
return s.GetHostDiskEncryptionKeyFunc(ctx, hostID)
}
func (s *DataStore) SetDiskEncryptionResetStatus(ctx context.Context, hostID uint, status bool) error {
s.mu.Lock()
s.SetDiskEncryptionResetStatusFuncInvoked = true
s.mu.Unlock()
return s.SetDiskEncryptionResetStatusFunc(ctx, hostID, status)
}
func (s *DataStore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) {
s.mu.Lock()
s.GetHostCertAssociationsToExpireFuncInvoked = true

View file

@ -564,41 +564,6 @@ func (svc *Service) GetDeviceMDMAppleEnrollmentProfile(ctx context.Context) ([]b
return signed, nil
}
////////////////////////////////////////////////////////////////////////////////
// Request a disk encryption reset
////////////////////////////////////////////////////////////////////////////////
type rotateEncryptionKeyRequest struct {
Token string `url:"token"`
}
func (r *rotateEncryptionKeyRequest) deviceAuthToken() string {
return r.Token
}
type rotateEncryptionKeyResponse struct {
Err error `json:"error,omitempty"`
}
func (r rotateEncryptionKeyResponse) error() error { return r.Err }
func rotateEncryptionKeyEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
host, ok := hostctx.FromContext(ctx)
if !ok {
err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
return rotateEncryptionKeyResponse{Err: err}, nil
}
if err := svc.RequestEncryptionKeyRotation(ctx, host.ID); err != nil {
return rotateEncryptionKeyResponse{Err: err}, nil
}
return rotateEncryptionKeyResponse{}, nil
}
func (svc *Service) RequestEncryptionKeyRotation(ctx context.Context, hostID uint) error {
return fleet.ErrMissingLicense
}
////////////////////////////////////////////////////////////////////////////////
// Signal start of mdm migration on a device
////////////////////////////////////////////////////////////////////////////////

View file

@ -799,10 +799,6 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
errorLimiter.Limit("get_device_mdm", desktopQuota),
).GET("/api/_version_/fleet/device/{token}/mdm/apple/manual_enrollment_profile", getDeviceMDMManualEnrollProfileEndpoint, getDeviceMDMManualEnrollProfileRequest{})
demdm.WithCustomMiddleware(
errorLimiter.Limit("post_device_rotate_encryption_key", desktopQuota),
).POST("/api/_version_/fleet/device/{token}/rotate_encryption_key", rotateEncryptionKeyEndpoint, rotateEncryptionKeyRequest{})
demdm.WithCustomMiddleware(
errorLimiter.Limit("post_device_migrate_mdm", desktopQuota),
).POST("/api/_version_/fleet/device/{token}/migrate_mdm", migrateMDMDeviceEndpoint, deviceMigrateMDMRequest{})

View file

@ -137,7 +137,7 @@ func TestHostDetailsMDMAppleDiskEncryption(t *testing.T) {
OperationType: fleet.MDMOperationTypeInstall,
},
fleet.DiskEncryptionActionRequired,
fleet.ActionRequiredLogOut,
fleet.ActionRequiredRotateKey,
&fleet.MDMDeliveryPending,
},
{
@ -1857,3 +1857,196 @@ func TestBulkOperationFilterValidation(t *testing.T) {
})
}
}
func TestSetDiskEncryptionNotifications(t *testing.T) {
ds := new(mock.Store)
ctx := context.Background()
svc := &Service{ds: ds}
tests := []struct {
name string
host *fleet.Host
appConfig *fleet.AppConfig
diskEncryptionConfigured bool
isConnectedToFleetMDM bool
mdmInfo *fleet.HostMDM
getHostDiskEncryptionKey func(context.Context, uint) (*fleet.HostDiskEncryptionKey, error)
expectedNotifications *fleet.OrbitConfigNotifications
expectedError bool
}{
{
name: "no MDM configured",
host: &fleet.Host{ID: 1, Platform: "darwin"},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: false},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "not connected to Fleet MDM",
host: &fleet.Host{ID: 1, Platform: "darwin"},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: false,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "host not enrolled in osquery",
host: &fleet.Host{ID: 1, Platform: "darwin", OsqueryHostID: nil},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "disk encryption not configured",
host: &fleet.Host{ID: 1, Platform: "darwin"},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: false,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: nil,
expectedNotifications: &fleet.OrbitConfigNotifications{},
expectedError: false,
},
{
name: "darwin with decryptable key",
host: &fleet.Host{ID: 1, Platform: "darwin"},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{Decryptable: ptr.Bool(true)}, nil
},
expectedNotifications: &fleet.OrbitConfigNotifications{
RotateDiskEncryptionKey: false,
},
expectedError: false,
},
{
name: "windows server with no encryption needed",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(true)},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: true},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: false,
},
expectedError: false,
},
{
name: "windows with encryption enabled but key missing",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(true)},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: false},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: true,
},
expectedError: false,
},
{
name: "darwin with missing encryption key",
host: &fleet.Host{ID: 1, Platform: "darwin"},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: nil,
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
RotateDiskEncryptionKey: false,
},
expectedError: false,
},
{
name: "windows with encryption key and not decryptable",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(true)},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: false},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return &fleet.HostDiskEncryptionKey{Decryptable: ptr.Bool(false)}, nil
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: true,
},
expectedError: false,
},
{
name: "windows with enforce BitLocker",
host: &fleet.Host{ID: 1, Platform: "windows", DiskEncryptionEnabled: ptr.Bool(false)},
appConfig: &fleet.AppConfig{
MDM: fleet.MDM{EnabledAndConfigured: true},
},
diskEncryptionConfigured: true,
isConnectedToFleetMDM: true,
mdmInfo: &fleet.HostMDM{IsServer: false},
getHostDiskEncryptionKey: func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
return nil, newNotFoundError()
},
expectedNotifications: &fleet.OrbitConfigNotifications{
EnforceBitLockerEncryption: true,
},
expectedError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.getHostDiskEncryptionKey != nil {
ds.GetHostDiskEncryptionKeyFunc = tt.getHostDiskEncryptionKey
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return tt.appConfig, nil
}
notifs := &fleet.OrbitConfigNotifications{}
err := svc.setDiskEncryptionNotifications(ctx, notifs, tt.host, tt.appConfig, tt.diskEncryptionConfigured, tt.isConnectedToFleetMDM, tt.mdmInfo)
if tt.expectedError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.expectedNotifications.RotateDiskEncryptionKey, notifs.RotateDiskEncryptionKey)
})
}
}

View file

@ -1729,7 +1729,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleHostDiskEncryption() {
require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
require.Equal(t, fleet.DiskEncryptionActionRequired, *getHostResp.Host.MDM.MacOSSettings.DiskEncryption)
require.NotNil(t, getHostResp.Host.MDM.MacOSSettings.ActionRequired)
require.Equal(t, fleet.ActionRequiredLogOut, *getHostResp.Host.MDM.MacOSSettings.ActionRequired)
require.Equal(t, fleet.ActionRequiredRotateKey, *getHostResp.Host.MDM.MacOSSettings.ActionRequired)
require.NotNil(t, getHostResp.Host.MDM.OSSettings)
require.NotNil(t, getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
require.Equal(t, fleet.DiskEncryptionActionRequired, *getHostResp.Host.MDM.OSSettings.DiskEncryption.Status)
@ -2521,36 +2521,6 @@ func (s *integrationMDMTestSuite) TestEnrollOrbitAfterDEPSync() {
require.Equal(t, h.ID, got.ID)
}
func (s *integrationMDMTestSuite) TestDiskEncryptionRotation() {
t := s.T()
h := createOrbitEnrolledHost(t, "darwin", "h", s.ds)
// false by default
resp := orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
require.False(t, resp.Notifications.RotateDiskEncryptionKey)
// create an auth token for h
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 (?, ?)`, h.ID, token)
return err
})
tokRes := s.DoRawNoAuth("POST", "/api/latest/fleet/device/"+token+"/rotate_encryption_key", nil, http.StatusOK)
tokRes.Body.Close()
// true after the POST request
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
require.True(t, resp.Notifications.RotateDiskEncryptionKey)
// false on following requests
resp = orbitGetConfigResponse{}
s.DoJSON("POST", "/api/fleet/orbit/config", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *h.OrbitNodeKey)), http.StatusOK, &resp)
require.False(t, resp.Notifications.RotateDiskEncryptionKey)
}
func (s *integrationMDMTestSuite) TestFleetdConfiguration() {
t := s.T()
s.assertConfigProfilesByIdentifier(nil, mobileconfig.FleetdConfigPayloadIdentifier, false)

View file

@ -207,15 +207,6 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
notifs.NeedsMDMMigration = true
}
if host.DiskEncryptionResetRequested != nil && *host.DiskEncryptionResetRequested {
notifs.RotateDiskEncryptionKey = true
// Since this is an user initiated action, we disable
// the flag when we deliver the notification to Orbit
if err := svc.ds.SetDiskEncryptionResetStatus(ctx, host.ID, false); err != nil {
return fleet.OrbitConfig{}, err
}
}
}
// set the host's orbit notifications for Windows MDM
@ -309,9 +300,17 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
}
}
if mdmConfig.EnableDiskEncryption &&
fleet.IsEligibleForBitLockerEncryption(host, mdmInfo, isConnectedToFleetMDM) {
notifs.EnforceBitLockerEncryption = true
err = svc.setDiskEncryptionNotifications(
ctx,
&notifs,
host,
appConfig,
mdmConfig.EnableDiskEncryption,
isConnectedToFleetMDM,
mdmInfo,
)
if err != nil {
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "setting team disk encryption notifications")
}
var updateChannels *fleet.OrbitUpdateChannels
@ -371,10 +370,17 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
}
}
if appConfig.MDM.WindowsEnabledAndConfigured &&
appConfig.MDM.EnableDiskEncryption.Value &&
fleet.IsEligibleForBitLockerEncryption(host, mdmInfo, isConnectedToFleetMDM) {
notifs.EnforceBitLockerEncryption = true
err = svc.setDiskEncryptionNotifications(
ctx,
&notifs,
host,
appConfig,
appConfig.MDM.EnableDiskEncryption.Value,
isConnectedToFleetMDM,
mdmInfo,
)
if err != nil {
return fleet.OrbitConfig{}, ctxerr.Wrap(ctx, err, "setting no-team disk encryption notifications")
}
var updateChannels *fleet.OrbitUpdateChannels
@ -396,6 +402,46 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
}, nil
}
func (svc *Service) setDiskEncryptionNotifications(
ctx context.Context,
notifs *fleet.OrbitConfigNotifications,
host *fleet.Host,
appConfig *fleet.AppConfig,
diskEncryptionConfigured bool,
isConnectedToFleetMDM bool,
mdmInfo *fleet.HostMDM,
) error {
anyMDMConfigured := appConfig.MDM.EnabledAndConfigured || appConfig.MDM.WindowsEnabledAndConfigured
if !anyMDMConfigured ||
!isConnectedToFleetMDM ||
!host.IsOsqueryEnrolled() ||
!diskEncryptionConfigured {
return nil
}
encryptionKey, err := svc.ds.GetHostDiskEncryptionKey(ctx, host.ID)
if err != nil {
if !fleet.IsNotFound(err) {
return ctxerr.Wrap(ctx, err, "fetching host disk encryption key")
}
}
switch host.FleetPlatform() {
case "darwin":
notifs.RotateDiskEncryptionKey = encryptionKey.Decryptable != nil && !*encryptionKey.Decryptable
case "windows":
isServer := mdmInfo != nil && mdmInfo.IsServer
needsEncryption := host.DiskEncryptionEnabled != nil && !*host.DiskEncryptionEnabled
keyWasDecrypted := encryptionKey != nil && encryptionKey.Decryptable != nil && *encryptionKey.Decryptable
encryptedWithoutKey := host.DiskEncryptionEnabled != nil && *host.DiskEncryptionEnabled && !keyWasDecrypted
notifs.EnforceBitLockerEncryption = !isServer &&
mdmInfo != nil &&
(needsEncryption || encryptedWithoutKey)
}
return nil
}
// filterExtensionsForHost filters a extensions configuration depending on the host platform and label membership.
//
// If all extensions are filtered, then it returns (nil, nil) (Orbit expects empty extensions if there

View file

@ -225,6 +225,19 @@ make nudge-app-tar-gz version=1.1.10.81462 out-path=.
fleetctl updates add --target /path/to/macos/nudge.app.tar.gz --platform macos --name nudge --version 1.1.10.81462 -t edge
```
#### Releasing `Escrow Buddy` to `stable`
> `releaser.sh` doesn't support `Escrow Buddy` yet.
> macOS only component
The `Escrow Buddy` pkg installer can be generated by running:
```sh
make escrow-buddy-pkg version=1.0.0 out-path=.
```
```sh
fleetctl updates add --target /path/to/escrowBuddy.pkg --platform macos --name escrowBuddy --version 1.0.0 -t stable
```
#### Updating timestamp
```sh

View file

@ -28,8 +28,7 @@ SYSTEMS=${SYSTEMS:-macos linux linux-arm64 windows}
echo "Generating packages for $SYSTEMS"
NUDGE_VERSION=stable
SWIFT_DIALOG_MACOS_APP_VERSION=2.2.1
SWIFT_DIALOG_MACOS_APP_BUILD_VERSION=4591
ESCROW_BUDDY_PKG_VERSION=1.0.0
if [[ -z "$OSQUERY_VERSION" ]]; then
OSQUERY_VERSION=5.12.2
@ -168,6 +167,20 @@ for system in $SYSTEMS; do
rm swiftDialog.app.tar.gz
fi
# Add Escrow Buddy on macos (if enabled).
if [[ $system == "macos" && -n "$ESCROW_BUDDY" ]]; then
make escrow-buddy-pkg version=$ESCROW_BUDDY_PKG_VERSION out-path=.
./build/fleetctl updates add \
--path $TUF_PATH \
--target escrowBuddy.pkg \
--platform macos \
--name escrowBuddy \
--version 42.0.0 -t 42.0 -t 42 -t stable
rm escrowBuddy.pkg
fi
# Add Fleet Desktop application on windows (if enabled).
if [[ $system == "windows" && -n "$FLEET_DESKTOP" ]]; then
FLEET_DESKTOP_VERSION=42.0.0 \