[Fleet] Use new Fleet Secrets ES APIs instead of reading/writing secrets index (#163075)

Closes https://github.com/elastic/kibana/issues/162915

## Summary
Replace direct calls to Fleet Secrets index with new API calls
introduced with https://github.com/elastic/elasticsearch/pull/97728

### New ES secrets APIs:
```
POST /_fleet/secret/
{
  "value": "<secret value>"
}

// Returns the id of the created secret
{
  "id": "<secret_id>"
}

DELETE /_fleet/secret/<secret_id>

// returns 
{
  "deleted": true
}
```

NOTE: I tried running the secrets integration tests in
https://github.com/elastic/kibana/issues/162732 but there is some ES
error that I'm not sure how to address. I think that the test can be
worked on separately

### Testing

Testing steps are the exact same as
https://github.com/elastic/kibana/pull/157176:
- Start EPR locally loading the `Secrets` test package from Kibana:

```
docker run -p 8080:8080 -v /Users/<YOUR_PATH>/kibana/x-pack/test/fleet_api_integration/apis/fixtures/test_packages:/packages/test-packages -v /Users/<YOUR_PATH>/kibana/x-pack/test/fleet_api_integration/apis/fixtures/package_registry_config.yml:/package-registry/config.yml docker.elastic.co/package-registry/package-registry:main
```
- Point `kibana.dev.yml` to local EPR:
```
  xpack.fleet.registryUrl: http://localhost:8080
```
- Enable the secrets feature flag `secretsStorage`
- Start kibana and navigate to `integrations`, install `Secrets`
package.
- It should create and edit the package policy successfully

<img width="1800" alt="Screenshot 2023-08-08 at 16 26 52"
src="5e2b77d9-71a9-4c5f-8b3b-5fc6546d562f">

- The yml policy should have the redacted secrets and secrets ids:

<img width="771" alt="Screenshot 2023-08-08 at 15 43 22"
src="7db22c6b-b0db-4eb6-bc68-7174374c9c74">

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Cristina Amico 2023-08-08 18:05:31 +02:00 committed by GitHub
parent 0ba941ac2a
commit c249c30d3b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 88 deletions

View file

@ -5,4 +5,4 @@
* 2.0. * 2.0.
*/ */
export const SECRETS_INDEX = '.fleet-secrets'; export const SECRETS_ENDPOINT_PATH = '/_fleet/secret';

View file

@ -4,10 +4,9 @@
* 2.0; you may not use this file except in compliance with the Elastic License * 2.0; you may not use this file except in compliance with the Elastic License
* 2.0. * 2.0.
*/ */
import type { PackagePolicyConfigRecordEntry } from '../..';
export interface Secret { export interface Secret {
id: string; id: string;
value: string;
} }
export interface SecretElasticDoc { export interface SecretElasticDoc {
@ -18,7 +17,19 @@ export interface VarSecretReference {
id: string; id: string;
isSecretRef: true; isSecretRef: true;
} }
export interface SecretPath {
path: string;
value: PackagePolicyConfigRecordEntry;
}
// this is used in the top level secret_refs array on package and agent policies // this is used in the top level secret_refs array on package and agent policies
export interface PolicySecretReference { export interface PolicySecretReference {
id: string; id: string;
} }
export interface DeletedSecretResponse {
deleted: boolean;
}
export interface DeletedSecretReference {
id: string;
deleted: boolean;
}

View file

@ -78,7 +78,7 @@ export {
// Message signing service // Message signing service
MESSAGE_SIGNING_SERVICE_API_ROUTES, MESSAGE_SIGNING_SERVICE_API_ROUTES,
// secrets // secrets
SECRETS_INDEX, SECRETS_ENDPOINT_PATH,
} from '../../common/constants'; } from '../../common/constants';
export { export {

View file

@ -755,7 +755,6 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
packageInfo: pkgInfo, packageInfo: pkgInfo,
esClient, esClient,
}); });
restOfPackagePolicy = secretsRes.packagePolicyUpdate; restOfPackagePolicy = secretsRes.packagePolicyUpdate;
secretReferences = secretsRes.secretReferences; secretReferences = secretsRes.secretReferences;
secretsToDelete = secretsRes.secretsToDelete; secretsToDelete = secretsRes.secretsToDelete;

View file

@ -6,19 +6,13 @@
*/ */
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server'; import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types';
import { keyBy, partition } from 'lodash'; import { keyBy } from 'lodash';
import { set } from '@kbn/safer-lodash-set'; import { set } from '@kbn/safer-lodash-set';
import { packageHasNoPolicyTemplates } from '../../common/services/policy_template'; import { packageHasNoPolicyTemplates } from '../../common/services/policy_template';
import type { import type { NewPackagePolicy, RegistryStream, UpdatePackagePolicy } from '../../common';
NewPackagePolicy,
PackagePolicyConfigRecordEntry,
RegistryStream,
UpdatePackagePolicy,
} from '../../common';
import { SO_SEARCH_LIMIT } from '../../common'; import { SO_SEARCH_LIMIT } from '../../common';
import { import {
@ -34,75 +28,61 @@ import type {
Secret, Secret,
VarSecretReference, VarSecretReference,
PolicySecretReference, PolicySecretReference,
SecretPath,
DeletedSecretResponse,
DeletedSecretReference,
} from '../types'; } from '../types';
import { FleetError } from '../errors'; import { FleetError } from '../errors';
import { SECRETS_INDEX } from '../constants'; import { SECRETS_ENDPOINT_PATH } from '../constants';
import { retryTransientEsErrors } from './epm/elasticsearch/retry';
import { auditLoggingService } from './audit_logging'; import { auditLoggingService } from './audit_logging';
import { appContextService } from './app_context'; import { appContextService } from './app_context';
import { packagePolicyService } from './package_policy'; import { packagePolicyService } from './package_policy';
interface SecretPath {
path: string;
value: PackagePolicyConfigRecordEntry;
}
// This will be removed once the secrets index PR is merged into elasticsearch
function getSecretsIndex() {
const testIndex = appContextService.getConfig()?.developer?.testSecretsIndex;
if (testIndex) {
return testIndex;
}
return SECRETS_INDEX;
}
export async function createSecrets(opts: { export async function createSecrets(opts: {
esClient: ElasticsearchClient; esClient: ElasticsearchClient;
values: string[]; values: string[];
}): Promise<Secret[]> { }): Promise<Secret[]> {
const { esClient, values } = opts; const { esClient, values } = opts;
const logger = appContextService.getLogger(); const logger = appContextService.getLogger();
const body = values.flatMap((value) => [
{ const secretsResponse: Secret[] = await Promise.all(
create: { _index: getSecretsIndex() }, values.map(async (value) => {
}, try {
{ value }, return await retryTransientEsErrors(
]); () =>
let res: BulkResponse; esClient.transport.request({
try { method: 'POST',
res = await esClient.bulk({ path: SECRETS_ENDPOINT_PATH,
body, body: { value },
}),
{ logger }
);
} catch (err) {
const msg = `Error creating secrets: ${err}`;
logger.error(msg);
throw new FleetError(msg);
}
})
);
secretsResponse.forEach((item) => {
auditLoggingService.writeCustomAuditLog({
message: `secret created: ${item.id}`,
event: {
action: 'secret_create',
category: ['database'],
type: ['access'],
outcome: 'success',
},
}); });
});
const [errorItems, successItems] = partition(res.items, (a) => a.create?.error); return secretsResponse;
successItems.forEach((item) => {
auditLoggingService.writeCustomAuditLog({
message: `secret created: ${item.create!._id}`,
event: {
action: 'secret_create',
category: ['database'],
type: ['access'],
outcome: 'success',
},
});
});
if (errorItems.length) {
throw new Error(JSON.stringify(errorItems));
}
return res.items.map((item, i) => ({
id: item.create!._id as string,
value: values[i],
}));
} catch (e) {
const msg = `Error creating secrets in ${getSecretsIndex()} index: ${e}`;
logger.error(msg);
throw new FleetError(msg);
}
} }
export async function deleteSecretsIfNotReferenced(opts: { export async function deleteSecretsIfNotReferenced(opts: {
@ -190,24 +170,32 @@ export async function _deleteSecrets(opts: {
}): Promise<void> { }): Promise<void> {
const { esClient, ids } = opts; const { esClient, ids } = opts;
const logger = appContextService.getLogger(); const logger = appContextService.getLogger();
const body = ids.flatMap((id) => [
{
delete: { _index: getSecretsIndex(), _id: id },
},
]);
let res: BulkResponse; const deletedRes: DeletedSecretReference[] = await Promise.all(
ids.map(async (id) => {
try {
const getDeleteRes: DeletedSecretResponse = await retryTransientEsErrors(
() =>
esClient.transport.request({
method: 'DELETE',
path: `${SECRETS_ENDPOINT_PATH}/${id}`,
}),
{ logger }
);
try { return { ...getDeleteRes, id };
res = await esClient.bulk({ } catch (err) {
body, const msg = `Error deleting secrets: ${err}`;
}); logger.error(msg);
throw new FleetError(msg);
}
})
);
const [errorItems, successItems] = partition(res.items, (a) => a.delete?.error); deletedRes.forEach((item) => {
if (item.deleted === true) {
successItems.forEach((item) => {
auditLoggingService.writeCustomAuditLog({ auditLoggingService.writeCustomAuditLog({
message: `secret deleted: ${item.delete!._id}`, message: `secret deleted: ${item.id}`,
event: { event: {
action: 'secret_delete', action: 'secret_delete',
category: ['database'], category: ['database'],
@ -215,16 +203,8 @@ export async function _deleteSecrets(opts: {
outcome: 'success', outcome: 'success',
}, },
}); });
});
if (errorItems.length) {
throw new Error(JSON.stringify(errorItems));
} }
} catch (e) { });
const msg = `Error deleting secrets from ${getSecretsIndex()} index: ${e}`;
logger.error(msg);
throw new FleetError(msg);
}
} }
export async function extractAndWriteSecrets(opts: { export async function extractAndWriteSecrets(opts: {

View file

@ -85,8 +85,11 @@ export type {
ExperimentalDataStreamFeature, ExperimentalDataStreamFeature,
Secret, Secret,
SecretElasticDoc, SecretElasticDoc,
SecretPath,
VarSecretReference, VarSecretReference,
PolicySecretReference, PolicySecretReference,
DeletedSecretResponse,
DeletedSecretReference,
PackageListItem, PackageListItem,
PackageList, PackageList,
InstallationInfo, InstallationInfo,