mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
6b61af3fde
commit
2758dbbeca
24 changed files with 404 additions and 159 deletions
|
@ -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`,
|
||||
|
|
|
@ -537,6 +537,7 @@ export interface DocLinks {
|
|||
datastreamsDownsampling: string;
|
||||
installElasticAgent: string;
|
||||
installElasticAgentStandalone: string;
|
||||
grantESAccessToStandaloneAgents: string;
|
||||
packageSignatures: string;
|
||||
upgradeElasticAgent: string;
|
||||
learnMoreBlog: string;
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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}'`);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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),
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
|
|
|
@ -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 won’t 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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -80,6 +80,8 @@ export type {
|
|||
GetOneEnrollmentAPIKeyResponse,
|
||||
PostEnrollmentAPIKeyRequest,
|
||||
PostEnrollmentAPIKeyResponse,
|
||||
PostStandaloneAgentAPIKeyRequest,
|
||||
PostStandaloneAgentAPIKeyResponse,
|
||||
PostLogstashApiKeyResponse,
|
||||
GetOutputsResponse,
|
||||
GetCurrentUpgradesResponse,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
};
|
|
@ -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
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue