From 9900b73b7437155f46158c33f882d50e072a0e93 Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Mon, 18 Nov 2024 16:44:25 -0600 Subject: [PATCH] Add LUKS escrow trigger and orbit config endpoints, persist/retrieve LUKS passphrase (#23763) #23583, #23584 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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 --- ee/server/service/devices.go | 48 +++++ server/datastore/mysql/hosts.go | 53 +++++ server/datastore/mysql/hosts_test.go | 102 +++++++++ ...3322_AddSlotKeyToHostDiskEncryptionKeys.go | 23 +++ server/datastore/mysql/schema.sql | 5 +- server/fleet/capabilities.go | 14 +- server/fleet/datastore.go | 11 +- server/fleet/hosts.go | 8 +- server/fleet/orbit.go | 3 + server/fleet/service.go | 5 + server/fleet/utils.go | 26 +++ server/mdm/apple/gdmf/api.go | 3 +- server/mdm/apple/util.go | 19 -- server/mdm/mdm.go | 54 +++++ server/mock/datastore_mock.go | 78 ++++++- server/service/devices.go | 37 ++++ server/service/devices_test.go | 109 ++++++++++ server/service/handler.go | 6 + server/service/hosts.go | 68 ++++-- server/service/hosts_test.go | 68 ++++++ server/service/orbit.go | 93 +++++++++ server/service/orbit_test.go | 195 +++++++++++++++++- 22 files changed, 973 insertions(+), 55 deletions(-) create mode 100644 server/datastore/mysql/migrations/tables/20241116233322_AddSlotKeyToHostDiskEncryptionKeys.go diff --git a/ee/server/service/devices.go b/ee/server/service/devices.go index 7c3b580e92b3..9fa82d0e556c 100644 --- a/ee/server/service/devices.go +++ b/ee/server/service/devices.go @@ -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) +} diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go index ee189ebef478..27f1b778c1f5 100644 --- a/server/datastore/mysql/hosts.go +++ b/server/datastore/mysql/hosts.go @@ -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 diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go index 4e0269ad3588..9f3d758e81ef 100644 --- a/server/datastore/mysql/hosts_test.go +++ b/server/datastore/mysql/hosts_test.go @@ -155,6 +155,7 @@ func TestHosts(t *testing.T) { {"SetOrUpdateHostDiskEncryptionKeys", testHostsSetOrUpdateHostDisksEncryptionKey}, {"SetHostsDiskEncryptionKeyStatus", testHostsSetDiskEncryptionKeyStatus}, {"GetUnverifiedDiskEncryptionKeys", testHostsGetUnverifiedDiskEncryptionKeys}, + {"LUKS", testLUKSDatastoreFunctions}, {"EnrollOrbit", testHostsEnrollOrbit}, {"EnrollUpdatesMissingInfo", testHostsEnrollUpdatesMissingInfo}, {"EncryptionKeyRawDecryption", testHostsEncryptionKeyRawDecryption}, @@ -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(), diff --git a/server/datastore/mysql/migrations/tables/20241116233322_AddSlotKeyToHostDiskEncryptionKeys.go b/server/datastore/mysql/migrations/tables/20241116233322_AddSlotKeyToHostDiskEncryptionKeys.go new file mode 100644 index 000000000000..0642229cb2db --- /dev/null +++ b/server/datastore/mysql/migrations/tables/20241116233322_AddSlotKeyToHostDiskEncryptionKeys.go @@ -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 +} diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql index e9740f5e9d2d..5acd57f91251 100644 --- a/server/datastore/mysql/schema.sql +++ b/server/datastore/mysql/schema.sql @@ -303,6 +303,7 @@ CREATE TABLE `host_device_auth` ( CREATE TABLE `host_disk_encryption_keys` ( `host_id` int unsigned NOT NULL, `base64_encrypted` text COLLATE utf8mb4_unicode_ci NOT NULL, + `base64_encrypted_slot_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `decryptable` tinyint(1) DEFAULT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, @@ -1102,9 +1103,9 @@ CREATE TABLE `migration_status_tables` ( `tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `id` (`id`) -) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=330 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=331 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; /*!40101 SET character_set_client = @saved_cs_client */; -INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'); +INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'); /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; CREATE TABLE `mobile_device_management_solutions` ( diff --git a/server/fleet/capabilities.go b/server/fleet/capabilities.go index 7da6f3620522..d8abea773166 100644 --- a/server/fleet/capabilities.go +++ b/server/fleet/capabilities.go @@ -80,6 +80,9 @@ const ( CapabilityEndUserEmail Capability = "end_user_email" // CapabilityEscrowBuddy allows to use Escrow Buddy to rotate FileVault keys CapabilityEscrowBuddy Capability = "escrow_buddy" + // CapabilityLinuxDiskEncryptionEscrow denotes the ability of the server to escrow Ubuntu and Fedora disk + // encryption LUKS passphrases + CapabilityLinuxDiskEncryptionEscrow Capability = "linux_disk_encryption_escrow" // CapabilitySetupExperience denotes the ability of the server to support // installing software and running a script during macOS ADE enrollment, and // the ability of the client to show the corresponding UI to support that @@ -89,11 +92,12 @@ const ( func GetServerOrbitCapabilities() CapabilityMap { return CapabilityMap{ - CapabilityOrbitEndpoints: {}, - CapabilityTokenRotation: {}, - CapabilityEndUserEmail: {}, - CapabilityEscrowBuddy: {}, - CapabilitySetupExperience: {}, + CapabilityOrbitEndpoints: {}, + CapabilityTokenRotation: {}, + CapabilityEndUserEmail: {}, + CapabilityEscrowBuddy: {}, + CapabilityLinuxDiskEncryptionEscrow: {}, + CapabilitySetupExperience: {}, } } diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index 2cfaf6deb3ff..12aeeacf2feb 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -898,19 +898,28 @@ type Datastore interface { // GetHostEmails returns the emails associated with the provided host for a given source, such as "google_chrome_profiles" GetHostEmails(ctx context.Context, hostUUID string, source string) ([]string, error) SetOrUpdateHostDisksSpace(ctx context.Context, hostID uint, gigsAvailable, percentAvailable, gigsTotal float64) error + SetOrUpdateHostDisksEncryption(ctx context.Context, hostID uint, encrypted bool) error // SetOrUpdateHostDiskEncryptionKey sets the base64, encrypted key for // a host SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID uint, encryptedBase64Key, clientError string, decryptable *bool) error + // SaveLUKSData sets base64'd encrypted LUKS passphrase and slot key data for a host + SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error + // GetUnverifiedDiskEncryptionKeys returns all the encryption keys that // are collected but their decryptable status is not known yet (ie: // we're able to decrypt the key using a private key in the server) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]HostDiskEncryptionKey, error) // SetHostsDiskEncryptionKeyStatus sets the encryptable status for the set // of encription keys provided - SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error + SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error // GetHostDiskEncryptionKey returns the encryption key information for a given host GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*HostDiskEncryptionKey, error) + IsHostPendingEscrow(ctx context.Context, hostID uint) bool + ClearPendingEscrow(ctx context.Context, hostID uint) error + ReportEscrowError(ctx context.Context, hostID uint, err string) error + QueueEscrow(ctx context.Context, hostID uint) error + AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error // GetHostCertAssociationsToExpire retrieves host certificate // associations that are close to expire and don't have a renewal in diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go index e402af547218..407af288f825 100644 --- a/server/fleet/hosts.go +++ b/server/fleet/hosts.go @@ -322,7 +322,7 @@ type Host struct { // DiskEncryptionEnabled is only returned by GET /host/{id} and so is not // exportable as CSV (which is the result of List Hosts endpoint). It is - // a *bool because for Linux we set it to NULL and omit it from the JSON + // a *bool because for some Linux we set it to NULL and omit it from the JSON // response if the host does not have disk encryption enabled. It is also // omitted if we don't have encryption information yet. DiskEncryptionEnabled *bool `json:"disk_encryption_enabled,omitempty" db:"disk_encryption_enabled" csv:"-"` @@ -682,6 +682,12 @@ func (h *Host) IsDEPAssignedToFleet() bool { return h.DEPAssignedToFleet != nil && *h.DEPAssignedToFleet } +// IsLUKSSupported returns true if the host's platform is Linux and running +// one of the supported OS versions. +func (h *Host) IsLUKSSupported() bool { + return h.Platform == "ubuntu" || strings.Contains(h.OSVersion, "Fedora") // fedora h.Platform reports as "rhel" +} + // IsEligibleForWindowsMDMUnenrollment returns true if the host must be // unenrolled from Fleet's Windows MDM (if it MDM was disabled). func (h *Host) IsEligibleForWindowsMDMUnenrollment(isConnectedToFleetMDM bool) bool { diff --git a/server/fleet/orbit.go b/server/fleet/orbit.go index e3c0c7b0db3e..6c06a963e263 100644 --- a/server/fleet/orbit.go +++ b/server/fleet/orbit.go @@ -40,6 +40,9 @@ type OrbitConfigNotifications struct { // RunSetupExperience indicates whether or not Orbit should run the Fleet setup experience // during macOS Setup Assistant. RunSetupExperience bool `json:"run_setup_experience,omitempty"` + + // RunDiskEncryptionEscrow tells Orbit to prompt the end user to escrow disk encryption data + RunDiskEncryptionEscrow bool `json:"run_disk_encryption_escrow,omitempty"` } type OrbitConfig struct { diff --git a/server/fleet/service.go b/server/fleet/service.go index 285ddac74d7a..79b3f19222e6 100644 --- a/server/fleet/service.go +++ b/server/fleet/service.go @@ -394,6 +394,7 @@ type Service interface { GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error) HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error) + EscrowLUKSData(ctx context.Context, passphrase string, slotKey string, clientError string) error // AddLabelsToHost adds the given label names to the host's label membership. // @@ -964,6 +965,8 @@ type Service interface { GetMDMManualEnrollmentProfile(ctx context.Context) ([]byte, error) + TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *Host) error + // CheckMDMAppleEnrollmentWithMinimumOSVersion checks if the minimum OS version is met for a MDM enrollment CheckMDMAppleEnrollmentWithMinimumOSVersion(ctx context.Context, m *MDMAppleMachineInfo) (*MDMAppleSoftwareUpdateRequired, error) @@ -1169,4 +1172,6 @@ const ( BatchSetSoftwareInstallersStatusCompleted = "completed" // BatchSetSoftwareInstallerStatusFailed is the value returned for a failed BatchSetSoftwareInstallers operation. BatchSetSoftwareInstallersStatusFailed = "failed" + // MinOrbitLUKSVersion is the earliest version of Orbit that can escrow LUKS passphrases + MinOrbitLUKSVersion = "1.36.0" ) diff --git a/server/fleet/utils.go b/server/fleet/utils.go index ddab26e8e797..fb286b56a764 100644 --- a/server/fleet/utils.go +++ b/server/fleet/utils.go @@ -6,6 +6,7 @@ import ( "io" "strings" + "github.com/Masterminds/semver" "github.com/fatih/color" "golang.org/x/text/unicode/norm" ) @@ -65,3 +66,28 @@ func Preprocess(input string) string { // Normalize Unicode characters. return norm.NFC.String(input) } + +// CompareVersions returns an integer comparing two versions according to semantic version +// precedence. The result will be 0 if a == b, -1 if a < b, or +1 if a > b. +// An invalid semantic version string is considered less than a valid one. All invalid semantic +// version strings compare equal to each other. +func CompareVersions(a string, b string) int { + verA, errA := semver.NewVersion(a) + verB, errB := semver.NewVersion(b) + switch { + case errA != nil && errB != nil: + return 0 + case errA != nil: + return -1 + case errB != nil: + return 1 + default: + return verA.Compare(verB) + } +} + +// IsAtLeastVersion returns whether currentVersion is at least minimumVersion, using semantics +// of CompareVersions for version validity +func IsAtLeastVersion(currentVersion string, minimumVersion string) bool { + return CompareVersions(currentVersion, minimumVersion) >= 0 +} diff --git a/server/mdm/apple/gdmf/api.go b/server/mdm/apple/gdmf/api.go index ee8c671814cd..d969c2580081 100644 --- a/server/mdm/apple/gdmf/api.go +++ b/server/mdm/apple/gdmf/api.go @@ -15,7 +15,6 @@ import ( "github.com/cenkalti/backoff" "github.com/fleetdm/fleet/v4/pkg/fleethttp" "github.com/fleetdm/fleet/v4/server/fleet" - apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" ) const baseURL = "https://gdmf.apple.com/v2/pmv" @@ -91,7 +90,7 @@ func GetLatestOSVersion(device fleet.MDMAppleMachineInfo) (*Asset, error) { latestIdx = i // first match found, update the index continue } - if apple_mdm.CompareVersions(assetSet[latestIdx].ProductVersion, s.ProductVersion) < 0 { + if fleet.CompareVersions(assetSet[latestIdx].ProductVersion, s.ProductVersion) < 0 { latestIdx = i // found a later version, update the index } } diff --git a/server/mdm/apple/util.go b/server/mdm/apple/util.go index 88267f953fa3..200e1d4f39ad 100644 --- a/server/mdm/apple/util.go +++ b/server/mdm/apple/util.go @@ -124,22 +124,3 @@ func IsLessThanVersion(current string, target string) (bool, error) { return cv.LessThan(tv), nil } - -// CompareVersions returns an integer comparing two versions according to semantic version -// precedence. The result will be 0 if a == b, -1 if a < b, or +1 if a > b. -// An invalid semantic version string is considered less than a valid one. All invalid semantic -// version strings compare equal to each other. -func CompareVersions(a string, b string) int { - verA, errA := semver.NewVersion(a) - verB, errB := semver.NewVersion(b) - switch { - case errA != nil && errB != nil: - return 0 - case errA != nil: - return -1 - case errB != nil: - return 1 - default: - return verA.Compare(verB) - } -} diff --git a/server/mdm/mdm.go b/server/mdm/mdm.go index 5aaae483d8e6..cf800dcfa690 100644 --- a/server/mdm/mdm.go +++ b/server/mdm/mdm.go @@ -3,8 +3,13 @@ package mdm import ( "bytes" "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" "crypto/x509" "encoding/base64" + "fmt" + "io" "github.com/smallstep/pkcs7" ) @@ -81,6 +86,55 @@ func GuessProfileExtension(profile []byte) string { } } +func EncryptAndEncode(plainText string, symmetricKey string) (string, error) { + block, err := aes.NewCipher([]byte(symmetricKey)) + if err != nil { + return "", fmt.Errorf("create new cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("create new gcm: %w", err) + } + + nonce := make([]byte, aesGCM.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("generate nonce: %w", err) + } + + return base64.StdEncoding.EncodeToString(aesGCM.Seal(nonce, nonce, []byte(plainText), nil)), nil +} + +func DecodeAndDecrypt(base64CipherText string, symmetricKey string) (string, error) { + encrypted, err := base64.StdEncoding.DecodeString(base64CipherText) + if err != nil { + return "", fmt.Errorf("base64 decode: %w", err) + } + + block, err := aes.NewCipher([]byte(symmetricKey)) + if err != nil { + return "", fmt.Errorf("create new cipher: %w", err) + } + + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("create new gcm: %w", err) + } + + // Get the nonce size + nonceSize := aesGCM.NonceSize() + + // Extract the nonce from the encrypted data + nonce, ciphertext := encrypted[:nonceSize], encrypted[nonceSize:] + + decrypted, err := aesGCM.Open(nil, nonce, ciphertext, nil) + if err != nil { + return "", fmt.Errorf("decrypting: %w", err) + } + + return string(decrypted), nil +} + const ( // FleetdConfigProfileName is the value for the PayloadDisplayName used by // fleetd to read configuration values from the system. diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index ad49d7a6aa28..169e64cde4c0 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -639,12 +639,24 @@ type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, e type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string, clientError string, decryptable *bool) error +type SaveLUKSDataFunc func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error + type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) -type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error +type SetHostsDiskEncryptionKeyStatusFunc func(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error type GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) +type IsHostPendingEscrowFunc func(ctx context.Context, hostID uint) bool + +type ClearPendingEscrowFunc func(ctx context.Context, hostID uint) error + +type ReportEscrowErrorFunc func(ctx context.Context, hostID uint, err string) error + +type QueueEscrowFunc func(ctx context.Context, hostID uint) error + +type AssertHasNoEncryptionKeyStoredFunc func(ctx context.Context, hostID uint) 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 @@ -2077,6 +2089,9 @@ type DataStore struct { SetOrUpdateHostDiskEncryptionKeyFunc SetOrUpdateHostDiskEncryptionKeyFunc SetOrUpdateHostDiskEncryptionKeyFuncInvoked bool + SaveLUKSDataFunc SaveLUKSDataFunc + SaveLUKSDataFuncInvoked bool + GetUnverifiedDiskEncryptionKeysFunc GetUnverifiedDiskEncryptionKeysFunc GetUnverifiedDiskEncryptionKeysFuncInvoked bool @@ -2086,6 +2101,21 @@ type DataStore struct { GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFuncInvoked bool + IsHostPendingEscrowFunc IsHostPendingEscrowFunc + IsHostPendingEscrowFuncInvoked bool + + ClearPendingEscrowFunc ClearPendingEscrowFunc + ClearPendingEscrowFuncInvoked bool + + ReportEscrowErrorFunc ReportEscrowErrorFunc + ReportEscrowErrorFuncInvoked bool + + QueueEscrowFunc QueueEscrowFunc + QueueEscrowFuncInvoked bool + + AssertHasNoEncryptionKeyStoredFunc AssertHasNoEncryptionKeyStoredFunc + AssertHasNoEncryptionKeyStoredFuncInvoked bool + GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFunc GetHostCertAssociationsToExpireFuncInvoked bool @@ -5008,6 +5038,13 @@ func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key, clientError, decryptable) } +func (s *DataStore) SaveLUKSData(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error { + s.mu.Lock() + s.SaveLUKSDataFuncInvoked = true + s.mu.Unlock() + return s.SaveLUKSDataFunc(ctx, hostID, encryptedBase64Passphrase, encryptedBase64SlotKey) +} + func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) { s.mu.Lock() s.GetUnverifiedDiskEncryptionKeysFuncInvoked = true @@ -5015,11 +5052,11 @@ func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]flee return s.GetUnverifiedDiskEncryptionKeysFunc(ctx) } -func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error { +func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, decryptable bool, threshold time.Time) error { s.mu.Lock() s.SetHostsDiskEncryptionKeyStatusFuncInvoked = true s.mu.Unlock() - return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, encryptable, threshold) + return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, decryptable, threshold) } func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) { @@ -5029,6 +5066,41 @@ func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) ( return s.GetHostDiskEncryptionKeyFunc(ctx, hostID) } +func (s *DataStore) IsHostPendingEscrow(ctx context.Context, hostID uint) bool { + s.mu.Lock() + s.IsHostPendingEscrowFuncInvoked = true + s.mu.Unlock() + return s.IsHostPendingEscrowFunc(ctx, hostID) +} + +func (s *DataStore) ClearPendingEscrow(ctx context.Context, hostID uint) error { + s.mu.Lock() + s.ClearPendingEscrowFuncInvoked = true + s.mu.Unlock() + return s.ClearPendingEscrowFunc(ctx, hostID) +} + +func (s *DataStore) ReportEscrowError(ctx context.Context, hostID uint, err string) error { + s.mu.Lock() + s.ReportEscrowErrorFuncInvoked = true + s.mu.Unlock() + return s.ReportEscrowErrorFunc(ctx, hostID, err) +} + +func (s *DataStore) QueueEscrow(ctx context.Context, hostID uint) error { + s.mu.Lock() + s.QueueEscrowFuncInvoked = true + s.mu.Unlock() + return s.QueueEscrowFunc(ctx, hostID) +} + +func (s *DataStore) AssertHasNoEncryptionKeyStored(ctx context.Context, hostID uint) error { + s.mu.Lock() + s.AssertHasNoEncryptionKeyStoredFuncInvoked = true + s.mu.Unlock() + return s.AssertHasNoEncryptionKeyStoredFunc(ctx, hostID) +} + func (s *DataStore) GetHostCertAssociationsToExpire(ctx context.Context, expiryDays int, limit int) ([]fleet.SCEPIdentityAssociation, error) { s.mu.Lock() s.GetHostCertAssociationsToExpireFuncInvoked = true diff --git a/server/service/devices.go b/server/service/devices.go index 3ae57851b102..187e168bf48a 100644 --- a/server/service/devices.go +++ b/server/service/devices.go @@ -609,6 +609,43 @@ func (svc *Service) TriggerMigrateMDMDevice(ctx context.Context, host *fleet.Hos return fleet.ErrMissingLicense } +//////////////////////////////////////////////////////////////////////////////// +// Trigger linux key escrow +//////////////////////////////////////////////////////////////////////////////// + +type triggerLinuxDiskEncryptionEscrowRequest struct { + Token string `url:"token"` +} + +func (r *triggerLinuxDiskEncryptionEscrowRequest) deviceAuthToken() string { + return r.Token +} + +type triggerLinuxDiskEncryptionEscrowResponse struct { + Err error `json:"error,omitempty"` +} + +func (r triggerLinuxDiskEncryptionEscrowResponse) error() error { return r.Err } + +func (r triggerLinuxDiskEncryptionEscrowResponse) Status() int { return http.StatusNoContent } + +func triggerLinuxDiskEncryptionEscrowEndpoint(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 triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil + } + + if err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host); err != nil { + return triggerLinuxDiskEncryptionEscrowResponse{Err: err}, nil + } + return triggerLinuxDiskEncryptionEscrowResponse{}, nil +} + +func (svc *Service) TriggerLinuxDiskEncryptionEscrow(ctx context.Context, host *fleet.Host) error { + return fleet.ErrMissingLicense +} + //////////////////////////////////////////////////////////////////////////////// // Get Current Device's Software //////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/devices_test.go b/server/service/devices_test.go index 1100683be4fb..774a941ca06f 100644 --- a/server/service/devices_test.go +++ b/server/service/devices_test.go @@ -3,10 +3,12 @@ package service import ( "context" "database/sql" + "errors" "fmt" "testing" "time" + "github.com/fleetdm/fleet/v4/pkg/optjson" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" @@ -475,3 +477,110 @@ func TestGetFleetDesktopSummary(t *testing.T) { }) } + +func TestTriggerLinuxDiskEncryptionEscrow(t *testing.T) { + t.Run("unavailable in Fleet Free", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{SkipCreateTestUsers: true}) + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, &fleet.Host{ID: 1}) + require.ErrorIs(t, err, fleet.ErrMissingLicense) + }) + + t.Run("no-op on already pending", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true}) + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return true + } + + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, &fleet.Host{ID: 1}) + require.NoError(t, err) + require.True(t, ds.IsHostPendingEscrowFuncInvoked) + }) + + t.Run("validation failures", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true}) + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } + var reportedErrors []string + host := &fleet.Host{ID: 1, Platform: "rhel", OSVersion: "Red Hat Enterprise Linux 9.0.0"} + ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error { + require.Equal(t, hostID, host.ID) + reportedErrors = append(reportedErrors, err) + return nil + } + + // invalid platform + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host platform does not support key escrow") + require.True(t, ds.IsHostPendingEscrowFuncInvoked) + + // valid platform, no-team, encryption not enabled + host.OSVersion = "Fedora 32.0.0" + appConfig := &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(false)}} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appConfig, nil + } + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Disk encryption is not enabled for hosts not assigned to a team") + + // valid platform, team, encryption not enabled + host.TeamID = ptr.Uint(1) + teamConfig := &fleet.TeamMDM{} + ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { + require.Equal(t, uint(1), teamID) + return teamConfig, nil + } + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Disk encryption is not enabled for this host's team") + + // valid platform, team, host disk is not encrypted or unknown encryption state + teamConfig = &fleet.TeamMDM{EnableDiskEncryption: true} + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + host.DiskEncryptionEnabled = ptr.Bool(false) + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host's disk is not encrypted. Please enable disk encryption for this host.") + + // Orbit version is too old + host.DiskEncryptionEnabled = ptr.Bool(true) + host.OrbitVersion = ptr.String("1.35.1") + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "Host's Orbit version does not support this feature. Please upgrade Orbit to the latest version.") + + // Encryption key is already escrowed + host.OrbitVersion = ptr.String(fleet.MinOrbitLUKSVersion) + ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error { + return errors.New("encryption key is already escrowed") + } + err = svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.Error(t, err, "encryption key is already escrowed") + + require.Len(t, reportedErrors, 7) + }) + + t.Run("validation success", func(t *testing.T) { + ds := new(mock.Store) + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: &fleet.LicenseInfo{Tier: fleet.TierPremium}, SkipCreateTestUsers: true}) + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}}, nil + } + ds.AssertHasNoEncryptionKeyStoredFunc = func(ctx context.Context, hostID uint) error { + return nil + } + host := &fleet.Host{ID: 1, Platform: "ubuntu", DiskEncryptionEnabled: ptr.Bool(true), OrbitVersion: ptr.String(fleet.MinOrbitLUKSVersion)} + ds.QueueEscrowFunc = func(ctx context.Context, hostID uint) error { + require.Equal(t, uint(1), hostID) + return nil + } + + err := svc.TriggerLinuxDiskEncryptionEscrow(ctx, host) + require.NoError(t, err) + require.True(t, ds.QueueEscrowFuncInvoked) + }) +} diff --git a/server/service/handler.go b/server/service/handler.go index 4f916f576ba2..147acacf4402 100644 --- a/server/service/handler.go +++ b/server/service/handler.go @@ -837,6 +837,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC errorLimiter.Limit("post_device_migrate_mdm", desktopQuota), ).POST("/api/_version_/fleet/device/{token}/migrate_mdm", migrateMDMDeviceEndpoint, deviceMigrateMDMRequest{}) + de.WithCustomMiddleware( + errorLimiter.Limit("post_device_trigger_linux_escrow", desktopQuota), + ).POST("/api/_version_/fleet/device/{token}/mdm/linux/trigger_escrow", triggerLinuxDiskEncryptionEscrowEndpoint, triggerLinuxDiskEncryptionEscrowRequest{}) + // host-authenticated endpoints he := newHostAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...) @@ -879,6 +883,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC oeWindowsMDM := oe.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyWindowsMDM()) oeWindowsMDM.POST("/api/fleet/orbit/disk_encryption_key", postOrbitDiskEncryptionKeyEndpoint, orbitPostDiskEncryptionKeyRequest{}) + oe.POST("/api/fleet/orbit/luks_data", postOrbitLUKSEndpoint, orbitPostLUKSRequest{}) + // unauthenticated endpoints - most of those are either login-related, // invite-related or host-enrolling. So they typically do some kind of // one-time authentication by verifying that a valid secret token is provided diff --git a/server/service/hosts.go b/server/service/hosts.go index 33e32c438036..b2ee905999f7 100644 --- a/server/service/hosts.go +++ b/server/service/hosts.go @@ -2200,12 +2200,51 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host return nil, err } - // The middleware checks that either Apple or Windows MDM are configured and - // enabled, but here we must check if the specific one is enabled for that - // particular host's platform. + var key *fleet.HostDiskEncryptionKey + if host.IsLUKSSupported() { + if svc.config.Server.PrivateKey == "" { + return nil, ctxerr.Wrap(ctx, errors.New("private key is unavailable"), "getting host encryption key") + } + + key, err = svc.ds.GetHostDiskEncryptionKey(ctx, id) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") + } + if key.Base64Encrypted == "" { + return nil, ctxerr.Wrap(ctx, newNotFoundError(), "host encryption key is not set") + } + + decryptedKey, err := mdm.DecodeAndDecrypt(key.Base64Encrypted, svc.config.Server.PrivateKey) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key") + } + key.DecryptedValue = decryptedKey + } else { + key, err = svc.decryptForMDMPlatform(ctx, host) + if err != nil { + return nil, err + } + } + + err = svc.NewActivity( + ctx, + authz.UserFromContext(ctx), + fleet.ActivityTypeReadHostDiskEncryptionKey{ + HostID: host.ID, + HostDisplayName: host.DisplayName(), + }, + ) + if err != nil { + return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity") + } + + return key, nil +} + +func (svc *Service) decryptForMDMPlatform(ctx context.Context, host *fleet.Host) (*fleet.HostDiskEncryptionKey, error) { + // Here we must check if the appropriate MDM is enabled for that particular host's platform. var decryptCert *tls.Certificate - switch host.FleetPlatform() { - case "windows": + if host.FleetPlatform() == "windows" { if err := svc.VerifyMDMWindowsConfigured(ctx); err != nil { return nil, err } @@ -2216,8 +2255,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host return nil, ctxerr.Wrap(ctx, err, "getting Microsoft WSTEP certificate to decrypt key") } decryptCert = cert - - default: + } else { if err := svc.VerifyMDMAppleConfigured(ctx); err != nil { return nil, err } @@ -2230,7 +2268,7 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host decryptCert = cert } - key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id) + key, err := svc.ds.GetHostDiskEncryptionKey(ctx, host.ID) if err != nil { return nil, ctxerr.Wrap(ctx, err, "getting host encryption key") } @@ -2242,20 +2280,8 @@ func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.Host if err != nil { return nil, ctxerr.Wrap(ctx, err, "decrypt host encryption key") } - key.DecryptedValue = string(decryptedKey) - - err = svc.NewActivity( - ctx, - authz.UserFromContext(ctx), - fleet.ActivityTypeReadHostDiskEncryptionKey{ - HostID: host.ID, - HostDisplayName: host.DisplayName(), - }, - ) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "create read host disk encryption key activity") - } + key.DecryptedValue = string(decryptedKey) return key, nil } diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go index 5b0837cf60c2..ebca688f3ff6 100644 --- a/server/service/hosts_test.go +++ b/server/service/hosts_test.go @@ -19,6 +19,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/viewer" "github.com/fleetdm/fleet/v4/server/datastore/mysql" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple" "github.com/fleetdm/fleet/v4/server/mdm/apple/mobileconfig" "github.com/fleetdm/fleet/v4/server/mdm/nanodep/tokenpki" @@ -1448,6 +1449,73 @@ func TestHostEncryptionKey(t *testing.T) { }) } }) + + t.Run("Linux encryption", func(t *testing.T) { + ds := new(mock.Store) + host := &fleet.Host{ID: 1, Platform: "ubuntu"} + symmetricKey := "this_is_a_32_byte_symmetric_key!" + passphrase := "this_is_a_passphrase" + base64EncryptedKey, err := mdm.EncryptAndEncode(passphrase, symmetricKey) + require.NoError(t, err) + + ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) { + return host, nil + } + + ds.NewActivityFunc = func( + ctx context.Context, user *fleet.User, activity fleet.ActivityDetails, details []byte, createdAt time.Time, + ) error { + return nil + } + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { // needed for new activity + return &fleet.AppConfig{}, nil + } + + // error when no server private key + fleetCfg.Server.PrivateKey = "" + svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err := svc.HostEncryptionKey(ctx, 1) + require.Error(t, err, "private key is unavailable") + require.Nil(t, key) + + // error when key is not set + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{}, nil + } + fleetCfg.Server.PrivateKey = symmetricKey + svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err = svc.HostEncryptionKey(ctx, 1) + require.Error(t, err, "host encryption key is not set") + require.Nil(t, key) + + // error when key is not set + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{ + Base64Encrypted: "thisIsWrong", + Decryptable: ptr.Bool(true), + }, nil + } + svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err = svc.HostEncryptionKey(ctx, 1) + require.Error(t, err, "decrypt host encryption key") + require.Nil(t, key) + + // happy path + ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) { + return &fleet.HostDiskEncryptionKey{ + Base64Encrypted: base64EncryptedKey, + Decryptable: ptr.Bool(true), + }, nil + } + svc, ctx = newTestServiceWithConfig(t, ds, fleetCfg, nil, nil) + ctx = test.UserContext(ctx, test.UserAdmin) + key, err = svc.HostEncryptionKey(ctx, 1) + require.NoError(t, err) + require.Equal(t, passphrase, key.DecryptedValue) + }) } func TestHostMDMProfileDetail(t *testing.T) { diff --git a/server/service/orbit.go b/server/service/orbit.go index be10578dc5e6..7b237544ad2e 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -16,6 +16,7 @@ import ( "github.com/fleetdm/fleet/v4/server/contexts/license" "github.com/fleetdm/fleet/v4/server/contexts/logging" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/worker" @@ -287,6 +288,8 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro } } + notifs.RunDiskEncryptionEscrow = svc.ds.IsHostPendingEscrow(ctx, host.ID) + pendingInstalls, err := svc.ds.ListPendingSoftwareInstalls(ctx, host.ID) if err != nil { return fleet.OrbitConfig{}, err @@ -368,6 +371,11 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro updateChannels = &uc } + // only unset this flag once we know there were no errors so this notification will be picked up by the agent + if notifs.RunDiskEncryptionEscrow { + _ = svc.ds.ClearPendingEscrow(ctx, host.ID) + } + return fleet.OrbitConfig{ ScriptExeTimeout: opts.ScriptExecutionTimeout, Flags: opts.CommandLineStartUpFlags, @@ -438,6 +446,11 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro updateChannels = &uc } + // only unset this flag once we know there were no errors so this notification will be picked up by the agent + if notifs.RunDiskEncryptionEscrow { + _ = svc.ds.ClearPendingEscrow(ctx, host.ID) + } + return fleet.OrbitConfig{ ScriptExeTimeout: opts.ScriptExecutionTimeout, Flags: opts.CommandLineStartUpFlags, @@ -1004,6 +1017,86 @@ func (svc *Service) SetOrUpdateDiskEncryptionKey(ctx context.Context, encryption return nil } +///////////////////////////////////////////////////////////////////////////////// +// Post Orbit LUKS (Linux disk encryption) data +///////////////////////////////////////////////////////////////////////////////// + +type orbitPostLUKSRequest struct { + OrbitNodeKey string `json:"orbit_node_key"` + Passphrase string `json:"passphrase"` + SlotKey string `json:"slot_key"` + ClientError string `json:"client_error"` +} + +// interface implementation required by the OrbitClient +func (r *orbitPostLUKSRequest) setOrbitNodeKey(nodeKey string) { + r.OrbitNodeKey = nodeKey +} + +// interface implementation required by orbit authentication +func (r *orbitPostLUKSRequest) orbitHostNodeKey() string { + return r.OrbitNodeKey +} + +type orbitPostLUKSResponse struct { + Err error `json:"error,omitempty"` +} + +func (r orbitPostLUKSResponse) error() error { return r.Err } +func (r orbitPostLUKSResponse) Status() int { return http.StatusNoContent } + +func postOrbitLUKSEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) { + req := request.(*orbitPostLUKSRequest) + if err := svc.EscrowLUKSData(ctx, req.Passphrase, req.SlotKey, req.ClientError); err != nil { + return orbitPostLUKSResponse{Err: err}, nil + } + return orbitPostLUKSResponse{}, nil +} + +func (svc *Service) EscrowLUKSData(ctx context.Context, passphrase string, slotKey string, clientError string) error { + // this is not a user-authenticated endpoint + svc.authz.SkipAuthorization(ctx) + + host, ok := hostctx.FromContext(ctx) + if !ok { + return newOsqueryError("internal error: missing host from request context") + } + + if clientError != "" { + return svc.ds.ReportEscrowError(ctx, host.ID, clientError) + } + encryptedPassphrase, encryptedSlotKey, err := svc.validateAndEncrypt(ctx, passphrase, slotKey) + if err != nil { + _ = svc.ds.ReportEscrowError(ctx, host.ID, err.Error()) + return err + } + + return svc.ds.SaveLUKSData(ctx, host.ID, encryptedPassphrase, encryptedSlotKey) +} + +func (svc *Service) validateAndEncrypt(ctx context.Context, passphrase string, slotKey string) (string, string, error) { + if passphrase == "" { + return "", "", badRequest("Blank passphrase provided") + } + if svc.config.Server.PrivateKey == "" { + return "", "", newOsqueryError("internal error: missing server private key") + } + + encryptedPassphrase, err := mdm.EncryptAndEncode(passphrase, svc.config.Server.PrivateKey) + if err != nil { + return "", "", ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data") + } + var encryptedSlotKey string + if slotKey != "" { + encryptedSlotKey, err = mdm.EncryptAndEncode(slotKey, svc.config.Server.PrivateKey) + if err != nil { + return "", "", ctxerr.Wrap(ctx, err, "internal error: could not encrypt LUKS data") + } + } + + return encryptedPassphrase, encryptedSlotKey, nil +} + ///////////////////////////////////////////////////////////////////////////////// // Get Orbit pending software installations ///////////////////////////////////////////////////////////////////////////////// diff --git a/server/service/orbit_test.go b/server/service/orbit_test.go index 992d8b057c70..f7c5bfbeed9e 100644 --- a/server/service/orbit_test.go +++ b/server/service/orbit_test.go @@ -4,16 +4,196 @@ import ( "context" "database/sql" "encoding/json" + "errors" "testing" "github.com/fleetdm/fleet/v4/pkg/optjson" + "github.com/fleetdm/fleet/v4/server/config" "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/mdm" "github.com/fleetdm/fleet/v4/server/mock" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/stretchr/testify/require" ) +func TestGetOrbitConfigLinuxEscrow(t *testing.T) { + t.Run("pending escrow sets config flag and clears in DB", func(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + os := &fleet.OperatingSystem{ + Platform: "ubuntu", + Version: "20.04", + } + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + } + + team := fleet.Team{ID: 1} + teamMDM := fleet.TeamMDM{EnableDiskEncryption: true} + ds.TeamMDMConfigFunc = func(ctx context.Context, teamID uint) (*fleet.TeamMDM, error) { + require.Equal(t, team.ID, teamID) + return &teamMDM, nil + } + ds.TeamAgentOptionsFunc = func(ctx context.Context, id uint) (*json.RawMessage, error) { + return ptr.RawMessage(json.RawMessage(`{}`)), nil + } + ds.ListPendingHostScriptExecutionsFunc = func(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) { + return nil, nil + } + ds.ListPendingSoftwareInstallsFunc = func(ctx context.Context, hostID uint) ([]string, error) { + return nil, nil + } + ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { + return true, nil + } + ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { + return nil, nil + } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return true + } + ds.ClearPendingEscrowFunc = func(ctx context.Context, hostID uint) error { + return nil + } + + appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnableDiskEncryption: optjson.SetBool(true)}} + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return appCfg, nil + } + ds.GetHostOperatingSystemFunc = func(ctx context.Context, hostID uint) (*fleet.OperatingSystem, error) { + return os, nil + } + + ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) { + return false, nil + } + + ctx = test.HostContext(ctx, host) + + // no-team + cfg, err := svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.True(t, cfg.Notifications.RunDiskEncryptionEscrow) + require.True(t, ds.ClearPendingEscrowFuncInvoked) + + // with team + ds.ClearPendingEscrowFuncInvoked = false + host.TeamID = ptr.Uint(team.ID) + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.True(t, cfg.Notifications.RunDiskEncryptionEscrow) + require.True(t, ds.ClearPendingEscrowFuncInvoked) + + // ignore clear escrow errors + ds.ClearPendingEscrowFuncInvoked = false + ds.ClearPendingEscrowFunc = func(ctx context.Context, hostID uint) error { + return errors.New("clear pending escrow") + } + cfg, err = svc.GetOrbitConfig(ctx) + require.NoError(t, err) + require.True(t, cfg.Notifications.RunDiskEncryptionEscrow) + require.True(t, ds.ClearPendingEscrowFuncInvoked) + }) +} + +func TestOrbitLUKSDataSave(t *testing.T) { + t.Run("when private key is set", func(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + } + ctx = test.HostContext(ctx, host) + expectedErrorMessage := "There was an error." + ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error { + require.Equal(t, expectedErrorMessage, err) + return nil + } + + // test reporting client errors + err := svc.EscrowLUKSData(ctx, "foo", "bar", expectedErrorMessage) + require.NoError(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + + // blank passphrase + ds.ReportEscrowErrorFuncInvoked = false + expectedErrorMessage = "Blank passphrase provided" + err = svc.EscrowLUKSData(ctx, "", "bar", "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + + ds.ReportEscrowErrorFuncInvoked = false + passphrase, slotKey := "foo", "" + ds.SaveLUKSDataFunc = func(ctx context.Context, hostID uint, encryptedBase64Passphrase string, encryptedBase64SlotKey string) error { + require.Equal(t, host.ID, hostID) + key := config.TestConfig().Server.PrivateKey + + decryptedPassphrase, err := mdm.DecodeAndDecrypt(encryptedBase64Passphrase, key) + require.NoError(t, err) + require.Equal(t, passphrase, decryptedPassphrase) + + if encryptedBase64SlotKey == "" { + require.Equal(t, slotKey, encryptedBase64SlotKey) + return nil + } + decryptedSlotKey, err := mdm.DecodeAndDecrypt(encryptedBase64SlotKey, key) + require.NoError(t, err) + require.Equal(t, slotKey, decryptedSlotKey) + + return nil + } + + // with no slot key + err = svc.EscrowLUKSData(ctx, passphrase, slotKey, "") + require.NoError(t, err) + require.False(t, ds.ReportEscrowErrorFuncInvoked) + require.True(t, ds.SaveLUKSDataFuncInvoked) + + // with slot key + slotKey = "baz" + ds.SaveLUKSDataFuncInvoked = false + err = svc.EscrowLUKSData(ctx, passphrase, slotKey, "") + require.NoError(t, err) + require.True(t, ds.SaveLUKSDataFuncInvoked) + }) + + t.Run("fail when no/invalid private key is set", func(t *testing.T) { + ds := new(mock.Store) + license := &fleet.LicenseInfo{Tier: fleet.TierPremium} + host := &fleet.Host{ + OsqueryHostID: ptr.String("test"), + ID: 1, + } + expectedErrorMessage := "internal error: missing server private key" + ds.ReportEscrowErrorFunc = func(ctx context.Context, hostID uint, err string) error { + require.Equal(t, expectedErrorMessage, err) + return nil + } + + cfg := config.TestConfig() + cfg.Server.PrivateKey = "" + svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + ctx = test.HostContext(ctx, host) + err := svc.EscrowLUKSData(ctx, "foo", "bar", "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + + expectedErrorMessage = "internal error: could not encrypt LUKS data: create new cipher: crypto/aes: invalid key size 7" + ds.ReportEscrowErrorFuncInvoked = false + cfg.Server.PrivateKey = "invalid" + svc, ctx = newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) + ctx = test.HostContext(ctx, host) + err = svc.EscrowLUKSData(ctx, "foo", "bar", "") + require.Error(t, err) + require.True(t, ds.ReportEscrowErrorFuncInvoked) + }) +} + func TestGetOrbitConfigNudge(t *testing.T) { t.Run("missing values in AppConfig", func(t *testing.T) { ds := new(mock.Store) @@ -39,6 +219,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { return true, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { return &fleet.HostMDM{ @@ -114,6 +297,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) { return true, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } ds.GetHostMDMFunc = func(ctx context.Context, hostID uint) (*fleet.HostMDM, error) { return &fleet.HostMDM{ @@ -161,7 +347,7 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.TeamMDMConfigFuncInvoked = false }) - t.Run("non-elegible MDM status", func(t *testing.T) { + t.Run("non-eligible MDM status", func(t *testing.T) { ds := new(mock.Store) license := &fleet.LicenseInfo{Tier: fleet.TierPremium} svc, ctx := newTestService(t, ds, nil, nil, &TestServerOpts{License: license, SkipCreateTestUsers: true}) @@ -207,6 +393,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostUUID string) (bool, error) { return false, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } checkEmptyNudgeConfig := func(h *fleet.Host) { ctx := test.HostContext(ctx, h) @@ -283,6 +472,9 @@ func TestGetOrbitConfigNudge(t *testing.T) { Name: fleet.WellKnownMDMFleet, }, nil } + ds.IsHostPendingEscrowFunc = func(ctx context.Context, hostID uint) bool { + return false + } appCfg := &fleet.AppConfig{MDM: fleet.MDM{EnabledAndConfigured: true}} appCfg.MDM.MacOSUpdates.Deadline = optjson.SetString("2022-04-01") @@ -315,6 +507,7 @@ func TestGetOrbitConfigNudge(t *testing.T) { ds.GetHostOperatingSystemFuncInvoked = false cfg, err = svc.GetOrbitConfig(ctx) require.NoError(t, err) + require.False(t, cfg.Notifications.RunDiskEncryptionEscrow) require.Empty(t, cfg.NudgeConfig) require.True(t, ds.GetHostOperatingSystemFuncInvoked)