[8.x] Fixes bulk re-encryption for encrypted objects located in all spaces (#217625) (#218178)

# Backport

This will backport the following commits from `main` to `8.x`:
- [Fixes bulk re-encryption for encrypted objects located in all spaces
(#217625)](https://github.com/elastic/kibana/pull/217625)

<!--- Backport version: 9.6.6 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sorenlouv/backport)

<!--BACKPORT [{"author":{"name":"Jeramy
Soucy","email":"jeramy.soucy@elastic.co"},"sourceCommit":{"committedDate":"2025-04-14T19:20:38Z","message":"Fixes
bulk re-encryption for encrypted objects located in all spaces
(#217625)\n\nCloses #215534\n\n## Summary\n\nThe Encrypted Saved Objects
Key Rotation service makes use of the Saved\nObjects Bulk Update API to
re-encrypt objects. Bulk update supports an\noptional 'namespace'
parameter, per-object, defining the space to access\na specific object.
This allows objects outside of the current space to\nbe affected in the
update operation. The Key Rotation service leverages\nthis optional
parameter for each object to ensure that the re-encryption\noperation is
not limited to the current space.\n\nHowever, should a multi-namespace
encrypted object reside in all spaces,\nthe only value in the object's
namespaces property is the\n`ALL_NAMESPACES_STRING` constant '*'. As
this is not a valid single\nnamespace, the Bulk Update operation will
skip updating the object.\n\nPR resolves the issue by only providing a
object namespace for objects\nthat do not reside in all spaces. Objects
that reside in all spaces can\nbe accessed from the current space
without the need for an override.\n\nThis PR also updates unit tests to
account for this case.\n\n### Testing\n- [x] Set the encrypted saved
objects encryption key to a known value\n(either in kibana.yml or
kibana.dev.yml). For example:\n```\nxpack.encryptedSavedObjects:\n
encryptionKey: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" \n```\n- [x] Start
ES & Kibana\n- [x] You will need to set up a Fleet agent policy and
create a\nsynthetics location and monitor. The UI will guide you through
this when\nyou navigate to Observability -> Synthetics\n- [x] Create a
synthetics parameter, Observability -> Synthetics ->\nSettings, Global
Parameters tab. Use anything for a value, but be sure\nto check the
`Share across spaces` option.\n- [x] Update the kibana config to change
the encryption key, and use the\nold key as a decryption-only
key\n```\nxpack.encryptedSavedObjects:\n encryptionKey:
\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\" \n keyRotation:\n
decryptionOnlyKeys: [\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"] \n```\n- [x]
Wait for Kibana to restart\n- [x] Call the key rotation HTTP API as a
privileged user (I just used\nthe `elastic` superuser
account)\n\n`[you_kibana_endpoint]/api/encrypted_saved_objects/_rotate_key?type=synthetics-param`\n-
[x] Verify that 1 out of 1 objects were processed with 0 failures.\n-
[x] Repeat these steps from Main and note that 0 of 1
objects\nsucceeded, and there is 1 failure\n\n### Release Note\nFixes an
issue where the Saved Objects Rotate Encryption Key API would\nnot
affect sharable encrypted object types that exist in all
spaces.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"1176625dcaf8ec8ca4e4aa0b1324279ab0f2def3","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team:Security","backport:all-open","v9.1.0"],"title":"Fixes
bulk re-encryption for encrypted objects located in all
spaces","number":217625,"url":"https://github.com/elastic/kibana/pull/217625","mergeCommit":{"message":"Fixes
bulk re-encryption for encrypted objects located in all spaces
(#217625)\n\nCloses #215534\n\n## Summary\n\nThe Encrypted Saved Objects
Key Rotation service makes use of the Saved\nObjects Bulk Update API to
re-encrypt objects. Bulk update supports an\noptional 'namespace'
parameter, per-object, defining the space to access\na specific object.
This allows objects outside of the current space to\nbe affected in the
update operation. The Key Rotation service leverages\nthis optional
parameter for each object to ensure that the re-encryption\noperation is
not limited to the current space.\n\nHowever, should a multi-namespace
encrypted object reside in all spaces,\nthe only value in the object's
namespaces property is the\n`ALL_NAMESPACES_STRING` constant '*'. As
this is not a valid single\nnamespace, the Bulk Update operation will
skip updating the object.\n\nPR resolves the issue by only providing a
object namespace for objects\nthat do not reside in all spaces. Objects
that reside in all spaces can\nbe accessed from the current space
without the need for an override.\n\nThis PR also updates unit tests to
account for this case.\n\n### Testing\n- [x] Set the encrypted saved
objects encryption key to a known value\n(either in kibana.yml or
kibana.dev.yml). For example:\n```\nxpack.encryptedSavedObjects:\n
encryptionKey: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" \n```\n- [x] Start
ES & Kibana\n- [x] You will need to set up a Fleet agent policy and
create a\nsynthetics location and monitor. The UI will guide you through
this when\nyou navigate to Observability -> Synthetics\n- [x] Create a
synthetics parameter, Observability -> Synthetics ->\nSettings, Global
Parameters tab. Use anything for a value, but be sure\nto check the
`Share across spaces` option.\n- [x] Update the kibana config to change
the encryption key, and use the\nold key as a decryption-only
key\n```\nxpack.encryptedSavedObjects:\n encryptionKey:
\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\" \n keyRotation:\n
decryptionOnlyKeys: [\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"] \n```\n- [x]
Wait for Kibana to restart\n- [x] Call the key rotation HTTP API as a
privileged user (I just used\nthe `elastic` superuser
account)\n\n`[you_kibana_endpoint]/api/encrypted_saved_objects/_rotate_key?type=synthetics-param`\n-
[x] Verify that 1 out of 1 objects were processed with 0 failures.\n-
[x] Repeat these steps from Main and note that 0 of 1
objects\nsucceeded, and there is 1 failure\n\n### Release Note\nFixes an
issue where the Saved Objects Rotate Encryption Key API would\nnot
affect sharable encrypted object types that exist in all
spaces.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"1176625dcaf8ec8ca4e4aa0b1324279ab0f2def3"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/217625","number":217625,"mergeCommit":{"message":"Fixes
bulk re-encryption for encrypted objects located in all spaces
(#217625)\n\nCloses #215534\n\n## Summary\n\nThe Encrypted Saved Objects
Key Rotation service makes use of the Saved\nObjects Bulk Update API to
re-encrypt objects. Bulk update supports an\noptional 'namespace'
parameter, per-object, defining the space to access\na specific object.
This allows objects outside of the current space to\nbe affected in the
update operation. The Key Rotation service leverages\nthis optional
parameter for each object to ensure that the re-encryption\noperation is
not limited to the current space.\n\nHowever, should a multi-namespace
encrypted object reside in all spaces,\nthe only value in the object's
namespaces property is the\n`ALL_NAMESPACES_STRING` constant '*'. As
this is not a valid single\nnamespace, the Bulk Update operation will
skip updating the object.\n\nPR resolves the issue by only providing a
object namespace for objects\nthat do not reside in all spaces. Objects
that reside in all spaces can\nbe accessed from the current space
without the need for an override.\n\nThis PR also updates unit tests to
account for this case.\n\n### Testing\n- [x] Set the encrypted saved
objects encryption key to a known value\n(either in kibana.yml or
kibana.dev.yml). For example:\n```\nxpack.encryptedSavedObjects:\n
encryptionKey: \"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\" \n```\n- [x] Start
ES & Kibana\n- [x] You will need to set up a Fleet agent policy and
create a\nsynthetics location and monitor. The UI will guide you through
this when\nyou navigate to Observability -> Synthetics\n- [x] Create a
synthetics parameter, Observability -> Synthetics ->\nSettings, Global
Parameters tab. Use anything for a value, but be sure\nto check the
`Share across spaces` option.\n- [x] Update the kibana config to change
the encryption key, and use the\nold key as a decryption-only
key\n```\nxpack.encryptedSavedObjects:\n encryptionKey:
\"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\" \n keyRotation:\n
decryptionOnlyKeys: [\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"] \n```\n- [x]
Wait for Kibana to restart\n- [x] Call the key rotation HTTP API as a
privileged user (I just used\nthe `elastic` superuser
account)\n\n`[you_kibana_endpoint]/api/encrypted_saved_objects/_rotate_key?type=synthetics-param`\n-
[x] Verify that 1 out of 1 objects were processed with 0 failures.\n-
[x] Repeat these steps from Main and note that 0 of 1
objects\nsucceeded, and there is 1 failure\n\n### Release Note\nFixes an
issue where the Saved Objects Rotate Encryption Key API would\nnot
affect sharable encrypted object types that exist in all
spaces.\n\n---------\n\nCo-authored-by: kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"1176625dcaf8ec8ca4e4aa0b1324279ab0f2def3"}}]}]
BACKPORT-->

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Jeramy Soucy 2025-04-15 04:23:06 +02:00 committed by GitHub
parent a58c356711
commit cb0bdae67d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 9 deletions

View file

@ -63,8 +63,11 @@ beforeEach(() => {
{ name: 'type-id-4', namespaceType: 'multiple', mappings: { properties: {} }, hidden: true },
{ name: 'type-id-5', namespaceType: 'single', mappings: { properties: {} }, hidden: false },
{ name: 'type-id-6', namespaceType: 'single', mappings: { properties: {} }, hidden: true },
{ name: 'type-id-7', namespaceType: 'multiple', mappings: { properties: {} }, hidden: false },
]);
typeRegistryMock.isSingleNamespace.mockImplementation((type) => type !== 'type-id-4');
typeRegistryMock.isSingleNamespace.mockImplementation(
(type) => type !== 'type-id-4' && type !== 'type-id-7'
);
mockSavedObjects.getTypeRegistry.mockReturnValue(typeRegistryMock);
mockRetrieveClient = savedObjectsClientMock.create();
@ -125,7 +128,7 @@ it('does not perform rotation if there are no Saved Objects to process', async (
expect(mockRetrieveClient.find).toHaveBeenCalledTimes(1);
expect(mockRetrieveClient.find).toHaveBeenCalledWith({
type: ['type-id-1', 'type-id-2', 'type-id-4', 'type-id-5'],
type: ['type-id-1', 'type-id-2', 'type-id-4', 'type-id-5', 'type-id-7'],
perPage: 12345,
namespaces: ['*'],
sortField: 'updated_at',
@ -200,11 +203,12 @@ it('properly rotates encryption key', async () => {
getMockSavedObject({ id: 'id-1' }),
getMockSavedObject({ id: 'id-2', namespaces: ['ns-1'] }),
getMockSavedObject({ id: 'id-4', namespaces: ['ns-2', 'ns-3'] }),
getMockSavedObject({ id: 'id-7', namespaces: ['*'] }),
];
mockRetrieveClient.find.mockResolvedValue({
total: 3,
total: 4,
saved_objects: savedObjects,
per_page: 3,
per_page: 4,
page: 0,
});
mockUpdateClient.bulkUpdate.mockResolvedValue({
@ -214,12 +218,12 @@ it('properly rotates encryption key', async () => {
await expect(
service.rotate(httpServerMock.createKibanaRequest(), { batchSize: 12345 })
).resolves.toEqual({
total: 3,
successful: 3,
total: 4,
successful: 4,
failed: 0,
});
expect(mockEncryptionService.decryptAttributes).toHaveBeenCalledTimes(3);
expect(mockEncryptionService.decryptAttributes).toHaveBeenCalledTimes(4);
expect(mockEncryptionService.decryptAttributes).toHaveBeenCalledWith(
{ type: 'type-id-1', id: 'id-1' },
{ attr: 'attr-id-1' },
@ -235,12 +239,18 @@ it('properly rotates encryption key', async () => {
{ attr: 'attr-id-4' },
{ omitPrimaryEncryptionKey: true }
);
expect(mockEncryptionService.decryptAttributes).toHaveBeenCalledWith(
{ type: 'type-id-7', id: 'id-7' },
{ attr: 'attr-id-7' },
{ omitPrimaryEncryptionKey: true }
);
expect(mockUpdateClient.bulkUpdate).toHaveBeenCalledTimes(1);
expect(mockUpdateClient.bulkUpdate).toHaveBeenCalledWith([
{ ...savedObjects[0], attributes: { attr: 'decrypted-attr-id-1' } },
{ ...savedObjects[1], namespace: 'ns-1', attributes: { attr: 'decrypted-attr-id-2' } },
{ ...savedObjects[2], namespace: 'ns-2', attributes: { attr: 'decrypted-attr-id-4' } },
{ ...savedObjects[3], attributes: { attr: 'decrypted-attr-id-7' } },
]);
});

View file

@ -14,6 +14,7 @@ import type {
StartServicesAccessor,
} from '@kbn/core/server';
import { ENCRYPTION_EXTENSION_ID } from '@kbn/core-saved-objects-server';
import { ALL_NAMESPACES_STRING } from '@kbn/core-saved-objects-utils-server';
import type { AuthenticatedUser } from '@kbn/core-security-common';
import type { PublicMethodsOf } from '@kbn/utility-types';
@ -258,11 +259,14 @@ export class EncryptionKeyRotationService {
continue;
}
const firstNamespace = savedObject.namespaces?.[0];
decryptedSavedObjects.push({
...savedObject,
attributes: decryptedAttributes,
// `bulkUpdate` expects objects with a single `namespace`.
namespace: savedObject.namespaces?.[0],
// The optional object namespace for `bulkUpdate` is used to affect objects outside of the current space
// '*' is an invalid option, and if the object exists in all spaces, we don't need to set the namespace
namespace: firstNamespace !== ALL_NAMESPACES_STRING ? firstNamespace : undefined,
});
}

View file

@ -15,6 +15,7 @@
"@kbn/core-security-common",
"@kbn/test-jest-helpers",
"@kbn/config",
"@kbn/core-saved-objects-utils-server",
],
"exclude": [
"target/**/*",