[Fleet] Use API key for standalone agent onboarding (#187133)

## Summary

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

This PR replaces the username and password with API key config in
standalone agent onboarding.

Note: the Observability Logs onboarding (at
`app/observabilityOnboarding/systemLogs/?category=logs`) shows the
encoded API key, I thought it would make more sense to show it in the
Beats format (see
https://www.elastic.co/guide/en/fleet/current/grant-access-to-elasticsearch.html).

### Testing

Below are the steps for testing standalone agent install from the main
Fleet UI (Agents table). I am not entirely sure how to test [the
component used in
CreatePackagePolicyPage](https://github.com/elastic/kibana/blob/main/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/multi_page_layout/components/page_steps/install_agent/install_agent_standalone.tsx),
so I'd appreciate a comment on that. 🙏

⚠️ Ideally, this should also be tested in serverless config.

1. Open the Add agent flyout and select Standalone. Notice that the
agent yaml config contains an `api_key` entry instead of `username` and
`password`.
4. Click the new "Create API key" button. This should create a new API
key and that can be copied (you can find this key under Stack Management
-> Api keys, it should be called `standalone_agent-{randomString}`).
5. Check that the `{API_KEY}` placeholder in the agent yaml was updated
with the API key value.
6. Download an Elastic Agent, e.g. on a VM.
7. Modify the `elastic-agent.yml` file of the agent to the yaml from the
UI. If using a Multipass VM, you can for instance download it from the
UI and copy it using `multipass transfer <localFileLocation>
<VMName>:./<elasticAgentDirectory>`.
8. Install the agent as standalone: `sudo ./elastic-agent install`
(answer `n` when asked about enrolling in Fleet).
9. Check the agent status and logs with `sudo elastic-agent status` and
`sudo elastic-agent logs`.
10. In the UI, go to Discover and search for the agent host name in the
logs, it should appear.

### Screenshots

Current behaviour (on `main`):
<img width="799" alt="Screenshot 2024-06-28 at 10 38 44"
src="059bf45c-66db-447b-bf01-4226c64ce2a6">

With this change:
<img width="799" alt="Screenshot 2024-06-28 at 10 31 35"
src="5853fbf3-221e-48fd-9d13-aececa78f389">

After having clicked "Create API key":
<img width="799" alt="Screenshot 2024-06-28 at 10 31 58"
src="df066040-cdbe-4fd5-a508-7093af86e85c">

### Checklist

- [ ] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [ ] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [ ] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [ ] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [ ] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: criamico <mariacristina.amico@elastic.co>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Jill Guyonnet 2024-07-02 10:01:26 +01:00 committed by GitHub
parent 6b61af3fde
commit 2758dbbeca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 404 additions and 159 deletions

View file

@ -848,6 +848,7 @@ export const getDocLinks = ({ kibanaBranch, buildFlavor }: GetDocLinkOptions): D
datastreamsDownsampling: `${ELASTICSEARCH_DOCS}downsampling.html`,
installElasticAgent: `${FLEET_DOCS}install-fleet-managed-elastic-agent.html`,
installElasticAgentStandalone: `${FLEET_DOCS}install-standalone-elastic-agent.html`,
grantESAccessToStandaloneAgents: `${FLEET_DOCS}grant-access-to-elasticsearch.html`,
upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`,
learnMoreBlog: `${ELASTIC_WEBSITE_URL}blog/elastic-agent-and-fleet-make-it-easier-to-integrate-your-systems-with-elastic`,
apiKeysLearnMore: isServerless ? `${SERVERLESS_DOCS}api-keys` : `${KIBANA_DOCS}api-keys.html`,

View file

@ -537,6 +537,7 @@ export interface DocLinks {
datastreamsDownsampling: string;
installElasticAgent: string;
installElasticAgentStandalone: string;
grantESAccessToStandaloneAgents: string;
packageSignatures: string;
upgradeElasticAgent: string;
learnMoreBlog: string;

View file

@ -212,8 +212,9 @@ export const DOWNLOAD_SOURCE_API_ROUTES = {
DELETE_PATTERN: `${API_ROOT}/agent_download_sources/{sourceId}`,
};
// Fleet debug routes
export const CREATE_STANDALONE_AGENT_API_KEY_ROUTE = `${INTERNAL_ROOT}/create_standalone_agent_api_key`;
// Fleet debug routes
export const FLEET_DEBUG_ROUTES = {
INDEX_PATTERN: `${INTERNAL_ROOT}/debug/index`,
SAVED_OBJECTS_PATTERN: `${INTERNAL_ROOT}/debug/saved_objects`,

View file

@ -28,15 +28,20 @@ const POLICY_KEYS_ORDER = [
'signed',
];
export const fullAgentPolicyToYaml = (policy: FullAgentPolicy, toYaml: typeof safeDump): string => {
export const fullAgentPolicyToYaml = (
policy: FullAgentPolicy,
toYaml: typeof safeDump,
apiKey?: string
): string => {
const yaml = toYaml(policy, {
skipInvalid: true,
sortKeys: _sortYamlKeys,
});
const formattedYml = apiKey ? replaceApiKey(yaml, apiKey) : yaml;
if (!policy?.secret_references?.length) return yaml;
if (!policy?.secret_references?.length) return formattedYml;
return _formatSecrets(policy.secret_references, yaml);
return _formatSecrets(policy.secret_references, formattedYml);
};
export function _sortYamlKeys(keyA: string, keyB: string) {
@ -67,3 +72,8 @@ function _formatSecrets(
return formattedText;
}
function replaceApiKey(ymlText: string, apiKey: string) {
const regex = new RegExp(/\'\${API_KEY}\'/, 'g');
return ymlText.replace(regex, `'${apiKey}'`);
}

View file

@ -20,3 +20,4 @@ export * from './package_policy';
export * from './settings';
export * from './health_check';
export * from './fleet_server_hosts';
export * from './standalone_agent_api_key';

View file

@ -0,0 +1,19 @@
/*
* 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 type { SecurityCreateApiKeyResponse } from '@elastic/elasticsearch/lib/api/types';
export interface PostStandaloneAgentAPIKeyRequest {
body: {
name: string;
};
}
export interface PostStandaloneAgentAPIKeyResponse {
action: string;
item: SecurityCreateApiKeyResponse;
}

View file

@ -5,79 +5,37 @@
* 2.0.
*/
import React, { useState, useEffect } from 'react';
import React, { useState } from 'react';
import { FormattedMessage } from '@kbn/i18n-react';
import { EuiSteps, EuiSpacer } from '@elastic/eui';
import { safeDump } from 'js-yaml';
import type { FullAgentPolicy } from '../../../../../../../../../../common/types/models/agent_policy';
import { API_VERSIONS } from '../../../../../../../../../../common/constants';
import { getRootIntegrations } from '../../../../../../../../../../common/services';
import {
AgentStandaloneBottomBar,
StandaloneModeWarningCallout,
NotObscuredByBottomBar,
} from '../..';
import {
fullAgentPolicyToYaml,
agentPolicyRouteService,
} from '../../../../../../../../../services';
import { Error as FleetError } from '../../../../../../../components';
import {
useKibanaVersion,
useStartServices,
sendGetOneAgentPolicyFull,
} from '../../../../../../../../../hooks';
import { useKibanaVersion } from '../../../../../../../../../hooks';
import {
InstallStandaloneAgentStep,
ConfigureStandaloneAgentStep,
} from '../../../../../../../../../components/agent_enrollment_flyout/steps';
import { StandaloneInstructions } from '../../../../../../../../../components/enrollment_instructions';
import { useFetchFullPolicy } from '../../../../../../../../../components/agent_enrollment_flyout/hooks';
import type { InstallAgentPageProps } from './types';
export const InstallElasticAgentStandalonePageStep: React.FC<InstallAgentPageProps> = (props) => {
const { setIsManaged, agentPolicy, cancelUrl, onNext, cancelClickHandler } = props;
const core = useStartServices();
const kibanaVersion = useKibanaVersion();
const [yaml, setYaml] = useState<any | undefined>('');
const [commandCopied, setCommandCopied] = useState(false);
const [policyCopied, setPolicyCopied] = useState(false);
const [fullAgentPolicy, setFullAgentPolicy] = useState<FullAgentPolicy | undefined>();
useEffect(() => {
async function fetchFullPolicy() {
try {
if (!agentPolicy?.id) {
return;
}
const query = { standalone: true, kubernetes: false };
const res = await sendGetOneAgentPolicyFull(agentPolicy?.id, query);
if (res.error) {
throw res.error;
}
if (!res.data) {
throw new Error('No data while fetching full agent policy');
}
setFullAgentPolicy(res.data.item);
} catch (error) {
core.notifications.toasts.addError(error, {
title: 'Error',
});
}
}
fetchFullPolicy();
}, [core.http.basePath, agentPolicy?.id, core.notifications.toasts]);
useEffect(() => {
if (!fullAgentPolicy) {
return;
}
setYaml(fullAgentPolicyToYaml(fullAgentPolicy, safeDump));
}, [fullAgentPolicy]);
const { yaml, onCreateApiKey, apiKey, downloadYaml } = useFetchFullPolicy(agentPolicy);
if (!agentPolicy) {
return (
@ -95,16 +53,13 @@ export const InstallElasticAgentStandalonePageStep: React.FC<InstallAgentPagePro
const installManagedCommands = StandaloneInstructions(kibanaVersion);
const downloadLink = core.http.basePath.prepend(
`${agentPolicyRouteService.getInfoFullDownloadPath(
agentPolicy?.id
)}?standalone=true&apiVersion=${API_VERSIONS.public.v1}`
);
const steps = [
ConfigureStandaloneAgentStep({
selectedPolicyId: agentPolicy?.id,
yaml,
downloadLink,
downloadYaml,
apiKey,
onCreateApiKey,
isComplete: policyCopied,
onCopy: () => setPolicyCopied(true),
}),

View file

@ -4,11 +4,20 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { useState, useEffect, useMemo } from 'react';
import crypto from 'crypto';
import { useState, useEffect, useMemo, useCallback } from 'react';
import { i18n } from '@kbn/i18n';
import { safeDump } from 'js-yaml';
import type { PackagePolicy, AgentPolicy } from '../../types';
import { sendGetOneAgentPolicy, useGetPackageInfoByKeyQuery, useStartServices } from '../../hooks';
import {
sendGetOneAgentPolicy,
sendGetOneAgentPolicyFull,
useGetPackageInfoByKeyQuery,
useStartServices,
} from '../../hooks';
import {
FLEET_KUBERNETES_PACKAGE,
FLEET_CLOUD_SECURITY_POSTURE_PACKAGE,
@ -23,6 +32,12 @@ import {
SUPPORTED_TEMPLATES_URL_FROM_AGENT_POLICY_CONFIG,
} from '../cloud_security_posture/services';
import { sendCreateStandaloneAgentAPIKey } from '../../hooks';
import type { FullAgentPolicy } from '../../../common';
import { fullAgentPolicyToYaml } from '../../services';
import type {
K8sMode,
CloudSecurityIntegrationType,
@ -190,3 +205,104 @@ const getCloudSecurityPackagePolicyFromAgentPolicy = (
(input) => input.package?.name === FLEET_CLOUD_SECURITY_POSTURE_PACKAGE
);
};
export function useGetCreateApiKey() {
const core = useStartServices();
const [apiKey, setApiKey] = useState<string | undefined>(undefined);
const onCreateApiKey = useCallback(async () => {
try {
const res = await sendCreateStandaloneAgentAPIKey({
name: crypto.randomBytes(16).toString('hex'),
});
const newApiKey = `${res.data?.item.id}:${res.data?.item.api_key}`;
setApiKey(newApiKey);
} catch (err) {
core.notifications.toasts.addError(err, {
title: i18n.translate('xpack.fleet.standaloneAgentPage.errorCreatingAgentAPIKey', {
defaultMessage: 'Error creating Agent API Key',
}),
});
}
}, [core.notifications.toasts]);
return {
apiKey,
onCreateApiKey,
};
}
export function useFetchFullPolicy(agentPolicy: AgentPolicy | undefined, isK8s?: K8sMode) {
const core = useStartServices();
const [yaml, setYaml] = useState<any | undefined>('');
const [fullAgentPolicy, setFullAgentPolicy] = useState<FullAgentPolicy | undefined>();
const { apiKey, onCreateApiKey } = useGetCreateApiKey();
useEffect(() => {
async function fetchFullPolicy() {
try {
if (!agentPolicy?.id) {
return;
}
let query = { standalone: true, kubernetes: false };
if (isK8s === 'IS_KUBERNETES') {
query = { standalone: true, kubernetes: true };
}
const res = await sendGetOneAgentPolicyFull(agentPolicy?.id, query);
if (res.error) {
throw res.error;
}
if (!res.data) {
throw new Error('No data while fetching full agent policy');
}
setFullAgentPolicy(res.data.item);
} catch (error) {
core.notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.standaloneAgentPage.errorFetchingFullAgentPolicy', {
defaultMessage: 'Error fetching full agent policy',
}),
});
}
}
if (isK8s === 'IS_NOT_KUBERNETES' || isK8s !== 'IS_LOADING') {
fetchFullPolicy();
}
}, [core.http.basePath, agentPolicy?.id, core.notifications.toasts, apiKey, isK8s, agentPolicy]);
useEffect(() => {
if (!fullAgentPolicy) {
return;
}
if (isK8s === 'IS_KUBERNETES') {
if (typeof fullAgentPolicy === 'object') {
return;
}
setYaml(fullAgentPolicy);
} else {
if (typeof fullAgentPolicy === 'string') {
return;
}
setYaml(fullAgentPolicyToYaml(fullAgentPolicy, safeDump, apiKey));
}
}, [apiKey, fullAgentPolicy, isK8s]);
const downloadYaml = useMemo(
() => () => {
const link = document.createElement('a');
link.href = `data:text/json;charset=utf-8,${yaml}`;
link.download = `elastic-agent.yaml`;
link.click();
},
[yaml]
);
return {
yaml,
onCreateApiKey,
fullAgentPolicy,
apiKey,
downloadYaml,
};
}

View file

@ -5,28 +5,20 @@
* 2.0.
*/
import React, { useState, useMemo, useEffect } from 'react';
import React, { useState, useMemo } from 'react';
import { EuiSteps, EuiLoadingSpinner } from '@elastic/eui';
import { safeDump } from 'js-yaml';
import type { EuiContainedStepProps } from '@elastic/eui/src/components/steps/steps';
import type { FullAgentPolicy } from '../../../../common/types/models/agent_policy';
import { API_VERSIONS } from '../../../../common/constants';
import { getRootIntegrations } from '../../../../common/services';
import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../../services';
import { getGcpIntegrationDetailsFromAgentPolicy } from '../../cloud_security_posture/services';
import { StandaloneInstructions, ManualInstructions } from '../../enrollment_instructions';
import {
useGetOneEnrollmentAPIKey,
useStartServices,
sendGetOneAgentPolicyFull,
useAgentVersion,
} from '../../../hooks';
import { useGetOneEnrollmentAPIKey, useStartServices, useAgentVersion } from '../../../hooks';
import { useFetchFullPolicy } from '../hooks';
import type { InstructionProps } from '../types';
import { usePollingAgentCount } from '../confirm_agent_enrollment';
@ -62,74 +54,7 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
isK8s,
cloudSecurityIntegration,
}) => {
const core = useStartServices();
const { notifications } = core;
const [fullAgentPolicy, setFullAgentPolicy] = useState<FullAgentPolicy | undefined>();
const [yaml, setYaml] = useState<any | undefined>('');
let downloadLink = '';
if (selectedPolicy?.id) {
downloadLink =
isK8s === 'IS_KUBERNETES'
? core.http.basePath.prepend(
`${agentPolicyRouteService.getInfoFullDownloadPath(
selectedPolicy?.id
)}?kubernetes=true&standalone=true&apiVersion=${API_VERSIONS.public.v1}`
)
: core.http.basePath.prepend(
`${agentPolicyRouteService.getInfoFullDownloadPath(
selectedPolicy?.id
)}?standalone=true&apiVersion=${API_VERSIONS.public.v1}`
);
}
useEffect(() => {
async function fetchFullPolicy() {
try {
if (!selectedPolicy?.id) {
return;
}
let query = { standalone: true, kubernetes: false };
if (isK8s === 'IS_KUBERNETES') {
query = { standalone: true, kubernetes: true };
}
const res = await sendGetOneAgentPolicyFull(selectedPolicy?.id, query);
if (res.error) {
throw res.error;
}
if (!res.data) {
throw new Error('No data while fetching full agent policy');
}
setFullAgentPolicy(res.data.item);
} catch (error) {
notifications.toasts.addError(error, {
title: 'Error',
});
}
}
if (isK8s !== 'IS_LOADING') {
fetchFullPolicy();
}
}, [selectedPolicy, notifications.toasts, isK8s, core.http.basePath]);
useEffect(() => {
if (!fullAgentPolicy) {
return;
}
if (isK8s === 'IS_KUBERNETES') {
if (typeof fullAgentPolicy === 'object') {
return;
}
setYaml(fullAgentPolicy);
} else {
if (typeof fullAgentPolicy === 'string') {
return;
}
setYaml(fullAgentPolicyToYaml(fullAgentPolicy, safeDump));
}
}, [fullAgentPolicy, isK8s]);
const { yaml, onCreateApiKey, apiKey, downloadYaml } = useFetchFullPolicy(selectedPolicy, isK8s);
const agentVersion = useAgentVersion();
@ -160,7 +85,9 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
isK8s,
selectedPolicyId: selectedPolicy?.id,
yaml,
downloadLink,
downloadYaml,
apiKey,
onCreateApiKey,
})
);
@ -176,8 +103,6 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
return steps;
}, [
agentVersion,
isK8s,
cloudSecurityIntegration,
agentPolicy,
selectedPolicy,
agentPolicies,
@ -186,8 +111,12 @@ export const StandaloneSteps: React.FunctionComponent<InstructionProps> = ({
setSelectedPolicyId,
refreshAgentPolicies,
selectionType,
isK8s,
yaml,
downloadLink,
downloadYaml,
apiKey,
onCreateApiKey,
cloudSecurityIntegration,
mode,
setMode,
]);

View file

@ -16,6 +16,9 @@ import {
EuiCopy,
EuiCodeBlock,
EuiLink,
EuiCallOut,
EuiFieldText,
EuiButtonIcon,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
@ -27,21 +30,25 @@ import { useStartServices } from '../../../hooks';
export const ConfigureStandaloneAgentStep = ({
isK8s,
selectedPolicyId,
yaml,
downloadLink,
downloadYaml,
apiKey,
onCreateApiKey,
isComplete,
onCopy,
}: {
isK8s?: K8sMode;
selectedPolicyId?: string;
yaml: string;
downloadLink: string;
downloadYaml: () => void;
apiKey: string | undefined;
onCreateApiKey: () => void;
isComplete?: boolean;
onCopy?: () => void;
}): EuiContainedStepProps => {
const core = useStartServices();
const { docLinks } = core;
const policyMsg =
isK8s === 'IS_KUBERNETES' ? (
<FormattedMessage
@ -67,12 +74,23 @@ export const ConfigureStandaloneAgentStep = ({
) : (
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepConfigureAgentDescription"
defaultMessage="Copy this policy to the {fileName} on the host where the Elastic Agent is installed. Modify {ESUsernameVariable} and {ESPasswordVariable} in the {outputSection} section of {fileName} to use your Elasticsearch credentials."
defaultMessage="Copy this policy to the {fileName} on the host where the Elastic Agent is installed. Either use an existing API key and modify {apiKeyVariable} in the {outputSection} section of {fileName} or click the button below to generate a new one. Refer to {guideLink} for details."
values={{
fileName: <EuiCode>elastic-agent.yml</EuiCode>,
ESUsernameVariable: <EuiCode>ES_USERNAME</EuiCode>,
ESPasswordVariable: <EuiCode>ES_PASSWORD</EuiCode>,
apiKeyVariable: <EuiCode>API_KEY</EuiCode>,
outputSection: <EuiCode>outputs</EuiCode>,
guideLink: (
<EuiLink
external
href={docLinks.links.fleet.grantESAccessToStandaloneAgents}
target="_blank"
>
<FormattedMessage
id="xpack.fleet.fleet.agentEnrollment.standaloneAgentAccessLinkText"
defaultMessage="Grant standalone Elastic Agents access to Elasticsearch"
/>
</EuiLink>
),
}}
/>
);
@ -89,6 +107,7 @@ export const ConfigureStandaloneAgentStep = ({
defaultMessage="Download Policy"
/>
);
return {
title: i18n.translate('xpack.fleet.agentEnrollment.stepConfigureAgentTitle', {
defaultMessage: 'Configure the agent',
@ -99,7 +118,63 @@ export const ConfigureStandaloneAgentStep = ({
<EuiText>
<>{policyMsg}</>
<EuiSpacer size="m" />
{apiKey && (
<EuiCallOut
title={i18n.translate('xpack.fleet.agentEnrollment.apiKeyBanner.created', {
defaultMessage: 'API Key created.',
})}
color="success"
iconType="check"
data-test-subj="obltOnboardingLogsApiKeyCreated"
>
<p>
{i18n.translate('xpack.fleet.agentEnrollment.apiKeyBanner.created.description', {
defaultMessage:
'Remember to store this information in a safe place. It wont be displayed anymore after you continue.',
})}
</p>
<EuiFieldText
data-test-subj="apmAgentKeyCallOutFieldText"
readOnly
value={apiKey}
aria-label={i18n.translate(
'xpack.fleet.agentEnrollment.apiKeyBanner.field.label',
{
defaultMessage: 'Api Key',
}
)}
append={
<EuiCopy textToCopy={apiKey}>
{(copy) => (
<EuiButtonIcon
iconType="copyClipboard"
onClick={copy}
color="success"
css={{
'> svg.euiIcon': {
borderRadius: '0 !important',
},
}}
aria-label={i18n.translate('xpack.fleet.apiKeyBanner.field.copyButton', {
defaultMessage: 'Copy to clipboard',
})}
/>
)}
</EuiCopy>
}
/>
</EuiCallOut>
)}
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
<EuiButton onClick={onCreateApiKey}>
<FormattedMessage
id="xpack.fleet.agentEnrollment.createApiKeyButton"
defaultMessage="Create API key"
/>
</EuiButton>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiCopy textToCopy={yaml}>
{(copy) => (
@ -119,14 +194,13 @@ export const ConfigureStandaloneAgentStep = ({
</EuiCopy>
</EuiFlexItem>
<EuiFlexItem grow={false}>
{/* eslint-disable-next-line @elastic/eui/href-or-on-click */}
<EuiButton
iconType="download"
href={downloadLink}
onClick={() => {
if (onCopy) onCopy();
downloadYaml();
}}
isDisabled={!downloadLink}
isDisabled={!downloadYaml}
>
<>{downloadMsg}</>
</EuiButton>

View file

@ -12,6 +12,7 @@ export * from './data_stream';
export * from './agents';
export * from './enrollment_api_keys';
export * from './epm';
export * from './standalone_agent_api_key';
export * from './outputs';
export * from './settings';
export * from './setup';

View file

@ -0,0 +1,24 @@
/*
* 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 type {
PostStandaloneAgentAPIKeyRequest,
PostStandaloneAgentAPIKeyResponse,
} from '../../types';
import { API_VERSIONS, CREATE_STANDALONE_AGENT_API_KEY_ROUTE } from '../../../common/constants';
import { sendRequest } from './use_request';
export function sendCreateStandaloneAgentAPIKey(body: PostStandaloneAgentAPIKeyRequest['body']) {
return sendRequest<PostStandaloneAgentAPIKeyResponse>({
method: 'post',
path: CREATE_STANDALONE_AGENT_API_KEY_ROUTE,
version: API_VERSIONS.internal.v1,
body,
});
}

View file

@ -80,6 +80,8 @@ export type {
GetOneEnrollmentAPIKeyResponse,
PostEnrollmentAPIKeyRequest,
PostEnrollmentAPIKeyResponse,
PostStandaloneAgentAPIKeyRequest,
PostStandaloneAgentAPIKeyResponse,
PostLogstashApiKeyResponse,
GetOutputsResponse,
GetCurrentUpgradesResponse,

View file

@ -38,6 +38,7 @@ export {
PRECONFIGURATION_API_ROUTES,
DOWNLOAD_SOURCE_API_ROOT,
DOWNLOAD_SOURCE_API_ROUTES,
CREATE_STANDALONE_AGENT_API_KEY_ROUTE,
FLEET_DEBUG_ROUTES,
// Saved Object indices
INGEST_SAVED_OBJECT_INDEX,

View file

@ -377,7 +377,9 @@ export const getFullAgentPolicy: FleetRequestHandler<
const fullAgentPolicy = await agentPolicyService.getFullAgentPolicy(
soClient,
request.params.agentPolicyId,
{ standalone: request.query.standalone === true }
{
standalone: request.query.standalone === true,
}
);
if (fullAgentPolicy) {
const body: GetFullAgentPolicyResponse = {

View file

@ -26,6 +26,7 @@ import { registerRoutes as registerFleetServerHostRoutes } from './fleet_server_
import { registerRoutes as registerFleetProxiesRoutes } from './fleet_proxies';
import { registerRoutes as registerMessageSigningServiceRoutes } from './message_signing_service';
import { registerRoutes as registerUninstallTokenRoutes } from './uninstall_token';
import { registerRoutes as registerStandaloneAgentApiKeyRoutes } from './standalone_agent_api_key';
import { registerRoutes as registerDebugRoutes } from './debug';
export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: FleetConfigType) {
@ -48,6 +49,7 @@ export function registerRoutes(fleetAuthzRouter: FleetAuthzRouter, config: Fleet
registerHealthCheckRoutes(fleetAuthzRouter);
registerMessageSigningServiceRoutes(fleetAuthzRouter);
registerUninstallTokenRoutes(fleetAuthzRouter, config);
registerStandaloneAgentApiKeyRoutes(fleetAuthzRouter);
registerDebugRoutes(fleetAuthzRouter);
// Conditional config routes

View file

@ -0,0 +1,26 @@
/*
* 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 type { TypeOf } from '@kbn/config-schema';
import { createStandaloneAgentApiKey } from '../../services/api_keys';
import type { FleetRequestHandler, PostStandaloneAgentAPIKeyRequestSchema } from '../../types';
export const createStandaloneAgentApiKeyHandler: FleetRequestHandler<
undefined,
undefined,
TypeOf<typeof PostStandaloneAgentAPIKeyRequestSchema.body>
> = async (context, request, response) => {
const coreContext = await context.core;
const esClient = coreContext.elasticsearch.client.asCurrentUser;
const key = await createStandaloneAgentApiKey(esClient, request.body.name);
return response.ok({
body: {
item: key,
},
});
};

View file

@ -0,0 +1,34 @@
/*
* 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 type { FleetAuthzRouter } from '../../services/security';
import { API_VERSIONS } from '../../../common/constants';
import { CREATE_STANDALONE_AGENT_API_KEY_ROUTE } from '../../constants';
import { PostStandaloneAgentAPIKeyRequestSchema } from '../../types';
import { createStandaloneAgentApiKeyHandler } from './handler';
export const registerRoutes = (router: FleetAuthzRouter) => {
router.versioned
.post({
path: CREATE_STANDALONE_AGENT_API_KEY_ROUTE,
access: 'internal',
fleetAuthz: {
fleet: { all: true },
},
})
.addVersion(
{
version: API_VERSIONS.internal.v1,
validate: { request: PostStandaloneAgentAPIKeyRequestSchema },
},
createStandaloneAgentApiKeyHandler
);
};

View file

@ -817,7 +817,7 @@ ssl.test: 123
`);
});
it('should return placeholder ES_USERNAME and ES_PASSWORD for elasticsearch output type in standalone ', () => {
it('should return placeholder API_KEY for elasticsearch output type in standalone ', () => {
const policyOutput = transformOutputToFullPolicyOutput(
{
id: 'id123',
@ -833,18 +833,17 @@ ssl.test: 123
expect(policyOutput).toMatchInlineSnapshot(`
Object {
"api_key": "\${API_KEY}",
"hosts": Array [
"http://host.fr",
],
"password": "\${ES_PASSWORD}",
"preset": "balanced",
"type": "elasticsearch",
"username": "\${ES_USERNAME}",
}
`);
});
it('should not return placeholder ES_USERNAME and ES_PASSWORD for logstash output type in standalone ', () => {
it('should not return placeholder API_KEY for logstash output type in standalone ', () => {
const policyOutput = transformOutputToFullPolicyOutput(
{
id: 'id123',

View file

@ -491,8 +491,8 @@ export function transformOutputToFullPolicyOutput(
}
if (output.type === outputType.Elasticsearch && standalone) {
newOutput.username = '${ES_USERNAME}';
newOutput.password = '${ES_PASSWORD}';
// adding a place_holder as API_KEY
newOutput.api_key = '${API_KEY}';
}
if (output.type === outputType.RemoteElasticsearch) {

View file

@ -0,0 +1,31 @@
/*
* 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 type { ElasticsearchClient } from '@kbn/core/server';
export function createStandaloneAgentApiKey(esClient: ElasticsearchClient, name: string) {
// Based on https://www.elastic.co/guide/en/fleet/master/grant-access-to-elasticsearch.html#create-api-key-standalone-agent
return esClient.security.createApiKey({
body: {
name: `standalone_agent-${name}`,
metadata: {
managed: true,
},
role_descriptors: {
standalone_agent: {
cluster: ['monitor'],
indices: [
{
names: ['logs-*-*', 'metrics-*-*', 'traces-*-*', 'synthetics-*-*'],
privileges: ['auto_configure', 'create_doc'],
},
],
},
},
},
});
}

View file

@ -8,3 +8,4 @@
export { invalidateAPIKeys } from './security';
export { generateLogstashApiKey, canCreateLogstashApiKey } from './logstash_api_keys';
export * from './enrollment_api_key';
export { createStandaloneAgentApiKey } from './create_standalone_agent_api_key';

View file

@ -23,3 +23,4 @@ export * from './tags';
export * from './health_check';
export * from './message_signing_service';
export * from './app';
export * from './standalone_agent_api_key';

View file

@ -0,0 +1,14 @@
/*
* 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 { schema } from '@kbn/config-schema';
export const PostStandaloneAgentAPIKeyRequestSchema = {
body: schema.object({
name: schema.string(),
}),
};