-
Notifications
You must be signed in to change notification settings - Fork 8.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce
Encrypted Saved Objects
plugin (#34526)
- Loading branch information
Showing
25 changed files
with
2,911 additions
and
7 deletions.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# Encrypted Saved Objects | ||
|
||
## Overview | ||
|
||
The purpose of this plugin is to provide a way to encrypt/decrypt attributes on the custom Saved Objects that works with | ||
security and spaces filtering as well as performing audit logging. | ||
|
||
[RFC #2: Encrypted Saved Objects Attributes](../../../rfcs/text/0002_encrypted_attributes.md). | ||
|
||
## Usage | ||
|
||
Follow these steps to use `encrypted_saved_objects` in your plugin: | ||
|
||
1. Declare `encrypted_saved_objects` as a dependency: | ||
|
||
```typescript | ||
... | ||
new kibana.Plugin({ | ||
... | ||
require: ['encrypted_saved_objects'], | ||
... | ||
}); | ||
``` | ||
|
||
2. Add attributes to be encrypted in `mappings.json` file for the respective Saved Object type. These attributes should | ||
always have a `binary` type since they'll contain encrypted content as a `Base64` encoded string and should never be | ||
searchable or analyzed: | ||
|
||
```json | ||
{ | ||
"my-saved-object-type": { | ||
"properties": { | ||
"name": { "type": "keyword" }, | ||
"mySecret": { "type": "binary" } | ||
} | ||
} | ||
} | ||
``` | ||
|
||
3. Register Saved Object type using the provided API: | ||
|
||
```typescript | ||
server.plugins.encrypted_saved_objects.registerType({ | ||
type: 'my-saved-object-type', | ||
attributesToEncrypt: new Set(['mySecret']), | ||
}); | ||
``` | ||
|
||
4. For any Saved Object operation that does not require retrieval of decrypted content, use standard REST or | ||
programmatic Saved Object API, e.g.: | ||
|
||
```typescript | ||
... | ||
async handler(request: Request) { | ||
return await server.savedObjects | ||
.getScopedSavedObjectsClient(request) | ||
.create('my-saved-object-type', { name: 'some name', mySecret: 'non encrypted secret' }); | ||
} | ||
... | ||
``` | ||
|
||
5. To retrieve Saved Object with decrypted content use the dedicated `getDecryptedAsInternalUser` API method. | ||
|
||
**Note:** As name suggests the method will retrieve the encrypted values and decrypt them on behalf of the internal Kibana | ||
user to make it possible to use this method even when user request context is not available (e.g. in background tasks). | ||
Hence this method should only be used wherever consumers would otherwise feel comfortable using `callWithInternalUser` | ||
and preferably only as a part of the Kibana server routines that are outside of the lifecycle of a HTTP request that a | ||
user has control over. | ||
|
||
```typescript | ||
const savedObjectWithDecryptedContent = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser( | ||
'my-saved-object-type', | ||
'saved-object-id' | ||
); | ||
``` | ||
|
||
`getDecryptedAsInternalUser` also accepts the 3rd optional `options` argument that has exactly the same type as `options` | ||
one would pass to `SavedObjectsClient.get`. These argument allows to specify `namespace` property that, for example, is | ||
required if Saved Object was created within a non-default space. | ||
|
||
## Testing | ||
|
||
### Unit tests | ||
|
||
From `kibana-root-folder/x-pack`, run: | ||
```bash | ||
$ node scripts/jest.js | ||
``` | ||
|
||
### API Integration tests | ||
|
||
In one shell, from `kibana-root-folder/x-pack`: | ||
```bash | ||
$ node scripts/functional_tests_server.js --config test/plugin_api_integration/config.js | ||
``` | ||
|
||
In another shell, from `kibana-root-folder/x-pack`: | ||
```bash | ||
$ node ../scripts/functional_test_runner.js --config test/plugin_api_integration/config.js --grep="{TEST_NAME}" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { Root } from 'joi'; | ||
import { Legacy, Server } from 'kibana'; | ||
|
||
// @ts-ignore | ||
import { AuditLogger } from '../../server/lib/audit_logger'; | ||
|
||
import { CONFIG_PREFIX, PLUGIN_ID, Plugin } from './server/plugin'; | ||
|
||
export const encryptedSavedObjects = (kibana: any) => | ||
new kibana.Plugin({ | ||
id: PLUGIN_ID, | ||
configPrefix: CONFIG_PREFIX, | ||
require: ['kibana', 'elasticsearch', 'xpack_main'], | ||
|
||
config(Joi: Root) { | ||
return Joi.object({ | ||
enabled: Joi.boolean().default(true), | ||
encryptionKey: Joi.string().min(32), | ||
}).default(); | ||
}, | ||
|
||
async init(server: Legacy.Server) { | ||
const loggerFacade = { | ||
fatal: (errorOrMessage: string | Error) => server.log(['fatal', PLUGIN_ID], errorOrMessage), | ||
trace: (message: string) => server.log(['debug', PLUGIN_ID], message), | ||
error: (message: string) => server.log(['error', PLUGIN_ID], message), | ||
warn: (message: string) => server.log(['warning', PLUGIN_ID], message), | ||
debug: (message: string) => server.log(['debug', PLUGIN_ID], message), | ||
info: (message: string) => server.log(['info', PLUGIN_ID], message), | ||
} as Server.Logger; | ||
|
||
const config = server.config(); | ||
const encryptedSavedObjectsSetup = new Plugin(loggerFacade).setup( | ||
{ | ||
config: { | ||
encryptionKey: config.get<string | undefined>(`${CONFIG_PREFIX}.encryptionKey`), | ||
}, | ||
savedObjects: server.savedObjects, | ||
elasticsearch: server.plugins.elasticsearch, | ||
}, | ||
{ audit: new AuditLogger(server, PLUGIN_ID, config, server.plugins.xpack_main.info) } | ||
); | ||
|
||
// Re-expose plugin setup contract through legacy mechanism. | ||
for (const [setupMethodName, setupMethod] of Object.entries(encryptedSavedObjectsSetup)) { | ||
server.expose(setupMethodName, setupMethod); | ||
} | ||
}, | ||
}); |
107 changes: 107 additions & 0 deletions
107
...k/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_audit_logger.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { EncryptedSavedObjectsAuditLogger } from './encrypted_saved_objects_audit_logger'; | ||
|
||
test('properly logs audit events', () => { | ||
const mockInternalAuditLogger = { log: jest.fn() }; | ||
const audit = new EncryptedSavedObjectsAuditLogger(mockInternalAuditLogger); | ||
|
||
audit.encryptAttributesSuccess(['one', 'two'], { | ||
type: 'known-type', | ||
id: 'object-id', | ||
}); | ||
audit.encryptAttributesSuccess(['one', 'two'], { | ||
type: 'known-type-ns', | ||
id: 'object-id-ns', | ||
namespace: 'object-ns', | ||
}); | ||
|
||
audit.decryptAttributesSuccess(['three', 'four'], { | ||
type: 'known-type-1', | ||
id: 'object-id-1', | ||
}); | ||
audit.decryptAttributesSuccess(['three', 'four'], { | ||
type: 'known-type-1-ns', | ||
id: 'object-id-1-ns', | ||
namespace: 'object-ns', | ||
}); | ||
|
||
audit.encryptAttributeFailure('five', { | ||
type: 'known-type-2', | ||
id: 'object-id-2', | ||
}); | ||
audit.encryptAttributeFailure('five', { | ||
type: 'known-type-2-ns', | ||
id: 'object-id-2-ns', | ||
namespace: 'object-ns', | ||
}); | ||
|
||
audit.decryptAttributeFailure('six', { | ||
type: 'known-type-3', | ||
id: 'object-id-3', | ||
}); | ||
audit.decryptAttributeFailure('six', { | ||
type: 'known-type-3-ns', | ||
id: 'object-id-3-ns', | ||
namespace: 'object-ns', | ||
}); | ||
|
||
expect(mockInternalAuditLogger.log).toHaveBeenCalledTimes(8); | ||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'encrypt_success', | ||
'Successfully encrypted attributes "[one,two]" for saved object "[known-type,object-id]".', | ||
{ id: 'object-id', type: 'known-type', attributesNames: ['one', 'two'] } | ||
); | ||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'encrypt_success', | ||
'Successfully encrypted attributes "[one,two]" for saved object "[object-ns,known-type-ns,object-id-ns]".', | ||
{ | ||
id: 'object-id-ns', | ||
type: 'known-type-ns', | ||
namespace: 'object-ns', | ||
attributesNames: ['one', 'two'], | ||
} | ||
); | ||
|
||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'decrypt_success', | ||
'Successfully decrypted attributes "[three,four]" for saved object "[known-type-1,object-id-1]".', | ||
{ id: 'object-id-1', type: 'known-type-1', attributesNames: ['three', 'four'] } | ||
); | ||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'decrypt_success', | ||
'Successfully decrypted attributes "[three,four]" for saved object "[object-ns,known-type-1-ns,object-id-1-ns]".', | ||
{ | ||
id: 'object-id-1-ns', | ||
type: 'known-type-1-ns', | ||
namespace: 'object-ns', | ||
attributesNames: ['three', 'four'], | ||
} | ||
); | ||
|
||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'encrypt_failure', | ||
'Failed to encrypt attribute "five" for saved object "[known-type-2,object-id-2]".', | ||
{ id: 'object-id-2', type: 'known-type-2', attributeName: 'five' } | ||
); | ||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'encrypt_failure', | ||
'Failed to encrypt attribute "five" for saved object "[object-ns,known-type-2-ns,object-id-2-ns]".', | ||
{ id: 'object-id-2-ns', type: 'known-type-2-ns', namespace: 'object-ns', attributeName: 'five' } | ||
); | ||
|
||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'decrypt_failure', | ||
'Failed to decrypt attribute "six" for saved object "[known-type-3,object-id-3]".', | ||
{ id: 'object-id-3', type: 'known-type-3', attributeName: 'six' } | ||
); | ||
expect(mockInternalAuditLogger.log).toHaveBeenCalledWith( | ||
'decrypt_failure', | ||
'Failed to decrypt attribute "six" for saved object "[object-ns,known-type-3-ns,object-id-3-ns]".', | ||
{ id: 'object-id-3-ns', type: 'known-type-3-ns', namespace: 'object-ns', attributeName: 'six' } | ||
); | ||
}); |
60 changes: 60 additions & 0 deletions
60
x-pack/plugins/encrypted_saved_objects/server/lib/encrypted_saved_objects_audit_logger.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { SavedObjectDescriptor, descriptorToArray } from './encrypted_saved_objects_service'; | ||
|
||
/** | ||
* Represents all audit events the plugin can log. | ||
*/ | ||
export class EncryptedSavedObjectsAuditLogger { | ||
constructor(private readonly auditLogger: any) {} | ||
|
||
public encryptAttributeFailure(attributeName: string, descriptor: SavedObjectDescriptor) { | ||
this.auditLogger.log( | ||
'encrypt_failure', | ||
`Failed to encrypt attribute "${attributeName}" for saved object "[${descriptorToArray( | ||
descriptor | ||
)}]".`, | ||
{ ...descriptor, attributeName } | ||
); | ||
} | ||
|
||
public decryptAttributeFailure(attributeName: string, descriptor: SavedObjectDescriptor) { | ||
this.auditLogger.log( | ||
'decrypt_failure', | ||
`Failed to decrypt attribute "${attributeName}" for saved object "[${descriptorToArray( | ||
descriptor | ||
)}]".`, | ||
{ ...descriptor, attributeName } | ||
); | ||
} | ||
|
||
public encryptAttributesSuccess( | ||
attributesNames: ReadonlyArray<string>, | ||
descriptor: SavedObjectDescriptor | ||
) { | ||
this.auditLogger.log( | ||
'encrypt_success', | ||
`Successfully encrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray( | ||
descriptor | ||
)}]".`, | ||
{ ...descriptor, attributesNames } | ||
); | ||
} | ||
|
||
public decryptAttributesSuccess( | ||
attributesNames: ReadonlyArray<string>, | ||
descriptor: SavedObjectDescriptor | ||
) { | ||
this.auditLogger.log( | ||
'decrypt_success', | ||
`Successfully decrypted attributes "[${attributesNames}]" for saved object "[${descriptorToArray( | ||
descriptor | ||
)}]".`, | ||
{ ...descriptor, attributesNames } | ||
); | ||
} | ||
} |
Oops, something went wrong.