Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.2] [Fleet] Encrypt ssl fields in logstash output (#129131) #129525

Merged
merged 1 commit into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
kibana: {
guide: `${KIBANA_DOCS}index.html`,
autocompleteSuggestions: `${KIBANA_DOCS}kibana-concepts-analysts.html#autocomplete-suggestions`,
secureSavedObject: `${KIBANA_DOCS}xpack-security-secure-saved-objects.html`,
xpackSecurity: `${KIBANA_DOCS}xpack-security.html`,
},
upgradeAssistant: {
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export interface DocLinks {
readonly kibana: {
readonly guide: string;
readonly autocompleteSuggestions: string;
readonly secureSavedObject: string;
readonly xpackSecurity: string;
};
readonly upgradeAssistant: {
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/fleet/common/types/models/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface NewOutput {

export type OutputSOAttributes = NewOutput & {
output_id?: string;
ssl?: string; // encrypted ssl field
};

export type Output = NewOutput & {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiCallOut, EuiLink } from '@elastic/eui';

import { useStartServices } from '../../../../hooks';

export const EncryptionKeyRequiredCallout: React.FunctionComponent = () => {
const { docLinks } = useStartServices();
return (
<EuiCallOut
iconType="alert"
color="warning"
title={
<FormattedMessage
id="xpack.fleet.encryptionKeyRequired.calloutTitle"
defaultMessage="Additional setup required"
/>
}
>
<FormattedMessage
id="xpack.fleet.encryptionKeyRequired.calloutDescription"
defaultMessage="You must configure an encryption key before configuring this output. {link}"
values={{
link: (
<EuiLink href={docLinks.links.kibana.secureSavedObject} target="_blank" external>
<FormattedMessage
id="xpack.fleet.encryptionKeyRequired.link"
defaultMessage="Learn more"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import React from 'react';

import type { Output } from '../../../../types';
import { createFleetTestRendererMock } from '../../../../../../mock';
import { useFleetStatus } from '../../../../../../hooks/use_fleet_status';

import { EditOutputFlyout } from '.';

Expand All @@ -20,8 +21,11 @@ jest.mock('../../../../../../hooks/use_fleet_status', () => ({
FleetStatusProvider: (props: any) => {
return props.children;
},
useFleetStatus: jest.fn().mockReturnValue({}),
}));

const mockedUsedFleetStatus = useFleetStatus as jest.MockedFunction<typeof useFleetStatus>;

function renderFlyout(output?: Output) {
const renderer = createFleetTestRendererMock();

Expand Down Expand Up @@ -66,4 +70,20 @@ describe('EditOutputFlyout', () => {
expect(utils.queryByLabelText('Client SSL certificate')).not.toBeNull();
expect(utils.queryByLabelText('Server SSL certificate authorities')).not.toBeNull();
});

it('should show a callout in the flyout if the selected output is logstash and no encrypted key is set', async () => {
mockedUsedFleetStatus.mockReturnValue({
missingRequirements: ['encrypted_saved_object_encryption_key_required'],
} as any);
const { utils } = renderFlyout({
type: 'logstash',
name: 'logstash output',
id: 'output123',
is_default: false,
is_default_monitoring: false,
});

// Show logstash SSL inputs
expect(utils.getByText('Additional setup required')).not.toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { useBreadcrumbs, useStartServices } from '../../../../hooks';

import { YamlCodeEditorWithPlaceholder } from './yaml_code_editor_with_placeholder';
import { useOutputForm } from './use_output_form';
import { EncryptionKeyRequiredCallout } from './encryption_key_required_callout';

export interface EditOutputFlyoutProps {
output?: Output;
Expand All @@ -60,6 +61,9 @@ export const EditOutputFlyout: React.FunctionComponent<EditOutputFlyoutProps> =
const isLogstashOutput = inputs.typeInput.value === 'logstash';
const isESOutput = inputs.typeInput.value === 'elasticsearch';

const showLogstashNeedEncryptedSavedObjectCallout =
isLogstashOutput && !form.hasEncryptedSavedObjectConfigured;

return (
<EuiFlyout maxWidth={FLYOUT_MAX_WIDTH} onClose={onClose}>
<EuiFlyoutHeader hasBorder={true}>
Expand Down Expand Up @@ -160,6 +164,12 @@ export const EditOutputFlyout: React.FunctionComponent<EditOutputFlyoutProps> =
)}
/>
</EuiFormRow>
{showLogstashNeedEncryptedSavedObjectCallout && (
<>
<EuiSpacer size="m" />
<EncryptionKeyRequiredCallout />
</>
)}
{isLogstashOutput && (
<>
<EuiSpacer size="m" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useSwitchInput,
useStartServices,
sendPutOutput,
useFleetStatus,
} from '../../../../hooks';
import type { Output, PostOutputRequest } from '../../../../types';
import { useConfirmModal } from '../../hooks/use_confirm_modal';
Expand All @@ -32,6 +33,12 @@ import {
import { confirmUpdate } from './confirm_update';

export function useOutputForm(onSucess: () => void, output?: Output) {
const fleetStatus = useFleetStatus();

const hasEncryptedSavedObjectConfigured = !fleetStatus.missingRequirements?.includes(
'encrypted_saved_object_encryption_key_required'
);

const [isLoading, setIsloading] = useState(false);
const { notifications } = useStartServices();
const { confirm } = useConfirmModal();
Expand Down Expand Up @@ -234,6 +241,11 @@ export function useOutputForm(onSucess: () => void, output?: Output) {
inputs,
submit,
isLoading,
isDisabled: isLoading || isPreconfigured || (output && !hasChanged),
hasEncryptedSavedObjectConfigured,
isDisabled:
isLoading ||
isPreconfigured ||
(output && !hasChanged) ||
(isLogstash && !hasEncryptedSavedObjectConfigured),
};
}
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export class HostedAgentPolicyRestrictionRelatedError extends IngestManagerError
);
}
}

export class FleetEncryptedSavedObjectEncryptionKeyRequired extends IngestManagerError {}
export class FleetSetupError extends IngestManagerError {}
export class GenerateServiceTokenError extends IngestManagerError {}
export class FleetUnauthorizedError extends IngestManagerError {}
Expand Down
3 changes: 3 additions & 0 deletions x-pack/plugins/fleet/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type { Observable } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { take } from 'rxjs/operators';

import { i18n } from '@kbn/i18n';
import type {
Expand Down Expand Up @@ -416,6 +417,8 @@ export class FleetPlugin
summary: 'Fleet is setting up',
});

await plugins.licensing.license$.pipe(take(1)).toPromise();

await setupFleet(
new SavedObjectsClient(core.savedObjects.createInternalRepository()),
core.elasticsearch.client.asInternalUser
Expand Down
10 changes: 9 additions & 1 deletion x-pack/plugins/fleet/server/routes/setup/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,25 @@ export const getFleetStatusHandler: FleetRequestHandler = async (context, reques
context.core.elasticsearch.client.asInternalUser
);

let isReady = true;
const missingRequirements: GetFleetStatusResponse['missing_requirements'] = [];

if (!isApiKeysEnabled) {
isReady = false;
missingRequirements.push('api_keys');
}

if (!isFleetServerSetup) {
isReady = false;
missingRequirements.push('fleet_server');
}

if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) {
missingRequirements.push('encrypted_saved_object_encryption_key_required');
}

const body: GetFleetStatusResponse = {
isReady: missingRequirements.length === 0,
isReady,
missing_requirements: missingRequirements,
};

Expand Down
19 changes: 18 additions & 1 deletion x-pack/plugins/fleet/server/saved_objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ const getSavedObjectTypes = (
config: { type: 'flattened' },
config_yaml: { type: 'text' },
is_preconfigured: { type: 'boolean', index: false },
ssl: { type: 'flattened', index: false },
ssl: { type: 'binary' },
},
},
migrations: {
Expand Down Expand Up @@ -310,5 +310,22 @@ export function registerSavedObjects(
export function registerEncryptedSavedObjects(
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup
) {
encryptedSavedObjects.registerType({
type: OUTPUT_SAVED_OBJECT_TYPE,
attributesToEncrypt: new Set([{ key: 'ssl', dangerouslyExposeValue: true }]),
attributesToExcludeFromAAD: new Set([
'output_id',
'name',
'type',
'is_default',
'is_default_monitoring',
'hosts',
'ca_sha256',
'ca_trusted_fingerprint',
'config',
'config_yaml',
'is_preconfigured',
]),
});
// Encrypted saved objects
}
40 changes: 40 additions & 0 deletions x-pack/plugins/fleet/server/services/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ function getMockedSoClient(
};
});

mockedAppContextService.getInternalUserSOClient.mockReturnValue(soClient);

return soClient;
}

Expand All @@ -169,6 +171,8 @@ describe('Output Service', () => {
mockedAgentPolicyService.list.mockClear();
mockedAgentPolicyService.hasAPMIntegration.mockClear();
mockedAgentPolicyService.removeOutputFromAll.mockReset();
mockedAppContextService.getInternalUserSOClient.mockReset();
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReset();
});
describe('create', () => {
it('work with a predefined id', async () => {
Expand Down Expand Up @@ -321,6 +325,42 @@ describe('Output Service', () => {
{ is_default: false }
);
});

// With logstash output
it('should throw if encryptedSavedObject is not configured', async () => {
const soClient = getMockedSoClient({});

await expect(
outputService.create(
soClient,
{
is_default: false,
is_default_monitoring: false,
name: 'Test',
type: 'logstash',
},
{ id: 'output-test' }
)
).rejects.toThrow(`Logstash output needs encrypted saved object api key to be set`);
});

it('should work if encryptedSavedObject is configured', async () => {
const soClient = getMockedSoClient({});
mockedAppContextService.getEncryptedSavedObjectsSetup.mockReturnValue({
canEncrypt: true,
} as any);
await outputService.create(
soClient,
{
is_default: false,
is_default_monitoring: false,
name: 'Test',
type: 'logstash',
},
{ id: 'output-test' }
);
expect(soClient.create).toBeCalled();
});
});

describe('update', () => {
Expand Down
Loading