[rfc] Adds RFC for saved objects encrypted attributes. (#33740)

Co-authored-by: Nicholas Dziedzic <nicholas.dziedzic@elastic.co>
This commit is contained in:
nicknak 2019-04-30 02:31:02 -04:00 committed by Aleh Zasypkin
parent efcfc8e73e
commit 7b3146d781

View file

@ -0,0 +1,252 @@
- Start Date: 2019-03-22
- RFC PR: [#33740](https://github.com/elastic/kibana/pull/33740)
- Kibana Issue: (leave this empty)
# Summary
In order to support the action service we need a way to encrypt/decrypt
attributes on saved objects that works with security and spaces filtering as
well as performing audit logging. Sufficiently hides the private key used and
removes encrypted attributes from being exposed through regular means.
# Basic example
Register saved object type with the `encrypted_saved_objects` plugin:
```typescript
server.plugins.encrypted_saved_objects.registerType({
type: 'server-action',
attributesToEncrypt: new Set(['credentials', 'apiKey']),
});
```
Use the same API to create saved objects with encrypted attributes as for any other saved object type:
```typescript
const savedObject = await server.savedObjects
.getScopedSavedObjectsClient(request)
.create('server-action', {
name: 'my-server-action',
data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
credentials: { username: 'some-user', password: 'some-password' },
apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb'
});
// savedObject = {
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
// type: 'server-action',
// name: 'my-server-action',
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
// };
```
Use dedicated method to retrieve saved object with decrypted attributes on behalf of Kibana internal user:
```typescript
const savedObject = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
'server-action',
'dd9750b9-ef0a-444c-8405-4dfcc2e9d670'
);
// savedObject = {
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
// type: 'server-action',
// name: 'my-server-action',
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
// credentials: { username: 'some-user', password: 'some-password' },
// apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb',
// };
```
# Motivation
Main motivation is the storage and usage of third-party credentials for use with
the action service to do notifications. Also perform other types integrations,
call webhooks using tokens.
# Detailed design
In order for this to be in basic it needs to be done as a wrapper around the
saved object client. This can be added from the `x-pack` plugin.
## General
To be able to manage saved objects with encrypted attributes from any plugin one should
do the following:
1. Define `encrypted_saved_objects` plugin as a dependency.
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. This makes defining of attributes that require encryption explicit and auditable, and significantly
simplifies implementation:
```json
{
"server-action": {
"properties": {
"name": { "type": "keyword" },
"data": {
"properties": {
"location": { "type": "geo_shape" },
"email": { "type": "text" }
}
},
"credentials": { "type": "binary" },
"apiKey": { "type": "binary" }
}
}
}
```
3. Register saved object type and attributes that should be encrypted with `encrypted_saved_objects` plugin:
```typescript
server.plugins.encrypted_saved_objects.registerType({
type: 'server-action',
attributesToEncrypt: new Set(['credentials', 'apiKey']),
attributesToExcludeFromAAD: new Set(['data']),
});
```
Notice the optional `attributesToExcludeFromAAD` property, it allows one to exclude some of the saved object attributes
from Additional authenticated data (AAD), read more about that below in `Encryption and decryption` section.
Since `encrypted_saved_objects` adds its own wrapper (`EncryptedSavedObjectsClientWrapper`) into `SavedObjectsClient`
wrapper chain consumers will be able to create, update, delete and retrieve saved objects using standard Saved Objects API.
Two main responsibilities of the wrapper are:
* It encrypts attributes that are supposed to be encrypted during `create`, `bulkCreate` and `update` operations
* It strips encrypted attributes from **any** saved object returned from the Saved Objects API
As noted above the wrapper is stripping encrypted attributes from saved objects returned from the API methods, that means
that there is no way at all to retrieve encrypted attributes using standard Saved Objects API unless `encrypted_saved_objects`
plugin is disabled. This potentially can lead to the situation when consumer retrieves saved object, updates its non-encrypted
properties and passes that same object to the `update` Saved Objects API method without re-defining encrypted attributes. In
this case only specified attributes will be updated and encrypted attributes will stay untouched. And if these updated
attributes are included into AAD, that is true by default for all attributes unless they are specifically excluded via
`attributesToExcludeFromAAD`, then it will be no longer possible to decrypt encrypted attributes. At this stage we consider
this as a developer mistake and don't prevent it from happening in any way apart from logging this type of event. Partial
update of only attributes that are not the part of AAD will not cause this issue.
Saved object ID is an essential part of AAD used during encryption process and hence should be as hard to guess as possible.
To fulfil this requirement wrapper generates highly random IDs (UUIDv4) for the saved objects that contain encrypted
attributes and hence consumers are not allowed to specify ID when calling `create` or `bulkCreate` method and if they try
to do so the error will be thrown.
To reduce the risk of unintentional decryption and consequent leaking of the sensitive information there is only one way
to retrieve saved object and decrypt its encrypted attributes and it's exposed only through `encrypted_saved_objects` plugin:
```typescript
const savedObject = await server.plugins.encrypted_saved_objects.getDecryptedAsInternalUser(
'server-action',
'dd9750b9-ef0a-444c-8405-4dfcc2e9d670'
);
// savedObject = {
// id: 'dd9750b9-ef0a-444c-8405-4dfcc2e9d670',
// type: 'server-action',
// name: 'my-server-action',
// data: { location: 'BBOX (100.0, ..., 0.0)', email: '<html>...</html>' },
// credentials: { username: 'some-user', password: 'some-password' },
// apiKey: 'dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb',
// };
```
As can be seen from the method name, the request to retrieve saved object and decrypt its attributes is performed on
behalf of the internal Kibana user and hence isn't supposed to be called within user request context.
**Note:** the fact that saved object with encrypted attributes is created using standard Saved Objects API within a
particular user and space context, but retrieved out of any context makes it unclear how consumers are supposed to
provide that context and retrieve saved object from a particular space. Current plan for `getDecryptedAsInternalUser`
method is to accept a third `BaseOptions` argument that allows consumers to specify `namespace` that they can retrieve
from the request using public `spaces` plugin API.
## Encryption and decryption
Saved object attributes are encrypted using [@elastic/node-crypto](https://github.com/elastic/node-crypto) library. Please
take a look at the source code of this library to know how encryption is performed exactly, what algorithm and encryption
parameters are used, but in short it's AES Encryption with AES-256-GCM that uses random initialization vector and salt.
As with encryption key for Kibana's session cookie, master encryption key used by `encrypted_saved_objects` plugin can be
defined as a configuration value (`xpack.encrypted_saved_objects.encryptionKey`) via `kibana.yml`, but it's **highly
recommended** to define this key in the [Kibana Keystore](https://www.elastic.co/guide/en/kibana/current/secure-settings.html)
instead. The master key should be cryptographically safe and be equal or greater than 32 bytes.
To prevent certain vectors of attacks where raw content of encrypted attributes of one saved object is copied to another
saved object which would unintentionally allow it to decrypt content that was not supposed to be decrypted we rely on Additional
authenticated data (AAD) during encryption and decryption. AAD consists of the following components:
* Saved object ID
* Saved object type
* Saved object attributes
AAD does not include encrypted attributes themselves and attributes defined in optional `attributesToExcludeFromAAD`
parameter provided during saved object type registration with `encrypted_saved_objects` plugin. There are a number of
reasons why one would want to exclude certain attributes from AAD:
* if attribute contains large amount of data that can significantly slow down encryption and decryption, especially during
bulk operations (e.g. large geo shape or arbitrary HTML document)
* if attribute contains data that is supposed to be updated separately from encrypted attributes or attributes included
into AAD (e.g some user defined content associated with the email action or alert)
## Audit
Encrypted attributes will most likely contain sensitive information and any attempt to access these should be properly
logged to allow any further audit procedures. The following events will be logged with Kibana audit log functionality:
* Successful attempt to encrypt attributes (incl. saved object ID, type and attributes names)
* Failed attempt to encrypt attribute (incl. saved object ID, type and attribute name)
* Successful attempt to decrypt attributes (incl. saved object ID, type and attributes names)
* Failed attempt to decrypt attribute (incl. saved object ID, type and attribute name)
In addition to audit log events we'll issue ordinary log events for any attempts to save, update or decrypt saved objects
with missing attributes that were supposed to be encrypted/decrypted based on the registration parameters.
# Benefits
* None of the registered types will expose their encrypted details. The saved
objects with their unencrypted attributes could still be obtained and searched
on. The wrapper will follow all the security and spaces filtering of saved
objects so that only users with appropriate permissions will be able to obtain
the scrubbed objects or _save_ objects with encrypted attributes.
* No explicit access to a method that takes in an encrypted string exists. If the
type was not registered no decryption is possible. No need to handle the saved object
with the encrypted attributes reducing the risk of accidentally returning it in a
handler.
# Drawbacks
* It isn't possible to decrypt existing encrypted attributes once encryption key changes
* Possibly have a performance impact on Saved Objects API operations that require encryption/decryption
* Will require non trivial tests to test functionality along with spaces and security
* The attributes that are encrypted have to be defined and if they change they need to be migrated
# Out of scope
* Encryption key rotation mechanism, either regular or emergency
* Mechanism that would detect and warn when Kibana does not use keystore to store encryption key
# Alternatives
Only allow this to be used within the Actions service itself where the details
of the saved object are handled there directly. And the saved objects are
`hidden` but still use the security and spaces wrappers.
# Adoption strategy
Integration should be pretty easy which would include depending on the plugin, registering the desired saved object type
with it and defining encrypted attributes in the `mappings.json`.
# How we teach this
The `encrypted_saved_objects` as the name of the `thing` where it's seen as a separate
extension on top of the saved object service.
Provide a README.md in the plugin directory with the usage examples.
# Unresolved questions
* Is it acceptable to have this plugin in Basic?
* Are there any other use-cases that are not served with that interface?
* How would this work with Saved Objects Export\Import API?
* How would this work with migrations, if the attribute names wanted to be
changed, a decrypt context would need to be created for migration?