diff --git a/changes/8708-fv-api b/changes/8708-fv-api
new file mode 100644
index 0000000000..fe5de98afa
--- /dev/null
+++ b/changes/8708-fv-api
@@ -0,0 +1 @@
+* Added an API endpoint to retrieve a host disk encryption key for macOS if Fleet's MDM is enabled.
diff --git a/cmd/fleet/serve_test.go b/cmd/fleet/serve_test.go
index a9887e0c6e..c2396489b4 100644
--- a/cmd/fleet/serve_test.go
+++ b/cmd/fleet/serve_test.go
@@ -1007,8 +1007,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) {
now := time.Now()
t.Run("able to decrypt", func(t *testing.T) {
- ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
- return []fleet.DiskEncryptionKey{
+ ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
+ return []fleet.HostDiskEncryptionKey{
{HostID: 1, Base64Encrypted: base64EncryptedKey, UpdatedAt: now},
{HostID: 2, Base64Encrypted: base64EncryptedKey, UpdatedAt: now.Add(time.Hour)},
{HostID: 3, Base64Encrypted: "BAD-KEY", UpdatedAt: now.Add(-time.Hour)},
@@ -1039,8 +1039,8 @@ func TestVerifyDiskEncryptionKeysJob(t *testing.T) {
})
t.Run("unable to decrypt", func(t *testing.T) {
- ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
- return []fleet.DiskEncryptionKey{{HostID: 1, Base64Encrypted: "RANDOM"}}, nil
+ ds.GetUnverifiedDiskEncryptionKeysFunc = func(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
+ return []fleet.HostDiskEncryptionKey{{HostID: 1, Base64Encrypted: "RANDOM"}}, nil
}
calls := 0
diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json
index 286baa078f..6cfab926f4 100644
--- a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json
+++ b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json
@@ -39,6 +39,7 @@
"config_tls_refresh": 0,
"logger_tls_period": 0,
"mdm": {
+ "encryption_key_available": false,
"enrollment_status": null,
"server_url": null
},
diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml
index b866a0249e..f31c37e7c3 100644
--- a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml
+++ b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml
@@ -30,6 +30,7 @@ spec:
last_enrolled_at: "0001-01-01T00:00:00Z"
logger_tls_period: 0
mdm:
+ encryption_key_available: false
enrollment_status: null
server_url: null
memory: 0
diff --git a/cmd/fleetctl/testdata/expectedListHostsJson.json b/cmd/fleetctl/testdata/expectedListHostsJson.json
index e72f2bd20b..7a46a80bdf 100644
--- a/cmd/fleetctl/testdata/expectedListHostsJson.json
+++ b/cmd/fleetctl/testdata/expectedListHostsJson.json
@@ -40,6 +40,7 @@
"config_tls_refresh": 0,
"logger_tls_period": 0,
"mdm": {
+ "encryption_key_available": false,
"enrollment_status": null,
"server_url": null
},
@@ -106,6 +107,7 @@
"config_tls_refresh": 0,
"logger_tls_period": 0,
"mdm": {
+ "encryption_key_available": false,
"enrollment_status": null,
"server_url": null
},
diff --git a/cmd/fleetctl/testdata/expectedListHostsYaml.yml b/cmd/fleetctl/testdata/expectedListHostsYaml.yml
index 737af4d19b..f40a34b644 100644
--- a/cmd/fleetctl/testdata/expectedListHostsYaml.yml
+++ b/cmd/fleetctl/testdata/expectedListHostsYaml.yml
@@ -34,6 +34,7 @@ spec:
last_enrolled_at: "0001-01-01T00:00:00Z"
logger_tls_period: 0
mdm:
+ encryption_key_available: false
enrollment_status: null
server_url: null
memory: 0
@@ -87,6 +88,7 @@ spec:
last_enrolled_at: "0001-01-01T00:00:00Z"
logger_tls_period: 0
mdm:
+ encryption_key_available: false
enrollment_status: null
server_url: null
memory: 0
diff --git a/docs/Using-Fleet/Audit-Activities.md b/docs/Using-Fleet/Audit-Activities.md
index 6fe5afb6ba..0ea6eb5f9d 100644
--- a/docs/Using-Fleet/Audit-Activities.md
+++ b/docs/Using-Fleet/Audit-Activities.md
@@ -583,6 +583,23 @@ This activity contains the following fields:
}
```
+### Type `read_host_disk_encryption_key`
+
+Generated when a user reads the disk encryption key for a host.
+
+This activity contains the following fields:
+- "host_id": ID of the host.
+- "host_display_name": Display name of the host.
+
+#### Example
+
+```json
+{
+ "host_id": 1,
+ "host_display_name": "Anna's MacBook Pro",
+}
+```
+
\ No newline at end of file
diff --git a/docs/Using-Fleet/Permissions.md b/docs/Using-Fleet/Permissions.md
index 3c3a9a4753..f932df8ec3 100644
--- a/docs/Using-Fleet/Permissions.md
+++ b/docs/Using-Fleet/Permissions.md
@@ -6,48 +6,45 @@ Users with the Admin role receive all permissions.
## User permissions
-| **Action** | Observer | Maintainer | Admin |
-| ---------------------------------------------------- | -------- | ---------- | ----- |
-| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ |
-| View all hosts | ✅ | ✅ | ✅ |
+| **Action** | Observer | Maintainer | Admin |
+| ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------- | ----- |
+| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ |
+| View all hosts | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ |
-| Target hosts using labels | ✅ | ✅ | ✅ |
-| Add and delete hosts | | ✅ | ✅ |
-| Transfer hosts between teams\* | | ✅ | ✅ |
-| Create, edit, and delete labels | | ✅ | ✅ |
-| View all software | ✅ | ✅ | ✅ |
-| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ |
-| Filter hosts by software | ✅ | ✅ | ✅ |
-| Filter software by team\* | ✅ | ✅ | ✅ |
-| Manage [vulnerability automations](https://fleetdm.com/docs/using-fleet/automations#vulnerability-automations) | | | ✅ |
-| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
-| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ |
-| Create, edit, and delete queries | | ✅ | ✅ |
-| View all queries | ✅ | ✅ | ✅ |
-| Add, edit, and remove queries from all schedules | | ✅ | ✅ |
-| Create, edit, view, and delete packs | | ✅ | ✅ |
-| View all policies | ✅ | ✅ | ✅ |
-| Filter hosts using policies | ✅ | ✅ | ✅ |
-| Create, edit, and delete policies for all hosts | | ✅ | ✅ |
-| Create, edit, and delete policies for all hosts assigned to team\* | | ✅ | ✅ |
-| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
-| Create, edit, view, and delete users | | | ✅ |
-| Add and remove team members\* | | | ✅ |
-| Create, edit, and delete teams\* | | | ✅ |
-| Create, edit, and delete [enroll secrets](https://fleetdm.com/docs/deploying/faq#when-do-i-need-to-deploy-a-new-enroll-secret-to-my-hosts) | | ✅ | ✅ |
-| Create, edit, and delete [enroll secrets for teams](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team)\* | | ✅ | ✅ |
-| Edit [organization settings](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) | | | ✅ |
-| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
-| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | ✅ |
-| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
-| Retrieve contents from file carving | | | ✅ |
-| View Apple mobile device management (MDM) certificate information | | | ✅ |
-| View Apple business manager (BM) information | | | ✅ |
-| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | ✅ |
-
-
-
-
+| Target hosts using labels | ✅ | ✅ | ✅ |
+| Add and delete hosts | | ✅ | ✅ |
+| Transfer hosts between teams\* | | ✅ | ✅ |
+| Create, edit, and delete labels | | ✅ | ✅ |
+| View all software | ✅ | ✅ | ✅ |
+| Filter software by [vulnerabilities](https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing) | ✅ | ✅ | ✅ |
+| Filter hosts by software | ✅ | ✅ | ✅ |
+| Filter software by team\* | ✅ | ✅ | ✅ |
+| Manage [vulnerability automations](https://fleetdm.com/docs/using-fleet/automations#vulnerability-automations) | | | ✅ |
+| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
+| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) against all hosts | | ✅ | ✅ |
+| Create, edit, and delete queries | | ✅ | ✅ |
+| View all queries | ✅ | ✅ | ✅ |
+| Add, edit, and remove queries from all schedules | | ✅ | ✅ |
+| Create, edit, view, and delete packs | | ✅ | ✅ |
+| View all policies | ✅ | ✅ | ✅ |
+| Filter hosts using policies | ✅ | ✅ | ✅ |
+| Create, edit, and delete policies for all hosts | | ✅ | ✅ |
+| Create, edit, and delete policies for all hosts assigned to team\* | | ✅ | ✅ |
+| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
+| Create, edit, view, and delete users | | | ✅ |
+| Add and remove team members\* | | | ✅ |
+| Create, edit, and delete teams\* | | | ✅ |
+| Create, edit, and delete [enroll secrets](https://fleetdm.com/docs/deploying/faq#when-do-i-need-to-deploy-a-new-enroll-secret-to-my-hosts) | | ✅ | ✅ |
+| Create, edit, and delete [enroll secrets for teams](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team)\* | | ✅ | ✅ |
+| Edit [organization settings](https://fleetdm.com/docs/using-fleet/configuration-files#organization-settings) | | | ✅ |
+| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
+| Edit [agent options for hosts assigned to teams](https://fleetdm.com/docs/using-fleet/configuration-files#team-agent-options)\* | | | ✅ |
+| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
+| Retrieve contents from file carving | | | ✅ |
+| View Apple mobile device management (MDM) certificate information | | | ✅ |
+| View Apple business manager (BM) information | | | ✅ |
+| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | ✅ |
+| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ |
\*Applies only to Fleet Premium
@@ -68,29 +65,29 @@ Users can be a member of multiple teams in Fleet.
Users that are members of multiple teams can be assigned different roles for each team. For example, a user can be given access to the "Workstations" team and assigned the "Observer" role. This same user can be given access to the "Servers" team and assigned the "Maintainer" role.
-| **Action** | Team observer | Team maintainer | Team admin |
-| ------------------------------------------------------------ | -------- | ---------- | ------- |
-| View hosts | ✅ | ✅ | ✅ |
-| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ |
-| Target hosts using labels | ✅ | ✅ | ✅ |
-| Add and delete hosts | | ✅ | ✅ |
-| Filter software by [vulnerabilities]((https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing)) | ✅ | ✅ | ✅ |
-| Filter hosts by software | ✅ | ✅ | ✅ |
-| Filter software | ✅ | ✅ | ✅ |
-| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
-| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ |
-| Create, edit, and delete only **self authored** queries | | ✅ | ✅ |
-| Add, edit, and remove queries from the schedule | | ✅ | ✅ |
-| View policies | ✅ | ✅ | ✅ |
-| View global (inherited) policies | ✅ | ✅ | ✅ |
-| Filter hosts using policies | ✅ | ✅ | ✅ |
-| Create, edit, and delete policies | | ✅ | ✅ |
-| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
-| Add and remove team members | | | ✅ |
-| Edit team name | | | ✅ |
-| Create, edit, and delete [team enroll secrets](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team) | | ✅ | ✅ |
-| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
-| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
-
+| **Action** | Team observer | Team maintainer | Team admin |
+| -------------------------------------------------------------------------------------------------------------------------------- | ------------- | --------------- | ---------- |
+| View hosts | ✅ | ✅ | ✅ |
+| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ |
+| Target hosts using labels | ✅ | ✅ | ✅ |
+| Add and delete hosts | | ✅ | ✅ |
+| Filter software by [vulnerabilities](<(https://fleetdm.com/docs/using-fleet/vulnerability-processing#vulnerability-processing)>) | ✅ | ✅ | ✅ |
+| Filter hosts by software | ✅ | ✅ | ✅ |
+| Filter software | ✅ | ✅ | ✅ |
+| Run only designated, **observer can run** ,queries as live queries against all hosts | ✅ | ✅ | ✅ |
+| Run any query as [live query](https://fleetdm.com/docs/using-fleet/fleet-ui#run-a-query) | | ✅ | ✅ |
+| Create, edit, and delete only **self authored** queries | | ✅ | ✅ |
+| Add, edit, and remove queries from the schedule | | ✅ | ✅ |
+| View policies | ✅ | ✅ | ✅ |
+| View global (inherited) policies | ✅ | ✅ | ✅ |
+| Filter hosts using policies | ✅ | ✅ | ✅ |
+| Create, edit, and delete policies | | ✅ | ✅ |
+| Manage [policy automations](https://fleetdm.com/docs/using-fleet/automations#policy-automations) | | | ✅ |
+| Add and remove team members | | | ✅ |
+| Edit team name | | | ✅ |
+| Create, edit, and delete [team enroll secrets](https://fleetdm.com/docs/using-fleet/rest-api#get-enroll-secrets-for-a-team) | | ✅ | ✅ |
+| Edit [agent options](https://fleetdm.com/docs/using-fleet/configuration-files#agent-options) | | | ✅ |
+| Initiate [file carving](https://fleetdm.com/docs/using-fleet/rest-api#file-carving) | | ✅ | ✅ |
+| View disk encryption key for macOS hosts enrolled in Fleet's MDM | ✅ | ✅ | ✅ |
diff --git a/docs/Using-Fleet/REST-API.md b/docs/Using-Fleet/REST-API.md
index 897f55d1f7..2c92776aa0 100644
--- a/docs/Using-Fleet/REST-API.md
+++ b/docs/Using-Fleet/REST-API.md
@@ -1723,6 +1723,7 @@ None.
- [Get aggregated host's mobile device management (MDM) and Munki information](#get-aggregated-hosts-macadmin-mobile-device-management-mdm-and-munki-information)
- [Get host OS versions](#get-host-os-versions)
- [Get hosts report in CSV](#get-hosts-report-in-csv)
+- [Get host's disk encryption key](#get-hosts-disk-encryption-key)
### On the different timestamps in the host data structure
@@ -1864,6 +1865,7 @@ If `after` is being used with `created_at` or `updated_at`, the table must be sp
}
},
"mdm": {
+ "encryption_key_available": false,
"enrollment_status": null,
"server_url": null
}
@@ -2243,6 +2245,7 @@ Returns the information of the specified host.
}
},
"mdm": {
+ "encryption_key_available": false,
"enrollment_status": null,
"server_url": null
}
@@ -2421,6 +2424,7 @@ Returns the information of the host specified using the `uuid`, `osquery_host_id
"display_text": "dogfood-ubuntu-box",
"display_name": "dogfood-ubuntu-box",
"mdm": {
+ "encryption_key_available": false,
"enrollment_status": null,
"server_url": null
}
@@ -2989,6 +2993,42 @@ created_at,updated_at,id,detail_updated_at,label_updated_at,policy_updated_at,la
2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,3,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:23:56Z,2022-03-15T17:21:56Z,false,foo.local2,48ebe4b0-39c3-4a74-a67f-308f7b5dd171,linux,,,,,,0s,0,,,,0,0,,,,,,,,,0,0,0,,,0,0,0,,,,
```
+### Get host's disk encryption key
+
+Requires the [macadmins osquery extension](https://github.com/macadmins/osquery-extension) which comes bundled
+in [Fleet's osquery installers](https://fleetdm.com/docs/using-fleet/adding-hosts#osquery-installer).
+
+Requires Fleet's MDM properly [enabled and configured](./Mobile-device-management.md).
+
+Retrieves the disk encryption key for a host.
+
+`GET /api/v1/fleet/hosts/:id/encryption_key`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ---- | ------- | ---- | ------------------------------------------------------------------ |
+| id | integer | path | **Required** The id of the host to get the disk encryption key for |
+
+
+#### Example
+
+`GET /api/v1/fleet/hosts/8/encryption_key`
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "host_id": 8,
+ "encryption_key": {
+ "key": "5ADZ-HTZ8-LJJ4-B2F8-JWH3-YPBT",
+ "updated_at": "2022-12-01T05:31:43Z"
+ }
+}
+```
+
---
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 9b5a2870ae..b218315151 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -429,6 +429,7 @@ FROM
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm on hmdm.host_id = h.id
+ LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
JOIN (
SELECT
count(*) as count
@@ -490,6 +491,16 @@ const hostMDMSelect = `,
CASE
WHEN hmdm.is_server = 1 THEN NULL
ELSE hmdm.server_url
+ END,
+ 'encryption_key_available',
+ CASE
+ /* roberto: this is the only way I have found for MySQL to
+ * return true and false instead of 0 and 1 in the JSON, the
+ * unmarshaller was having problems converting int values to
+ * booleans.
+ */
+ WHEN hdek.decryptable IS NULL OR hdek.decryptable = 0 THEN CAST(FALSE AS JSON)
+ ELSE CAST(TRUE AS JSON)
END
) mdm_host_data
`
@@ -685,7 +696,8 @@ func (ds *Datastore) applyHostFilters(opt fleet.HostListOptions, sql string, fil
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN teams t ON (h.team_id = t.id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
- LEFT JOIN host_mdm hmdm ON hmdm.host_id = h.id
+ LEFT JOIN host_mdm hmdm ON hmdm.host_id = h.id
+ LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
%s
%s
%s
@@ -1485,6 +1497,7 @@ func (ds *Datastore) SearchHosts(ctx context.Context, filter fleet.TeamFilter, m
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm on hmdm.host_id = h.id
+ LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
WHERE TRUE`
var args []interface{}
@@ -1592,6 +1605,7 @@ func (ds *Datastore) HostByIdentifier(ctx context.Context, identifier string) (*
LEFT JOIN host_updates hu ON (h.id = hu.host_id)
LEFT JOIN host_disks hd ON hd.host_id = h.id
LEFT JOIN host_mdm hmdm ON hmdm.host_id = h.id
+ LEFT JOIN host_disk_encryption_keys hdek ON hdek.host_id = h.id
WHERE ? IN (h.hostname, h.osquery_host_id, h.node_key, h.uuid)
LIMIT 1
`
@@ -2348,8 +2362,8 @@ func (ds *Datastore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostI
}
-func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
- var keys []fleet.DiskEncryptionKey
+func (ds *Datastore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
+ var keys []fleet.HostDiskEncryptionKey
err := sqlx.SelectContext(ctx, ds.reader, &keys, `
SELECT
base64_encrypted,
@@ -2384,6 +2398,25 @@ func (ds *Datastore) SetHostsDiskEncryptionKeyStatus(
return err
}
+func (ds *Datastore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
+ var key fleet.HostDiskEncryptionKey
+ err := sqlx.GetContext(ctx, ds.reader, &key, `
+ SELECT
+ host_id, base64_encrypted, decryptable, updated_at
+ FROM
+ host_disk_encryption_keys
+ WHERE host_id = ?`, hostID)
+
+ if err != nil {
+ if err == sql.ErrNoRows {
+ msg := fmt.Sprintf("for host %d", hostID)
+ return nil, ctxerr.Wrap(ctx, notFound("HostDiskEncryptionKey").WithMessage(msg))
+ }
+ return nil, ctxerr.Wrapf(ctx, err, "getting data from host_mdm for host_id %d", hostID)
+ }
+ return &key, nil
+}
+
func (ds *Datastore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {
return ds.updateOrInsert(
ctx,
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index 482646722d..ce8995ce59 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -6034,20 +6034,9 @@ func testHostsSetOrUpdateHostDisksEncryptionKey(t *testing.T, ds *Datastore) {
require.NoError(t, err)
checkEncryptionKey := func(hostID uint, expected string) {
- ExecAdhocSQL(t, ds, func(tx sqlx.ExtContext) error {
- var actual string
-
- row := tx.QueryRowxContext(
- context.Background(),
- "SELECT base64_encrypted FROM host_disk_encryption_keys WHERE host_id = ?",
- hostID,
- )
-
- err := row.Scan(&actual)
- require.NoError(t, err)
- require.Equal(t, expected, actual)
- return nil
- })
+ actual, err := ds.GetHostDiskEncryptionKey(context.Background(), hostID)
+ require.NoError(t, err)
+ require.Equal(t, expected, actual.Base64Encrypted)
}
h, err := ds.Host(context.Background(), host.ID)
diff --git a/server/fleet/activities.go b/server/fleet/activities.go
index ad84ecc4a3..7aaf71a5bd 100644
--- a/server/fleet/activities.go
+++ b/server/fleet/activities.go
@@ -47,6 +47,8 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeMDMUnenrolled{},
ActivityTypeEditedMacOSMinVersion{},
+
+ ActivityTypeReadHostDiskEncryptionKey{},
}
type ActivityDetails interface {
@@ -708,3 +710,22 @@ func (a ActivityTypeEditedMacOSMinVersion) Documentation() (activity string, det
"deadline": "2023-06-01"
}`
}
+
+type ActivityTypeReadHostDiskEncryptionKey struct {
+ HostID uint `json:"host_id"`
+ HostDisplayName string `json:"host_display_name"`
+}
+
+func (a ActivityTypeReadHostDiskEncryptionKey) ActivityName() string {
+ return "read_host_disk_encryption_key"
+}
+
+func (a ActivityTypeReadHostDiskEncryptionKey) Documentation() (activity string, details string, detailsExample string) {
+ return `Generated when a user reads the disk encryption key for a host.`,
+ `This activity contains the following fields:
+- "host_id": ID of the host.
+- "host_display_name": Display name of the host.`, `{
+ "host_id": 1,
+ "host_display_name": "Anna's MacBook Pro",
+}`
+}
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 971662834d..6d5d6e71f0 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -642,10 +642,12 @@ type Datastore interface {
// 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) ([]DiskEncryptionKey, error)
- // SetHostsDiskEncryptionKeyStatus sets the encryptable status for the set
+ GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]HostDiskEncryptionKey, error)
+ // SetHostDiskEncryptionKeyStatus sets the encryptable status for the set
// of encription keys provided
SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs []uint, encryptable bool, threshold time.Time) error
+ // GetHostDiskEncryptionKey returns the encryption key information for a given host
+ GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*HostDiskEncryptionKey, error)
// SetOrUpdateHostOrbitInfo inserts of updates the orbit info for a host
SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error
diff --git a/server/fleet/hosts.go b/server/fleet/hosts.go
index 4357fdedc1..d8bd8174b5 100644
--- a/server/fleet/hosts.go
+++ b/server/fleet/hosts.go
@@ -220,6 +220,9 @@ type MDMHostData struct {
// ServerURL is the server_url stored in the host_mdm table, loaded by
// JOIN in datastore
ServerURL *string `json:"server_url" db:"-" csv:"mdm.server_url"`
+ // EncryptionKeyAvailable indicates if Fleet was able to retrieve and
+ // decode an encryption key for the host.
+ EncryptionKeyAvailable bool `json:"encryption_key_available" db:"-" csv:"-"`
}
// Scan implements the Scanner interface for sqlx, to support unmarshaling a
@@ -640,9 +643,10 @@ type HostMDMCheckinInfo struct {
DisplayName string `json:"display_name" db:"display_name"`
}
-type DiskEncryptionKey struct {
- HostID uint `db:"host_id"`
- Base64Encrypted string `db:"base64_encrypted"`
- Decryptable *bool `db:"decryptable"`
- UpdatedAt time.Time `db:"updated_at"`
+type HostDiskEncryptionKey struct {
+ HostID uint `json:"-" db:"host_id"`
+ Base64Encrypted string `json:"-" db:"base64_encrypted"`
+ Decryptable *bool `json:"-" db:"decryptable"`
+ UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
+ DecryptedValue string `json:"key" db:"-"`
}
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 28e2b2bab9..5a8e6853fa 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -324,6 +324,8 @@ type Service interface {
GetMDMSolution(ctx context.Context, mdmID uint) (*MDMSolution, error)
GetMunkiIssue(ctx context.Context, munkiIssueID uint) (*MunkiIssue, error)
+ HostEncryptionKey(ctx context.Context, id uint) (*HostDiskEncryptionKey, error)
+
// OSVersions returns a list of operating systems and associated host counts, which may be
// filtered using the following optional criteria: team id, platform, or name and version.
// Name cannot be used without version, and conversely, version cannot be used without name.
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index eacefbfe75..2e8ef90915 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -464,10 +464,12 @@ type SetOrUpdateHostDisksEncryptionFunc func(ctx context.Context, hostID uint, e
type SetOrUpdateHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint, encryptedBase64Key string) error
-type GetUnverifiedDiskEncryptionKeysFunc func(ctx context.Context) ([]fleet.DiskEncryptionKey, 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 GetHostDiskEncryptionKeyFunc func(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error)
+
type SetOrUpdateHostOrbitInfoFunc func(ctx context.Context, hostID uint, version string) error
type ReplaceHostDeviceMappingFunc func(ctx context.Context, id uint, mappings []*fleet.HostDeviceMapping) error
@@ -1216,6 +1218,9 @@ type DataStore struct {
SetHostsDiskEncryptionKeyStatusFunc SetHostsDiskEncryptionKeyStatusFunc
SetHostsDiskEncryptionKeyStatusFuncInvoked bool
+ GetHostDiskEncryptionKeyFunc GetHostDiskEncryptionKeyFunc
+ GetHostDiskEncryptionKeyFuncInvoked bool
+
SetOrUpdateHostOrbitInfoFunc SetOrUpdateHostOrbitInfoFunc
SetOrUpdateHostOrbitInfoFuncInvoked bool
@@ -2441,7 +2446,7 @@ func (s *DataStore) SetOrUpdateHostDiskEncryptionKey(ctx context.Context, hostID
return s.SetOrUpdateHostDiskEncryptionKeyFunc(ctx, hostID, encryptedBase64Key)
}
-func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.DiskEncryptionKey, error) {
+func (s *DataStore) GetUnverifiedDiskEncryptionKeys(ctx context.Context) ([]fleet.HostDiskEncryptionKey, error) {
s.GetUnverifiedDiskEncryptionKeysFuncInvoked = true
return s.GetUnverifiedDiskEncryptionKeysFunc(ctx)
}
@@ -2451,6 +2456,11 @@ func (s *DataStore) SetHostsDiskEncryptionKeyStatus(ctx context.Context, hostIDs
return s.SetHostsDiskEncryptionKeyStatusFunc(ctx, hostIDs, encryptable, threshold)
}
+func (s *DataStore) GetHostDiskEncryptionKey(ctx context.Context, hostID uint) (*fleet.HostDiskEncryptionKey, error) {
+ s.GetHostDiskEncryptionKeyFuncInvoked = true
+ return s.GetHostDiskEncryptionKeyFunc(ctx, hostID)
+}
+
func (s *DataStore) SetOrUpdateHostOrbitInfo(ctx context.Context, hostID uint, version string) error {
s.SetOrUpdateHostOrbitInfoFuncInvoked = true
return s.SetOrUpdateHostOrbitInfoFunc(ctx, hostID, version)
diff --git a/server/service/handler.go b/server/service/handler.go
index c330921e56..b42d8752fc 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -437,6 +437,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.GET("/api/_version_/fleet/mdm/apple/installers", listMDMAppleInstallersEndpoint, listMDMAppleInstallersRequest{})
ue.GET("/api/_version_/fleet/mdm/apple/devices", listMDMAppleDevicesEndpoint, listMDMAppleDevicesRequest{})
ue.GET("/api/_version_/fleet/mdm/apple/dep/devices", listMDMAppleDEPDevicesEndpoint, listMDMAppleDEPDevicesRequest{})
+ ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/encryption_key", getHostEncryptionKey, getHostEncryptionKeyRequest{})
// host-specific mdm commands
ue.PATCH("/api/_version_/fleet/mdm/hosts/{id:[0-9]+}/unenroll", mdmAppleCommandRemoveEnrollmentProfileEndpoint, mdmAppleCommandRemoveEnrollmentProfileRequest{})
diff --git a/server/service/hosts.go b/server/service/hosts.go
index f3e83d91a7..8771bd7f63 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -11,12 +11,14 @@ import (
"strings"
"time"
- "github.com/fleetdm/fleet/v4/server/contexts/authz"
+ "github.com/fleetdm/fleet/v4/server/authz"
+ authzctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/license"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
+ apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/gocarina/gocsv"
)
@@ -388,7 +390,7 @@ func getHostEndpoint(ctx context.Context, request interface{}, svc fleet.Service
}
func (svc *Service) GetHost(ctx context.Context, id uint, opts fleet.HostDetailOptions) (*fleet.HostDetail, error) {
- alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken)
+ alreadyAuthd := svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken)
if !alreadyAuthd {
// First ensure the user has access to list hosts, then check the specific
// host once team_id is loaded.
@@ -723,7 +725,7 @@ func refetchHostEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
}
func (svc *Service) RefetchHost(ctx context.Context, id uint) error {
- if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
+ if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return err
}
@@ -871,7 +873,7 @@ func listHostDeviceMappingEndpoint(ctx context.Context, request interface{}, svc
}
func (svc *Service) ListHostDeviceMapping(ctx context.Context, id uint) ([]*fleet.HostDeviceMapping, error) {
- if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
+ if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
@@ -963,7 +965,7 @@ func getMacadminsDataEndpoint(ctx context.Context, request interface{}, svc flee
}
func (svc *Service) MacadminsData(ctx context.Context, id uint) (*fleet.MacadminsData, error) {
- if !svc.authz.IsAuthenticatedWith(ctx, authz.AuthnDeviceToken) {
+ if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
return nil, err
}
@@ -1250,7 +1252,7 @@ func hostsReportEndpoint(ctx context.Context, request interface{}, svc fleet.Ser
// for now, only csv format is allowed
if req.Format != "csv" {
// prevent returning an "unauthorized" error, we want that specific error
- if az, ok := authz.FromContext(ctx); ok {
+ if az, ok := authzctx.FromContext(ctx); ok {
az.SetChecked()
}
err := ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("format", "unsupported or unspecified report format").
@@ -1365,3 +1367,80 @@ func (svc *Service) OSVersions(ctx context.Context, teamID *uint, platform *stri
return osVersions, nil
}
+
+////////////////////////////////////////////////////////////////////////////////
+// Encryption Key
+////////////////////////////////////////////////////////////////////////////////
+
+type getHostEncryptionKeyRequest struct {
+ ID uint `url:"id"`
+}
+
+type getHostEncryptionKeyResponse struct {
+ Err error `json:"error,omitempty"`
+ EncryptionKey *fleet.HostDiskEncryptionKey `json:"encryption_key,omitempty"`
+ HostID uint `json:"host_id,omitempty"`
+}
+
+func (r getHostEncryptionKeyResponse) error() error { return r.Err }
+
+func getHostEncryptionKey(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
+ req := request.(*getHostEncryptionKeyRequest)
+ key, err := svc.HostEncryptionKey(ctx, req.ID)
+ if err != nil {
+ return getHostEncryptionKeyResponse{Err: err}, nil
+ }
+ return getHostEncryptionKeyResponse{EncryptionKey: key, HostID: req.ID}, nil
+}
+
+func (svc *Service) HostEncryptionKey(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
+ return nil, err
+ }
+
+ host, err := svc.ds.HostLite(ctx, id)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
+ }
+
+ // Permissions to read encryption keys are exactly the same
+ // as the ones required to read hosts.
+ if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
+ return nil, err
+ }
+
+ key, err := svc.ds.GetHostDiskEncryptionKey(ctx, id)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
+ }
+
+ if key.Decryptable == nil || !*key.Decryptable {
+ return nil, ctxerr.Wrap(ctx, notFoundError{}, "getting host encryption key")
+ }
+
+ cert, _, _, err := svc.config.MDM.AppleSCEP()
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
+ }
+
+ decryptedKey, err := apple_mdm.DecryptBase64CMS(key.Base64Encrypted, cert.Leaf, cert.PrivateKey)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting host encryption key")
+ }
+
+ key.DecryptedValue = string(decryptedKey)
+
+ err = svc.ds.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
+}
diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go
index 82b43bb223..134df616cc 100644
--- a/server/service/hosts_test.go
+++ b/server/service/hosts_test.go
@@ -2,6 +2,8 @@ package service
import (
"context"
+ "crypto/x509"
+ "encoding/base64"
"errors"
"fmt"
"testing"
@@ -9,14 +11,19 @@ import (
"github.com/WatchBeam/clock"
"github.com/fleetdm/fleet/v4/server/authz"
+ "github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
+ apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
+ nanodep_client "github.com/micromdm/nanodep/client"
+ "github.com/micromdm/nanodep/tokenpki"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
+ "go.mozilla.org/pkcs7"
)
func TestHostDetails(t *testing.T) {
@@ -508,3 +515,160 @@ func TestEmptyTeamOSVersions(t *testing.T) {
require.Error(t, err)
require.Equal(t, "some unknown error", fmt.Sprint(err))
}
+
+func TestHostEncryptionKey(t *testing.T) {
+ cases := []struct {
+ name string
+ host *fleet.Host
+ allowedUsers []*fleet.User
+ disallowedUsers []*fleet.User
+ }{
+ {
+ name: "global host",
+ host: &fleet.Host{
+ ID: 1,
+ Platform: "darwin",
+ NodeKey: ptr.String("test_key"),
+ Hostname: "test_hostname",
+ UUID: "test_uuid",
+ TeamID: nil,
+ },
+ allowedUsers: []*fleet.User{
+ test.UserAdmin,
+ test.UserMaintainer,
+ test.UserObserver,
+ },
+ disallowedUsers: []*fleet.User{
+ test.UserTeamAdminTeam1,
+ test.UserTeamMaintainerTeam1,
+ test.UserTeamObserverTeam1,
+ test.UserNoRoles,
+ },
+ },
+ {
+ name: "team host",
+ host: &fleet.Host{
+ ID: 2,
+ Platform: "darwin",
+ NodeKey: ptr.String("test_key_2"),
+ Hostname: "test_hostname_2",
+ UUID: "test_uuid_2",
+ TeamID: ptr.Uint(1),
+ },
+ allowedUsers: []*fleet.User{
+ test.UserAdmin,
+ test.UserMaintainer,
+ test.UserObserver,
+ test.UserTeamAdminTeam1,
+ test.UserTeamMaintainerTeam1,
+ test.UserTeamObserverTeam1,
+ },
+ disallowedUsers: []*fleet.User{
+ test.UserTeamAdminTeam2,
+ test.UserTeamMaintainerTeam2,
+ test.UserTeamObserverTeam2,
+ test.UserNoRoles,
+ },
+ },
+ }
+
+ testBMToken := &nanodep_client.OAuth1Tokens{
+ ConsumerKey: "test_consumer",
+ ConsumerSecret: "test_secret",
+ AccessToken: "test_access_token",
+ AccessSecret: "test_access_secret",
+ AccessTokenExpiry: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC),
+ }
+ testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
+ require.NoError(t, err)
+ testCertPEM := tokenpki.PEMCertificate(testCert.Raw)
+ testKeyPEM := tokenpki.PEMRSAPrivateKey(testKey)
+
+ fleetCfg := config.TestConfig()
+ config.SetTestMDMConfig(t, &fleetCfg, testCertPEM, testKeyPEM, testBMToken)
+
+ recoveryKey := "AAA-BBB-CCC"
+ encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{testCert})
+ require.NoError(t, err)
+ base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey)
+
+ for _, tt := range cases {
+ t.Run(tt.name, func(t *testing.T) {
+ ds := new(mock.Store)
+ svc, ctx := newTestServiceWithConfig(t, ds, fleetCfg, nil, nil)
+
+ ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
+ require.Equal(t, tt.host.ID, id)
+ return tt.host, nil
+ }
+
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ return &fleet.HostDiskEncryptionKey{
+ Base64Encrypted: base64EncryptedKey,
+ Decryptable: ptr.Bool(true),
+ }, nil
+ }
+
+ ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
+ act := activity.(fleet.ActivityTypeReadHostDiskEncryptionKey)
+ require.Equal(t, tt.host.ID, act.HostID)
+ require.EqualValues(t, act.HostDisplayName, tt.host.DisplayName())
+ return nil
+ }
+
+ t.Run("allowed users", func(t *testing.T) {
+ for _, u := range tt.allowedUsers {
+ _, err := svc.HostEncryptionKey(test.UserContext(ctx, u), tt.host.ID)
+ require.NoError(t, err)
+ }
+ })
+
+ t.Run("disallowed users", func(t *testing.T) {
+ for _, u := range tt.disallowedUsers {
+ _, err := svc.HostEncryptionKey(test.UserContext(ctx, u), tt.host.ID)
+ require.Error(t, err)
+ require.Contains(t, authz.ForbiddenErrorMessage, err.Error())
+ }
+ })
+
+ t.Run("no user in context", func(t *testing.T) {
+ _, err := svc.HostEncryptionKey(ctx, tt.host.ID)
+ require.Error(t, err)
+ require.Contains(t, authz.ForbiddenErrorMessage, err.Error())
+ })
+ })
+ }
+
+ t.Run("test error cases", func(t *testing.T) {
+ ds := new(mock.Store)
+ svc, ctx := newTestService(t, ds, nil, nil)
+ ctx = test.UserContext(ctx, test.UserAdmin)
+
+ hostErr := errors.New("host error")
+ ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
+ return nil, hostErr
+ }
+ _, err := svc.HostEncryptionKey(ctx, 1)
+ require.ErrorIs(t, err, hostErr)
+ ds.HostLiteFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
+ return &fleet.Host{}, nil
+ }
+
+ keyErr := errors.New("key error")
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ return nil, keyErr
+ }
+ _, err = svc.HostEncryptionKey(ctx, 1)
+ require.ErrorIs(t, err, keyErr)
+ ds.GetHostDiskEncryptionKeyFunc = func(ctx context.Context, id uint) (*fleet.HostDiskEncryptionKey, error) {
+ return &fleet.HostDiskEncryptionKey{Base64Encrypted: "key"}, nil
+ }
+
+ ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
+ return errors.New("activity error")
+ }
+
+ _, err = svc.HostEncryptionKey(ctx, 1)
+ require.Error(t, err)
+ })
+}
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index 06465e510c..79cbe546af 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -2257,28 +2257,6 @@ func (s *integrationEnterpriseTestSuite) TestOrbitConfigNudgeSettings() {
require.Equal(t, wantCfg.OSVersionRequirements[0].RequiredInstallationDate.String(), "2022-01-04 04:00:00 +0000 UTC")
}
-// gets the latest activity and checks that it matches any provided properties.
-// empty string or 0 id means do not check that property. It returns the ID of that
-// latest activity.
-func (s *integrationEnterpriseTestSuite) lastActivityMatches(name, details string, id uint) uint {
- var listActivities listActivitiesResponse
- s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "1")
- require.True(s.T(), len(listActivities.Activities) > 0)
-
- act := listActivities.Activities[0]
- if name != "" {
- assert.Equal(s.T(), name, act.Type)
- }
- if details != "" {
- require.NotNil(s.T(), act.Details)
- assert.JSONEq(s.T(), details, string(*act.Details))
- }
- if id > 0 {
- assert.Equal(s.T(), id, act.ID)
- }
- return act.ID
-}
-
// allEqual compares all fields of a struct.
// If a field is a pointer on one side but not on the other, then it follows that pointer. This is useful for optional
// arguments.
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index 8a4280c235..e3668c6952 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -31,8 +31,10 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
+ "github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/mock"
"github.com/fleetdm/fleet/v4/server/service/schedule"
+ "github.com/fleetdm/fleet/v4/server/test"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
"github.com/google/uuid"
@@ -587,6 +589,174 @@ func (s *integrationMDMTestSuite) TestMDMAppleUnenroll() {
s.Do("PATCH", fmt.Sprintf("/api/latest/fleet/mdm/hosts/%d/unenroll", h.ID), nil, http.StatusOK)
}
+func (s *integrationMDMTestSuite) TestMDMAppleGetEncryptionKey() {
+ t := s.T()
+ ctx := context.Background()
+
+ // create a host
+ host, err := s.ds.NewHost(context.Background(), &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now().Add(-1 * time.Minute),
+ OsqueryHostID: ptr.String(t.Name()),
+ NodeKey: ptr.String(t.Name()),
+ UUID: uuid.New().String(),
+ Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+
+ // add an encryption key for the host
+ cert, _, _, err := s.fleetCfg.MDM.AppleSCEP()
+ require.NoError(t, err)
+ parsed, err := x509.ParseCertificate(cert.Certificate[0])
+ require.NoError(t, err)
+ recoveryKey := "AAA-BBB-CCC"
+ encryptedKey, err := pkcs7.Encrypt([]byte(recoveryKey), []*x509.Certificate{parsed})
+ require.NoError(t, err)
+ base64EncryptedKey := base64.StdEncoding.EncodeToString(encryptedKey)
+
+ err = s.ds.SetOrUpdateHostDiskEncryptionKey(ctx, host.ID, base64EncryptedKey)
+ require.NoError(t, err)
+
+ // request with no token
+ res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusUnauthorized)
+ res.Body.Close()
+
+ // encryption key not processed yet
+ resp := getHostEncryptionKeyResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)
+
+ // unable to decrypt encryption key
+ err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, false, time.Now())
+ require.NoError(t, err)
+ resp = getHostEncryptionKeyResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusNotFound, &resp)
+
+ // no activities created so far
+ activities := listActivitiesResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &activities)
+ found := false
+ for _, activity := range activities.Activities {
+ if activity.Type == "read_host_disk_encryption_key" {
+ found = true
+ }
+ }
+ require.False(t, found)
+
+ // decryptable key
+ checkDecryptableKey := func(u fleet.User) {
+ err = s.ds.SetHostsDiskEncryptionKeyStatus(ctx, []uint{host.ID}, true, time.Now())
+ require.NoError(t, err)
+ resp = getHostEncryptionKeyResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusOK, &resp)
+ require.Equal(t, recoveryKey, resp.EncryptionKey.DecryptedValue)
+
+ // use the admin token to get the activities
+ currToken := s.token
+ defer func() { s.token = currToken }()
+ s.token = s.getTestAdminToken()
+ s.lastActivityMatches(
+ "read_host_disk_encryption_key",
+ fmt.Sprintf(`{"host_display_name": "%s", "host_id": %d}`, host.DisplayName(), host.ID),
+ 0,
+ )
+ }
+
+ // we're about to mess up with the token, make sure to set it to the
+ // default value when the test ends
+ currToken := s.token
+ t.Cleanup(func() { s.token = currToken })
+
+ // admins are able to see the host encryption key
+ s.token = s.getTestAdminToken()
+ checkDecryptableKey(s.users["admin1@example.com"])
+
+ // maintainers are able to see the token
+ u := s.users["user1@example.com"]
+ s.token = s.getTestToken(u.Email, test.GoodPassword)
+ checkDecryptableKey(u)
+
+ // observers are able to see the token
+ u = s.users["user2@example.com"]
+ s.token = s.getTestToken(u.Email, test.GoodPassword)
+ checkDecryptableKey(u)
+
+ // add the host to a team
+ team, err := s.ds.NewTeam(context.Background(), &fleet.Team{
+ ID: 4827,
+ Name: "team1_" + t.Name(),
+ Description: "desc team1_" + t.Name(),
+ })
+ require.NoError(t, err)
+ err = s.ds.AddHostsToTeam(ctx, &team.ID, []uint{host.ID})
+ require.NoError(t, err)
+
+ // admins are still able to see the token
+ s.token = s.getTestAdminToken()
+ checkDecryptableKey(s.users["admin1@example.com"])
+
+ // maintainers are still able to see the token
+ u = s.users["user1@example.com"]
+ s.token = s.getTestToken(u.Email, test.GoodPassword)
+ checkDecryptableKey(u)
+
+ // observers are still able to see the token
+ u = s.users["user2@example.com"]
+ s.token = s.getTestToken(u.Email, test.GoodPassword)
+ checkDecryptableKey(u)
+
+ // add a team member
+ u = fleet.User{
+ Name: "test team user",
+ Email: "user1+team@example.com",
+ GlobalRole: nil,
+ Teams: []fleet.UserTeam{
+ {
+ Team: *team,
+ Role: fleet.RoleMaintainer,
+ },
+ },
+ }
+ require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
+ _, err = s.ds.NewUser(ctx, &u)
+ require.NoError(t, err)
+
+ // members are able to see the token
+ s.token = s.getTestToken(u.Email, test.GoodPassword)
+ checkDecryptableKey(u)
+
+ // create a separate team
+ team2, err := s.ds.NewTeam(context.Background(), &fleet.Team{
+ ID: 4828,
+ Name: "team2_" + t.Name(),
+ Description: "desc team2_" + t.Name(),
+ })
+ require.NoError(t, err)
+ // add a team member
+ u = fleet.User{
+ Name: "test team user",
+ Email: "user1+team2@example.com",
+ GlobalRole: nil,
+ Teams: []fleet.UserTeam{
+ {
+ Team: *team2,
+ Role: fleet.RoleMaintainer,
+ },
+ },
+ }
+ require.NoError(t, u.SetPassword(test.GoodPassword, 10, 10))
+ _, err = s.ds.NewUser(ctx, &u)
+ require.NoError(t, err)
+
+ // non-members aren't able to see the token
+ s.token = s.getTestToken(u.Email, test.GoodPassword)
+ resp = getHostEncryptionKeyResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/encryption_key", host.ID), nil, http.StatusForbidden, &resp)
+
+}
+
type device struct {
uuid string
serial string
diff --git a/server/service/osquery_utils/gen_queries_doc.go b/server/service/osquery_utils/gen_queries_doc.go
index e6f039dc08..8759a9c83c 100644
--- a/server/service/osquery_utils/gen_queries_doc.go
+++ b/server/service/osquery_utils/gen_queries_doc.go
@@ -23,7 +23,7 @@ func main() {
App: config.AppConfig{
EnableScheduledQueryStats: true,
},
- }, &fleet.Features{
+ }, nil, &fleet.Features{
EnableSoftwareInventory: true,
EnableHostUsers: true,
})
diff --git a/server/service/testing_client.go b/server/service/testing_client.go
index 151552064f..0d95720da0 100644
--- a/server/service/testing_client.go
+++ b/server/service/testing_client.go
@@ -304,3 +304,26 @@ func (ts *withServer) LoginSSOUser(username, password string) (fleet.Auth, strin
require.NoError(t, err)
return auth, string(body)
}
+
+// gets the latest activity and checks that it matches any provided properties.
+// empty string or 0 id means do not check that property. It returns the ID of that
+// latest activity.
+func (ts *withServer) lastActivityMatches(name, details string, id uint) uint {
+ t := ts.s.T()
+ var listActivities listActivitiesResponse
+ ts.DoJSON("GET", "/api/latest/fleet/activities", nil, http.StatusOK, &listActivities, "order_key", "a.id", "order_direction", "desc", "per_page", "1")
+ require.True(t, len(listActivities.Activities) > 0)
+
+ act := listActivities.Activities[0]
+ if name != "" {
+ assert.Equal(t, name, act.Type)
+ }
+ if details != "" {
+ require.NotNil(t, act.Details)
+ assert.JSONEq(t, details, string(*act.Details))
+ }
+ if id > 0 {
+ assert.Equal(t, id, act.ID)
+ }
+ return act.ID
+}