mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Optional ssl for fleet logstash output (#216216)
## Summary Resolves: https://github.com/elastic/kibana/issues/145266 * Allows SSL configuration to be disabled for Fleet agent logstash output * Adds an SSL toggle in the logstash output form. * On is the default state of the form * When off: * Authentication form section is removed * Logstash input config has SSL related fields removed * Submitting update removes SSL fields and related SSL secrets in output config * Shows a call out to proceed with caution
This commit is contained in:
parent
8a7860cf61
commit
cc09a96efe
7 changed files with 167 additions and 33 deletions
|
@ -144,6 +144,10 @@ describe('EditOutputFlyout', () => {
|
|||
id: 'output123',
|
||||
is_default: false,
|
||||
is_default_monitoring: false,
|
||||
ssl: {
|
||||
certificate: 'ssl-cert-value',
|
||||
key: 'ssl-key-value',
|
||||
},
|
||||
});
|
||||
expect(utils.queryByTestId('advancedSSLOptionsButton')).not.toBeNull();
|
||||
fireEvent.click(utils.getByTestId('advancedSSLOptionsButton'));
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { EuiSpacer, EuiLink } from '@elastic/eui';
|
||||
import { EuiSpacer, EuiLink, EuiSwitch, EuiCallOut } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -76,7 +76,37 @@ export const OutputFormLogstashSection: React.FunctionComponent<Props> = (props)
|
|||
</>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<LogstashInstructions />
|
||||
<EuiSwitch
|
||||
label={i18n.translate('xpack.fleet.settings.editOutputFlyout.logstashSSLSwitchLabel', {
|
||||
defaultMessage: 'Enable SSL',
|
||||
})}
|
||||
{...inputs.logstashEnableSSLInput.props}
|
||||
/>
|
||||
{!inputs.logstashEnableSSLInput.value && (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCallOut
|
||||
title={i18n.translate(
|
||||
'xpack.fleet.settings.editOutputFlyout.logstashSSLSwitchCalloutTitle',
|
||||
{ defaultMessage: 'Proceed with caution!' }
|
||||
)}
|
||||
color="warning"
|
||||
iconType="warning"
|
||||
>
|
||||
<p>
|
||||
{i18n.translate(
|
||||
'xpack.fleet.settings.editOutputFlyout.logstashSSLSwitchCalloutMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Using SSL/TLS ensures that your Elastic Agents send encrypted data to trusted Logstash servers, and that your Logstash servers receive data from trusted Elastic Agent clients.',
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</EuiCallOut>
|
||||
</>
|
||||
)}
|
||||
<EuiSpacer size="m" />
|
||||
<LogstashInstructions isSSLEnabled={inputs.logstashEnableSSLInput.value} />
|
||||
<EuiSpacer size="m" />
|
||||
<MultiRowInput
|
||||
placeholder={i18n.translate(
|
||||
|
@ -108,13 +138,15 @@ export const OutputFormLogstashSection: React.FunctionComponent<Props> = (props)
|
|||
{...inputs.logstashHostsInput.props}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<SSLFormSection
|
||||
inputs={inputs}
|
||||
useSecretsStorage={useSecretsStorage}
|
||||
isConvertedToSecret={isConvertedToSecret.sslKey}
|
||||
onToggleSecretAndClearValue={onToggleSecretAndClearValue}
|
||||
type={inputs.typeInput.value as FormType}
|
||||
/>
|
||||
{inputs.logstashEnableSSLInput.value && (
|
||||
<SSLFormSection
|
||||
inputs={inputs}
|
||||
useSecretsStorage={useSecretsStorage}
|
||||
isConvertedToSecret={isConvertedToSecret.sslKey}
|
||||
onToggleSecretAndClearValue={onToggleSecretAndClearValue}
|
||||
type={inputs.typeInput.value as FormType}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -92,6 +92,7 @@ export interface OutputFormInputsType {
|
|||
diskQueueEncryptionEnabled: ReturnType<typeof useSwitchInput>;
|
||||
diskQueueCompressionEnabled: ReturnType<typeof useSwitchInput>;
|
||||
compressionLevelInput: ReturnType<typeof useSelectInput>;
|
||||
logstashEnableSSLInput: ReturnType<typeof useSwitchInput>;
|
||||
logstashHostsInput: ReturnType<typeof useComboInput>;
|
||||
presetInput: ReturnType<typeof useInput>;
|
||||
additionalYamlConfigInput: ReturnType<typeof useInput>;
|
||||
|
@ -362,6 +363,11 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu
|
|||
|
||||
const isSSLEditable = isDisabled('ssl');
|
||||
// Logstash inputs
|
||||
const logstashEnableSSLInput = useSwitchInput(
|
||||
output?.type === 'logstash' ? Boolean(output?.ssl) : true,
|
||||
isSSLEditable
|
||||
);
|
||||
|
||||
const logstashHostsInput = useComboInput(
|
||||
'logstashHostsComboxBox',
|
||||
output?.hosts ?? [],
|
||||
|
@ -376,18 +382,20 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu
|
|||
);
|
||||
const sslCertificateInput = useInput(
|
||||
output?.ssl?.certificate ?? '',
|
||||
output?.type === 'logstash' ? validateSSLCertificate : undefined,
|
||||
output?.type === 'logstash' && logstashEnableSSLInput.value
|
||||
? validateSSLCertificate
|
||||
: undefined,
|
||||
isSSLEditable
|
||||
);
|
||||
const sslKeyInput = useInput(
|
||||
output?.ssl?.key ?? '',
|
||||
output?.type === 'logstash' ? validateSSLKey : undefined,
|
||||
output?.type === 'logstash' && logstashEnableSSLInput.value ? validateSSLKey : undefined,
|
||||
isSSLEditable
|
||||
);
|
||||
|
||||
const sslKeySecretInput = useSecretInput(
|
||||
(output as NewLogstashOutput)?.secrets?.ssl?.key,
|
||||
output?.type === 'logstash' ? validateSSLKeySecret : undefined,
|
||||
output?.type === 'logstash' && logstashEnableSSLInput.value ? validateSSLKeySecret : undefined,
|
||||
isSSLEditable
|
||||
);
|
||||
|
||||
|
@ -582,6 +590,7 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu
|
|||
diskQueueMaxSizeInput,
|
||||
diskQueueCompressionEnabled,
|
||||
compressionLevelInput,
|
||||
logstashEnableSSLInput,
|
||||
logstashHostsInput,
|
||||
presetInput,
|
||||
additionalYamlConfigInput,
|
||||
|
@ -680,7 +689,7 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu
|
|||
additionalYamlConfigValid &&
|
||||
nameInputValid &&
|
||||
sslCertificateValid &&
|
||||
((sslKeyInput.value && sslKeyValid) || (sslKeySecretInput.value && sslKeySecretValid))
|
||||
(sslKeyValid || sslKeySecretValid)
|
||||
);
|
||||
}
|
||||
if (isKafka) {
|
||||
|
@ -943,19 +952,23 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu
|
|||
is_default: defaultOutputInput.value,
|
||||
is_default_monitoring: defaultMonitoringOutputInput.value,
|
||||
config_yaml: additionalYamlConfigInput.value,
|
||||
ssl: {
|
||||
certificate: sslCertificateInput.value,
|
||||
key: sslKeyInput.value || undefined,
|
||||
certificate_authorities: sslCertificateAuthoritiesInput.value.filter(
|
||||
(val) => val !== ''
|
||||
),
|
||||
},
|
||||
ssl: logstashEnableSSLInput.value
|
||||
? {
|
||||
certificate: sslCertificateInput.value,
|
||||
key: sslKeyInput.value || undefined,
|
||||
certificate_authorities: sslCertificateAuthoritiesInput.value.filter(
|
||||
(val) => val !== ''
|
||||
),
|
||||
}
|
||||
: null,
|
||||
...(!sslKeyInput.value &&
|
||||
sslKeySecretInput.value && {
|
||||
secrets: {
|
||||
ssl: {
|
||||
key: sslKeySecretInput.value,
|
||||
},
|
||||
ssl: logstashEnableSSLInput.value
|
||||
? {
|
||||
key: sslKeySecretInput.value,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
}),
|
||||
proxy_id: proxyIdValue,
|
||||
|
@ -1116,6 +1129,7 @@ export function useOutputForm(onSucess: () => void, output?: Output, defaultOupu
|
|||
kafkaBrokerTimeoutInput.value,
|
||||
kafkaBrokerReachabilityTimeoutInput.value,
|
||||
kafkaBrokerAckReliabilityInput.value,
|
||||
logstashEnableSSLInput.value,
|
||||
logstashHostsInput.value,
|
||||
sslCertificateInput.value,
|
||||
sslKeyInput.value,
|
||||
|
|
|
@ -9,15 +9,17 @@ export const LOGSTASH_CONFIG_PIPELINES = `- pipeline.id: elastic-agent-pipeline
|
|||
path.config: "/etc/path/to/elastic-agent-pipeline.conf"
|
||||
`;
|
||||
|
||||
export function getLogstashPipeline(apiKey?: string) {
|
||||
return `input {
|
||||
elastic_agent {
|
||||
port => 5044
|
||||
const inputSSLConfig = `
|
||||
ssl_enabled => true
|
||||
ssl_certificate_authorities => ["<ca_path>"]
|
||||
ssl_certificate => "<server_cert_path>"
|
||||
ssl_key => "<server_cert_key_in_pkcs8>"
|
||||
ssl_client_authentication => "required"
|
||||
ssl_client_authentication => "required"`;
|
||||
|
||||
export function getLogstashPipeline(isSSLEnabled: boolean, apiKey?: string) {
|
||||
return `input {
|
||||
elastic_agent {
|
||||
port => 5044 ${isSSLEnabled ? inputSSLConfig : ''}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,11 @@ import { MissingPrivilegesToolTip } from '../../../../../../components/missing_p
|
|||
import { getLogstashPipeline, LOGSTASH_CONFIG_PIPELINES } from './helpers';
|
||||
import { useLogstashApiKey } from './hooks';
|
||||
|
||||
export const LogstashInstructions = () => {
|
||||
interface LogstashInstructionsProps {
|
||||
isSSLEnabled: boolean;
|
||||
}
|
||||
|
||||
export const LogstashInstructions = ({ isSSLEnabled }: LogstashInstructionsProps) => {
|
||||
const { docLinks } = useStartServices();
|
||||
|
||||
return (
|
||||
|
@ -57,7 +61,7 @@ export const LogstashInstructions = () => {
|
|||
}}
|
||||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<LogstashInstructionSteps />
|
||||
<LogstashInstructionSteps isSSLEnabled={isSSLEnabled} />
|
||||
</>
|
||||
</CollapsibleCallout>
|
||||
);
|
||||
|
@ -100,7 +104,10 @@ const CollapsibleCallout: React.FunctionComponent<EuiCallOutProps> = ({ children
|
|||
);
|
||||
};
|
||||
|
||||
const LogstashInstructionSteps = () => {
|
||||
interface LogstashInstructionStepsProps {
|
||||
isSSLEnabled: boolean;
|
||||
}
|
||||
const LogstashInstructionSteps = ({ isSSLEnabled }: LogstashInstructionStepsProps) => {
|
||||
const { docLinks } = useStartServices();
|
||||
const logstashApiKey = useLogstashApiKey();
|
||||
const authz = useAuthz();
|
||||
|
@ -170,7 +177,7 @@ const LogstashInstructionSteps = () => {
|
|||
/>
|
||||
<EuiSpacer size="m" />
|
||||
<EuiCodeBlock paddingSize="m" language="yaml" isCopyable>
|
||||
{getLogstashPipeline(logstashApiKey.apiKey)}
|
||||
{getLogstashPipeline(isSSLEnabled, logstashApiKey.apiKey)}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
|
@ -225,7 +232,7 @@ const LogstashInstructionSteps = () => {
|
|||
),
|
||||
},
|
||||
],
|
||||
[logstashApiKey, docLinks, hasAllSettings]
|
||||
[logstashApiKey, docLinks, hasAllSettings, isSSLEnabled]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -136,6 +136,24 @@ function getMockedSoClient(
|
|||
});
|
||||
}
|
||||
|
||||
case outputIdToUuid('existing-logstash-output-with-ssl'): {
|
||||
return mockOutputSO('existing-logstash-output-with-ssl', {
|
||||
type: 'logstash',
|
||||
is_default: false,
|
||||
ssl: {
|
||||
certificate: 'cert-value',
|
||||
certificate_authorities: ['/path/to/CAs'],
|
||||
},
|
||||
secrets: {
|
||||
ssl: {
|
||||
key: {
|
||||
id: 'wnES3pUBqsj3cVixODPG',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
case outputIdToUuid('existing-kafka-output'): {
|
||||
return mockOutputSO('existing-kafka-output', {
|
||||
type: 'kafka',
|
||||
|
@ -2317,6 +2335,30 @@ describe('Output Service', () => {
|
|||
'Remote_elasticsearch output cannot be used with agentless integration in agentless policy. Please create a new Elasticsearch output.'
|
||||
);
|
||||
});
|
||||
|
||||
it('Should delete SSL fields if SSL field is null', async () => {
|
||||
const soClient = getMockedSoClient({});
|
||||
mockedAgentPolicyService.list.mockResolvedValue({
|
||||
items: [{}],
|
||||
} as unknown as ReturnType<typeof mockedAgentPolicyService.list>);
|
||||
mockedAgentPolicyService.hasAPMIntegration.mockReturnValue(false);
|
||||
mockedAgentPolicyService.hasFleetServerIntegration.mockReturnValue(false);
|
||||
mockedAgentPolicyService.list.mockResolvedValue({
|
||||
items: [],
|
||||
} as any);
|
||||
|
||||
await outputService.update(soClient, esClientMock, 'existing-logstash-output-with-ssl', {
|
||||
type: 'logstash',
|
||||
hosts: ['0.0.0.0'],
|
||||
ssl: null,
|
||||
});
|
||||
|
||||
expect(soClient.update).toBeCalledWith(expect.anything(), expect.anything(), {
|
||||
type: 'logstash',
|
||||
hosts: ['0.0.0.0'],
|
||||
ssl: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
|
|
@ -1689,6 +1689,39 @@ describe('secrets', () => {
|
|||
|
||||
expect(result.secretsToDelete).toEqual([{ id: 'token' }]);
|
||||
});
|
||||
|
||||
it('should delete secret if secret is undefined in update', async () => {
|
||||
const result = await extractAndUpdateOutputSecrets({
|
||||
oldOutput: {
|
||||
id: 'logstash-id',
|
||||
name: 'logstash',
|
||||
type: 'logstash',
|
||||
is_default: false,
|
||||
is_default_monitoring: false,
|
||||
secrets: {
|
||||
ssl: {
|
||||
key: {
|
||||
id: 'ssl-key-token',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
outputUpdate: {
|
||||
id: 'logstash-id',
|
||||
name: 'logstash',
|
||||
type: 'logstash',
|
||||
secrets: {
|
||||
ssl: undefined,
|
||||
},
|
||||
is_default: false,
|
||||
is_default_monitoring: false,
|
||||
proxy_id: null,
|
||||
},
|
||||
esClient: esClientMock,
|
||||
});
|
||||
|
||||
expect(result.secretsToDelete).toEqual([{ id: 'ssl-key-token' }]);
|
||||
});
|
||||
});
|
||||
describe('deleteOutputSecrets', () => {
|
||||
it('should delete existing secrets', async () => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue