Skip to content

Commit

Permalink
Add LUKS escrow trigger and orbit config endpoints, persist/retrieve …
Browse files Browse the repository at this point in the history
…LUKS passphrase (#23763)

#23583, #23584
# 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] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [ ] Manual QA for all new/changed functionality -- should be tested
end-to-end

---------

Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
  • Loading branch information
iansltx and Jacob Shandling authored Nov 18, 2024
1 parent 22ff501 commit 9900b73
Show file tree
Hide file tree
Showing 22 changed files with 973 additions and 55 deletions.
48 changes: 48 additions & 0 deletions ee/server/service/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,51 @@ func (svc *Service) GetFleetDesktopSummary(ctx context.Context) (fleet.DesktopSu

return sum, nil
}

func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error {
if svc.ds.IsHostPendingEscrow(ctx, host.ID) {
return nil
}

if err := svc.validateReadyForLinuxEscrow(ctx, host); err != nil {
_ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error())
return err
}

return svc.ds.QueueEscrow(ctx, host.ID)
}

func (svc *Service) validateReadyForLinuxEscrow(ctx context.Context, host *fleet.Host) error {
if !host.IsLUKSSupported() {
return &fleet.BadRequestError{Message: "Host platform does not support key escrow"}
}

ac, err := svc.ds.AppConfig(ctx)
if err != nil {
return err
}

if host.TeamID == nil {
if !ac.MDM.EnableDiskEncryption.Value {
return &fleet.BadRequestError{Message: "Disk encryption is not enabled for hosts not assigned to a team"}
}
} else {
tc, err := svc.ds.TeamMDMConfig(ctx, *host.TeamID)
if err != nil {
return err
}
if !tc.EnableDiskEncryption {
return &fleet.BadRequestError{Message: "Disk encryption is not enabled for this host's team"}
}
}

if host.DiskEncryptionEnabled == nil || !*host.DiskEncryptionEnabled {
return &fleet.BadRequestError{Message: "Host's disk is not encrypted. Please enable disk encryption for this host."}
}

if host.OrbitVersion == nil || !fleet.IsAtLeastVersion(*host.OrbitVersion, fleet.MinOrbitLUKSVersion) {
return &fleet.BadRequestError{Message: "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version."}
}

return svc.ds.AssertHasNoEncryptionKeyStored(ctx, host.ID)
}
53 changes: 53 additions & 0 deletions server/datastore/mysql/hosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -3819,6 +3819,59 @@ ON DUPLICATE KEY UPDATE
return err
}

func (ds *Datastore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error {
if encryptedBase64Passphrase == "" { // should have been caught at service level
return errors.New("blank encrypted passphrase")
}

_, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_disk_encryption_keys
(host_id, base64_encrypted, base64_encrypted_slot_key, client_error, decryptable)
VALUES
(?, ?, ?, '', TRUE)
ON DUPLICATE KEY UPDATE
decryptable = TRUE,
base64_encrypted = VALUES(base64_encrypted),
base64_encrypted_slot_key = VALUES(base64_encrypted_slot_key),
client_error = ''
`, hostID, encryptedBase64Passphrase, encryptedBase64SlotKey)
return err
}
func (ds *Datastore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool {
var pendingEscrowCount uint
_ = sqlx.GetContext(ctx, ds.reader(ctx), &pendingEscrowCount, `
SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND reset_requested = TRUE`, hostID)
return pendingEscrowCount > 0
}
func (ds *Datastore) ClearPendingEscrow(ctx context.Context, hostID uint) error {
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE host_disk_encryption_keys SET reset_requested = FALSE WHERE host_id = ?`, hostID)
return err
}
func (ds *Datastore) ReportEscrowError(ctx context.Context, hostID uint, errorMessage string) error {
_, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_disk_encryption_keys
(host_id, base64_encrypted, client_error) VALUES (?, '', ?) ON DUPLICATE KEY UPDATE client_error = VALUES(client_error)
`, hostID, errorMessage)
return err
}
func (ds *Datastore) QueueEscrow(ctx context.Context, hostID uint) error {
_, err := ds.writer(ctx).ExecContext(ctx, `
INSERT INTO host_disk_encryption_keys
(host_id, base64_encrypted, reset_requested) VALUES (?, '', TRUE) ON DUPLICATE KEY UPDATE reset_requested = TRUE
`, hostID)
return err
}
func (ds *Datastore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error {
var hasKeyCount uint
err := sqlx.GetContext(ctx, ds.reader(ctx), &hasKeyCount, `
SELECT COUNT(*) FROM host_disk_encryption_keys WHERE host_id = ? AND base64_encrypted != ''`, hostID)
if hasKeyCount > 0 {
return &fleet.BadRequestError{Message: "Key has already been escrowed for this host"}
}

return err
}

func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
// NOTE(mna): currently we only verify encryption keys for macOS,
// Windows/bitlocker uses a different approach where orbit sends the
Expand Down
102 changes: 102 additions & 0 deletions server/datastore/mysql/hosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ func TestHosts(t *testing.T) {
{"SetOrUpdateHostDiskEncryptionKeys", testHostsSetOrUpdateHostDisksEncryptionKey},
{"SetHostsDiskEncryptionKeyStatus", testHostsSetDiskEncryptionKeyStatus},
{"GetUnverifiedDiskEncryptionKeys", testHostsGetUnverifiedDiskEncryptionKeys},
{"LUKS", testLUKSDatastoreFunctions},
{"EnrollOrbit", testHostsEnrollOrbit},
{"EnrollUpdatesMissingInfo", testHostsEnrollUpdatesMissingInfo},
{"EncryptionKeyRawDecryption", testHostsEncryptionKeyRawDecryption},
Expand Down Expand Up @@ -7807,6 +7808,107 @@ func checkEncryptionKeyStatus(t *testing.T, ds *Datastore, hostID uint, expected
require.Equal(t, expectedDecryptable, got.Decryptable)
}

func testLUKSDatastoreFunctions(t *testing.T, ds *Datastore) {
ctx := context.Background()

host1, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("1"),
UUID: "1",
OsqueryHostID: ptr.String("1"),
Hostname: "foo.local",
PrimaryIP: "192.168.1.1",
PrimaryMac: "30-65-EC-6F-C4-58",
})
require.NoError(t, err)
host2, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("2"),
UUID: "2",
OsqueryHostID: ptr.String("2"),
Hostname: "foo.local2",
PrimaryIP: "192.168.1.2",
PrimaryMac: "30-65-EC-6F-C4-59",
})
require.NoError(t, err)
host3, err := ds.NewHost(ctx, &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now(),
NodeKey: ptr.String("3"),
UUID: "3",
OsqueryHostID: ptr.String("3"),
Hostname: "foo.local3",
PrimaryIP: "192.168.1.3",
PrimaryMac: "30-65-EC-6F-C4-60",
})
require.NoError(t, err)

// queue shows as pending
require.False(t, ds.IsHostPendingEscrow(ctx, host1.ID))
err = ds.QueueEscrow(ctx, host1.ID)
require.NoError(t, err)
require.False(t, ds.IsHostPendingEscrow(ctx, host2.ID))
require.True(t, ds.IsHostPendingEscrow(ctx, host1.ID))

// clear removes pending
err = ds.QueueEscrow(ctx, host2.ID)
require.NoError(t, err)
err = ds.ClearPendingEscrow(ctx, host1.ID)
require.NoError(t, err)
require.False(t, ds.IsHostPendingEscrow(ctx, host1.ID))
require.True(t, ds.IsHostPendingEscrow(ctx, host2.ID))

// report escrow error does not remove pending
err = ds.ReportEscrowError(ctx, host2.ID, "this broke")
require.NoError(t, err)
require.True(t, ds.IsHostPendingEscrow(ctx, host2.ID))
// TODO confirm error was persisted

// assert no key stored on hosts with varying no-key-stored states
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID))

// no change when blank key attempted to save
err = ds.SaveLUKSData(ctx, host1.ID, "", "")
require.Error(t, err)
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))

// persists with just passphrase
err = ds.SaveLUKSData(ctx, host1.ID, "foobar", "")
require.NoError(t, err)
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
require.NoError(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
key, err := ds.GetHostDiskEncryptionKey(ctx, host1.ID)
require.NoError(t, err)
require.Equal(t, "foobar", key.Base64Encrypted)

// persists with passphrase and slot key
err = ds.SaveLUKSData(ctx, host2.ID, "bazqux", "fuzzmuffin")
require.NoError(t, err)
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host1.ID))
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host2.ID))
key, err = ds.GetHostDiskEncryptionKey(ctx, host2.ID)
require.NoError(t, err)
require.Equal(t, "bazqux", key.Base64Encrypted)

// persists when host hasn't had anything queued
err = ds.SaveLUKSData(ctx, host3.ID, "newstuff", "")
require.NoError(t, err)
require.Error(t, ds.AssertHasNoEncryptionKeyStored(ctx, host3.ID))
key, err = ds.GetHostDiskEncryptionKey(ctx, host3.ID)
require.NoError(t, err)
require.Equal(t, "newstuff", key.Base64Encrypted)
}

func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
host, err := ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package tables

import (
"database/sql"
"fmt"
)

func init() {
MigrationClient.AddMigration(Up_20241116233322, Down_20241116233322)
}

func Up_20241116233322(tx *sql.Tx) error {
_, err := tx.Exec(`ALTER TABLE host_disk_encryption_keys ADD COLUMN base64_encrypted_slot_key VARCHAR(255) NOT NULL DEFAULT '' AFTER base64_encrypted`)
if err != nil {
return fmt.Errorf("failed to add base64_encrypted_slot_key to host_disk_encryption_keys: %w", err)
}

return nil
}

func Down_20241116233322(tx *sql.Tx) error {
return nil
}
Loading

0 comments on commit 9900b73

Please sign in to comment.