diff --git a/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap b/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap
index ff4f0a3b3346e..f4ecf2f4f6c8a 100644
--- a/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap
+++ b/web/packages/teleport/src/Discover/SelectResource/__snapshots__/SelectResource.story.test.tsx.snap
@@ -437,7 +437,7 @@ exports[`render with URL loc state set to "server" 1`] = `
- EC2 Instance
+ EC2 Auto Enrollment
@@ -1256,7 +1256,7 @@ exports[`render with all access 1`] = `
- EC2 Instance
+ EC2 Auto Enrollment
@@ -3622,7 +3622,7 @@ exports[`render with no access 1`] = `
- EC2 Instance
+ EC2 Auto Enrollment
@@ -5350,7 +5350,7 @@ exports[`render with partial access 1`] = `
- EC2 Instance
+ EC2 Auto Enrollment
diff --git a/web/packages/teleport/src/Discover/SelectResource/resources.tsx b/web/packages/teleport/src/Discover/SelectResource/resources.tsx
index cb6d1449da646..d5ebe43810ede 100644
--- a/web/packages/teleport/src/Discover/SelectResource/resources.tsx
+++ b/web/packages/teleport/src/Discover/SelectResource/resources.tsx
@@ -20,7 +20,10 @@ import { Platform } from 'design/platform';
import { assertUnreachable } from 'shared/utils/assertUnreachable';
-import { DiscoverEventResource } from 'teleport/services/userEvent';
+import {
+ DiscoverDiscoveryConfigMethod,
+ DiscoverEventResource,
+} from 'teleport/services/userEvent';
import { ResourceKind } from '../Shared/ResourceKind';
@@ -81,13 +84,17 @@ export const SERVERS: ResourceSpec[] = [
platform: Platform.macOS,
},
{
- name: 'EC2 Instance',
+ name: 'EC2 Auto Enrollment',
kind: ResourceKind.Server,
keywords:
- baseServerKeywords + 'ec2 instance connect endpoint aws amazon eice',
+ baseServerKeywords +
+ 'ec2 instance aws amazon simple systems manager ssm auto enrollment',
icon: 'Aws',
event: DiscoverEventResource.Ec2Instance,
- nodeMeta: { location: ServerLocation.Aws },
+ nodeMeta: {
+ location: ServerLocation.Aws,
+ discoveryConfigMethod: DiscoverDiscoveryConfigMethod.AwsEc2Ssm,
+ },
},
{
name: 'Connect My Computer',
diff --git a/web/packages/teleport/src/Discover/SelectResource/types.ts b/web/packages/teleport/src/Discover/SelectResource/types.ts
index b5d033d42f372..cce21de26d7f1 100644
--- a/web/packages/teleport/src/Discover/SelectResource/types.ts
+++ b/web/packages/teleport/src/Discover/SelectResource/types.ts
@@ -24,7 +24,10 @@ import { AuthType } from 'teleport/services/user';
import { ResourceKind } from '../Shared/ResourceKind';
-import type { DiscoverEventResource } from 'teleport/services/userEvent';
+import type {
+ DiscoverDiscoveryConfigMethod,
+ DiscoverEventResource,
+} from 'teleport/services/userEvent';
import type { ResourceIconName } from 'design/ResourceIcon';
@@ -77,8 +80,11 @@ export enum SamlServiceProviderPreset {
export interface ResourceSpec {
dbMeta?: { location: DatabaseLocation; engine: DatabaseEngine };
- nodeMeta?: { location: ServerLocation };
appMeta?: { awsConsole?: boolean };
+ nodeMeta?: {
+ location: ServerLocation;
+ discoveryConfigMethod: DiscoverDiscoveryConfigMethod;
+ };
kubeMeta?: { location: KubeLocation };
samlMeta?: { preset: SamlServiceProviderPreset };
name: string;
diff --git a/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx
new file mode 100644
index 0000000000000..1e22bfabee8d0
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.story.tsx
@@ -0,0 +1,115 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { MemoryRouter } from 'react-router';
+
+import { ContextProvider } from 'teleport';
+import cfg from 'teleport/config';
+import { createTeleportContext } from 'teleport/mocks/contexts';
+import {
+ DiscoverProvider,
+ DiscoverContextState,
+} from 'teleport/Discover/useDiscover';
+import {
+ IntegrationKind,
+ IntegrationStatusCode,
+} from 'teleport/services/integrations';
+import { ResourceKind } from 'teleport/Discover/Shared';
+import {
+ DiscoverDiscoveryConfigMethod,
+ DiscoverEventResource,
+} from 'teleport/services/userEvent';
+import { ServerLocation } from 'teleport/Discover/SelectResource';
+
+import { ConfigureDiscoveryService as Comp } from './ConfigureDiscoveryService';
+
+export default {
+ title: 'Teleport/Discover/Server/EC2',
+};
+
+export const ConfigureDiscoveryService = () => {
+ return ;
+};
+
+const Component = () => {
+ const ctx = createTeleportContext();
+ const discoverCtx: DiscoverContextState = {
+ agentMeta: {
+ resourceName: 'aws-console',
+ agentMatcherLabels: [],
+ awsRegion: 'ap-south-1',
+ awsIntegration: {
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-oidc-name',
+ resourceType: 'integration',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/test-role-arn',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
+ statusCode: IntegrationStatusCode.Running,
+ },
+ autoDiscovery: {
+ config: {
+ name: 'discovery-config-name',
+ discoveryGroup: 'discovery-group-name',
+ aws: [],
+ },
+ },
+ },
+ currentStep: 0,
+ nextStep: () => null,
+ prevStep: () => null,
+ onSelectResource: () => null,
+ resourceSpec: {
+ name: '',
+ kind: ResourceKind.Application,
+ icon: null,
+ keywords: '',
+ event: DiscoverEventResource.Ec2Instance,
+ nodeMeta: {
+ location: ServerLocation.Aws,
+ discoveryConfigMethod: DiscoverDiscoveryConfigMethod.AwsEc2Ssm,
+ },
+ },
+ exitFlow: () => null,
+ viewConfig: null,
+ indexedViews: [],
+ setResourceSpec: () => null,
+ updateAgentMeta: () => null,
+ emitErrorEvent: () => null,
+ emitEvent: () => null,
+ eventState: null,
+ };
+
+ cfg.proxyCluster = 'localhost';
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx
new file mode 100644
index 0000000000000..1511701659bc2
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/ConfigureDiscoveryService/ConfigureDiscoveryService.tsx
@@ -0,0 +1,67 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React, { useState } from 'react';
+import { Box, Text } from 'design';
+
+import { useDiscover } from 'teleport/Discover/useDiscover';
+import useTeleport from 'teleport/useTeleport';
+
+import { SelfHostedAutoDiscoverDirections } from 'teleport/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections';
+import { DEFAULT_DISCOVERY_GROUP_NON_CLOUD } from 'teleport/services/discovery';
+
+import { ActionButtons, Header } from '../../Shared';
+import { SingleEc2InstanceInstallation } from '../Shared';
+
+export function ConfigureDiscoveryService() {
+ const { nextStep, prevStep, agentMeta, updateAgentMeta } = useDiscover();
+
+ const [discoveryGroupName, setDiscoveryGroupName] = useState(
+ DEFAULT_DISCOVERY_GROUP_NON_CLOUD
+ );
+
+ const { storeUser } = useTeleport();
+
+ function handleNextStep() {
+ updateAgentMeta({
+ ...agentMeta,
+ autoDiscovery: {
+ config: { name: '', aws: [], discoveryGroup: discoveryGroupName },
+ },
+ });
+ nextStep();
+ }
+
+ return (
+
+ Configure Teleport Discovery Service
+
+ The Teleport Discovery Service can connect to Amazon EC2 and
+ automatically discover and enroll EC2 instances.
+
+
+
+
+
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx
new file mode 100644
index 0000000000000..4146162067ad5
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigCreatedDialog.tsx
@@ -0,0 +1,53 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { Text, Flex, ButtonPrimary, Box } from 'design';
+import * as Icons from 'design/Icon';
+import Dialog, { DialogContent } from 'design/DialogConfirmation';
+
+export function DiscoveryConfigCreatedDialog({
+ toNextStep,
+}: {
+ toNextStep: () => void;
+}) {
+ return (
+
+ );
+}
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx
new file mode 100644
index 0000000000000..bc4a4db243854
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.story.tsx
@@ -0,0 +1,187 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React, { useEffect } from 'react';
+import { MemoryRouter } from 'react-router';
+import { initialize, mswLoader } from 'msw-storybook-addon';
+import { rest } from 'msw';
+import { Info } from 'design/Alert';
+
+import { ContextProvider } from 'teleport';
+import cfg from 'teleport/config';
+import { createTeleportContext } from 'teleport/mocks/contexts';
+import {
+ DiscoverProvider,
+ DiscoverContextState,
+} from 'teleport/Discover/useDiscover';
+import {
+ IntegrationKind,
+ IntegrationStatusCode,
+} from 'teleport/services/integrations';
+import { ResourceKind } from 'teleport/Discover/Shared';
+import {
+ DiscoverDiscoveryConfigMethod,
+ DiscoverEventResource,
+} from 'teleport/services/userEvent';
+import { ServerLocation } from 'teleport/Discover/SelectResource';
+
+import { DiscoveryConfigSsm } from './DiscoveryConfigSsm';
+
+initialize();
+
+const defaultIsCloud = cfg.isCloud;
+export default {
+ title: 'Teleport/Discover/Server/EC2/DiscoveryConfigSsm',
+ loaders: [mswLoader],
+ decorators: [
+ Story => {
+ useEffect(() => {
+ // Clean up
+ return () => {
+ cfg.isCloud = defaultIsCloud;
+ };
+ }, []);
+ return ;
+ },
+ ],
+};
+
+export const SuccessCloud = () => {
+ cfg.isCloud = true;
+ return ;
+};
+SuccessCloud.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.joinTokenPath, (req, res, ctx) =>
+ res(ctx.json({ id: 'token-id' }))
+ ),
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res(ctx.json({ name: 'discovery-cfg-name' }))
+ ),
+ ],
+ },
+};
+
+export const SuccessSelfHosted = () => ;
+SuccessSelfHosted.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.joinTokenPath, (req, res, ctx) =>
+ res(ctx.json({ id: 'token-id' }))
+ ),
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res(ctx.json({ name: 'discovery-cfg-name' }))
+ ),
+ ],
+ },
+};
+
+export const Loading = () => {
+ cfg.isCloud = true;
+ return ;
+};
+Loading.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.joinTokenPath, (req, res, ctx) =>
+ res(ctx.json({ id: 'token-id' }))
+ ),
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res(ctx.delay('infinite'))
+ ),
+ ],
+ },
+};
+
+export const Failed = () => ;
+Failed.parameters = {
+ msw: {
+ handlers: [
+ rest.post(cfg.api.joinTokenPath, (req, res, ctx) =>
+ res(ctx.json({ id: 'token-id' }))
+ ),
+ rest.post(cfg.api.discoveryConfigPath, (req, res, ctx) =>
+ res(
+ ctx.status(403),
+ ctx.json({ message: 'Some kind of error message' })
+ )
+ ),
+ ],
+ },
+};
+
+const Component = () => {
+ const ctx = createTeleportContext();
+ const discoverCtx: DiscoverContextState = {
+ agentMeta: {
+ resourceName: 'aws-console',
+ agentMatcherLabels: [],
+ awsIntegration: {
+ kind: IntegrationKind.AwsOidc,
+ name: 'some-oidc-name',
+ resourceType: 'integration',
+ spec: {
+ roleArn: 'arn:aws:iam::123456789012:role/test-role-arn',
+ issuerS3Bucket: '',
+ issuerS3Prefix: '',
+ },
+ statusCode: IntegrationStatusCode.Running,
+ },
+ },
+ currentStep: 0,
+ nextStep: () => null,
+ prevStep: () => null,
+ onSelectResource: () => null,
+ resourceSpec: {
+ name: '',
+ kind: ResourceKind.Application,
+ icon: null,
+ keywords: '',
+ event: DiscoverEventResource.Ec2Instance,
+ nodeMeta: {
+ location: ServerLocation.Aws,
+ discoveryConfigMethod: DiscoverDiscoveryConfigMethod.AwsEc2Ssm,
+ },
+ },
+ exitFlow: () => null,
+ viewConfig: null,
+ indexedViews: [],
+ setResourceSpec: () => null,
+ updateAgentMeta: () => null,
+ emitErrorEvent: () => null,
+ emitEvent: () => null,
+ eventState: null,
+ };
+
+ cfg.proxyCluster = 'localhost';
+ return (
+
+
+
+ Devs: Click next to see next state
+
+
+
+
+ );
+};
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
new file mode 100644
index 0000000000000..799846063ec9c
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -0,0 +1,380 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React, { useState, useRef } from 'react';
+import { Box, Link as ExternalLink, Text, Flex, ButtonSecondary } from 'design';
+import styled from 'styled-components';
+import { Danger, Info } from 'design/Alert';
+import TextEditor from 'shared/components/TextEditor';
+import { ToolTipInfo } from 'shared/components/ToolTip';
+import FieldInput from 'shared/components/FieldInput';
+import { Rule } from 'shared/components/Validation/rules';
+import Validation, { Validator } from 'shared/components/Validation';
+import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
+import { TextSelectCopyMulti } from 'shared/components/TextSelectCopy';
+
+import cfg from 'teleport/config';
+import { useDiscover } from 'teleport/Discover/useDiscover';
+import { Regions } from 'teleport/services/integrations';
+import { AwsRegionSelector } from 'teleport/Discover/Shared/AwsRegionSelector';
+import JoinTokenService, { JoinToken } from 'teleport/services/joinToken';
+
+import {
+ DISCOVERY_GROUP_CLOUD,
+ createDiscoveryConfig,
+ InstallParamEnrollMode,
+} from 'teleport/services/discovery';
+import { splitAwsIamArn } from 'teleport/services/integrations/aws';
+import useStickyClusterId from 'teleport/useStickyClusterId';
+
+import { ActionButtons, Header, Mark, StyledBox } from '../../Shared';
+
+import { SingleEc2InstanceInstallation } from '../Shared';
+
+import { DiscoveryConfigCreatedDialog } from './DiscoveryConfigCreatedDialog';
+
+const IAM_POLICY_NAME = 'EC2DiscoverWithSSM';
+
+export function DiscoveryConfigSsm() {
+ const { agentMeta, emitErrorEvent, nextStep, updateAgentMeta, prevStep } =
+ useDiscover();
+
+ const { arnResourceName, awsAccountId } = splitAwsIamArn(
+ agentMeta.awsIntegration.spec.roleArn
+ );
+
+ const { clusterId } = useStickyClusterId();
+
+ const [selectedRegion, setSelectedRegion] = useState();
+ const [ssmDocumentName, setSsmDocumentName] = useState(
+ 'TeleportDiscoveryInstaller'
+ );
+ const [scriptUrl, setScriptUrl] = useState('');
+ const joinTokenRef = useRef();
+ const [showRestOfSteps, setShowRestOfSteps] = useState(false);
+
+ const [attempt, createJoinTokenAndDiscoveryConfig, setAttempt] = useAsync(
+ async () => {
+ try {
+ const joinTokenService = new JoinTokenService();
+ // Don't create another token if token was already created.
+ // This can happen if creating discovery config attempt failed
+ // and the user retries.
+ if (!joinTokenRef.current) {
+ joinTokenRef.current = await joinTokenService.fetchJoinToken({
+ roles: ['Node'],
+ method: 'iam',
+ rules: [{ awsAccountId }],
+ });
+ }
+
+ const config = await createDiscoveryConfig(clusterId, {
+ name: crypto.randomUUID(),
+ discoveryGroup: cfg.isCloud
+ ? DISCOVERY_GROUP_CLOUD
+ : agentMeta.autoDiscovery.config.discoveryGroup,
+ aws: [
+ {
+ types: ['ec2'],
+ regions: [selectedRegion],
+ tags: { '*': ['*'] },
+ integration: agentMeta.awsIntegration.name,
+ ssm: { documentName: ssmDocumentName },
+ install: {
+ enrollMode: InstallParamEnrollMode.Script,
+ installTeleport: true,
+ joinToken: joinTokenRef.current.id,
+ },
+ },
+ ],
+ });
+
+ updateAgentMeta({
+ ...agentMeta,
+ awsRegion: selectedRegion,
+ autoDiscovery: {
+ config,
+ },
+ });
+ } catch (err) {
+ emitErrorEvent(err.message);
+ throw err;
+ }
+ }
+ );
+
+ function generateScriptUrl(validator: Validator) {
+ if (!validator.validate()) return;
+
+ const scriptUrl = cfg.getAwsIamConfigureScriptEc2AutoDiscoverWithSsmUrl({
+ iamRoleName: arnResourceName,
+ region: selectedRegion,
+ ssmDocument: ssmDocumentName,
+ });
+ setScriptUrl(scriptUrl);
+ }
+
+ function clear() {
+ setAttempt(makeEmptyAttempt);
+ joinTokenRef.current = undefined;
+ }
+
+ function handleOnSubmit(
+ e: React.MouseEvent,
+ validator: Validator
+ ) {
+ e.preventDefault();
+ if (scriptUrl) {
+ setScriptUrl('');
+ return;
+ }
+ generateScriptUrl(validator);
+ }
+
+ return (
+
+ Setup Discovery Config for Teleport Discovery Service
+ {cfg.isCloud ? (
+
+ The Teleport Discovery Service can connect to Amazon EC2 and
+ automatically discover and enroll EC2 instances.
+
+ ) : (
+
+ Discovery config defines the setup that enables Teleport to
+ automatically discover and register instances.
+
+ )}
+ {cfg.isCloud && }
+ {attempt.status === 'error' && (
+ {attempt.statusText}
+ )}
+
+ Step 1
+
+
+ Select the AWS Region that contains the EC2 instances that you would
+ like to enroll:
+
+ setSelectedRegion(region)}
+ clear={clear}
+ disableSelector={!!scriptUrl}
+ />
+
+ {!showRestOfSteps && (
+ setShowRestOfSteps(true)}
+ disabled={!selectedRegion}
+ mt={3}
+ >
+ Next
+
+ )}
+ {scriptUrl && (
+ setScriptUrl('')} mt={3}>
+ Edit
+
+ )}
+
+ {showRestOfSteps && (
+ <>
+
+ Step 2
+
+ Attach AWS managed{' '}
+
+ AmazonSSMManagedInstanceCore
+ {' '}
+ policy to EC2 instances IAM profile. The policy enables EC2
+ instances to use SSM core functionality.
+
+
+
+ Step 3
+ Each EC2 instance requires{' '}
+
+ SSM Agent
+ {' '}
+ to be running. The SSM{' '}
+
+ Nodes Manager dashboard
+ {' '}
+ will list all instances that have SSM agent already running. Ensure
+ ping statuses are Online.
+
+ If you do not see your instances listed in the dashboard, it might
+ take up to 30 minutes for your instances to use the IAM
+ credentials you updated in step 2.
+
+
+
+ {({ validator }) => (
+
+ )}
+
+ {scriptUrl && (
+
+ Step 5
+
+
+ Run the command below on your{' '}
+
+ AWS CloudShell
+ {' '}
+ to configure your IAM permissions.
+
+
+ The following IAM permissions will be added as an inline
+ policy named {IAM_POLICY_NAME} to IAM role{' '}
+ {arnResourceName}
+
+
+
+
+
+
+
+
+
+ )}
+ >
+ )}
+
+ {attempt.status === 'success' && (
+
+ )}
+
+
+
+ );
+}
+
+const EditorWrapper = styled(Flex)`
+ flex-directions: column;
+ height: ${p => p.$height}px;
+ margin-top: ${p => p.theme.space[3]}px;
+ width: 450px;
+`;
+
+const inlinePolicyJson = `{
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Effect": "Allow",
+ "Action": [
+ "ec2:DescribeInstances",
+ "ssm:DescribeInstanceInformation",
+ "ssm:GetCommandInvocation",
+ "ssm:ListCommandInvocations",
+ "ssm:SendCommand"
+ ],
+ "Resource": "*"
+ }
+ ]
+}`;
+
+const SSM_DOCUMENT_NAME_REGEX = new RegExp('^[0-9A-Za-z._-]*$');
+const requiredSsmDocument: Rule = name => () => {
+ if (!name || name.length < 3 || name.length > 128) {
+ return {
+ valid: false,
+ message: 'name must be between 3 and 128 characters',
+ };
+ }
+
+ const match = name.match(SSM_DOCUMENT_NAME_REGEX);
+ if (!match) {
+ return {
+ valid: false,
+ message: 'valid characters are a-z, A-Z, 0-9, and _, -, and . only',
+ };
+ }
+
+ return {
+ valid: true,
+ };
+};
+
+const SsmInfoHeaderText = () => (
+ <>
+ The service will execute an install script on these discovered instances
+ using{' '}
+
+ AWS Systems Manager
+ {' '}
+ that will install Teleport, start it and join the cluster.
+ >
+);
diff --git a/web/packages/teleport/src/Discover/Server/Shared.tsx b/web/packages/teleport/src/Discover/Server/Shared.tsx
new file mode 100644
index 0000000000000..32ddfc2aed77e
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/Shared.tsx
@@ -0,0 +1,49 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import React from 'react';
+import { Link as InternalLink } from 'react-router-dom';
+import { OutlineInfo } from 'design/Alert/Alert';
+import { Box } from 'design';
+
+import cfg from 'teleport/config';
+
+import { InfoIcon } from '../Shared/InfoIcon';
+import { Mark } from '../Shared';
+
+export const SingleEc2InstanceInstallation = () => (
+
+
+
+
+
+ Auto discovery will enroll all EC2 instances found in a region. If you
+ want to enroll a single EC2 instance instead, consider
+ following the{' '}
+
+ Teleport service installation
+ {' '}
+ flow.
+
+
+);
diff --git a/web/packages/teleport/src/Discover/Server/index.tsx b/web/packages/teleport/src/Discover/Server/index.tsx
index 9e18226035d4d..1e449b88a8434 100644
--- a/web/packages/teleport/src/Discover/Server/index.tsx
+++ b/web/packages/teleport/src/Discover/Server/index.tsx
@@ -23,7 +23,11 @@ import { DownloadScript } from 'teleport/Discover/Server/DownloadScript';
import { SetupAccess } from 'teleport/Discover/Server/SetupAccess';
import { TestConnection } from 'teleport/Discover/Server/TestConnection';
import { AwsAccount, ResourceKind, Finished } from 'teleport/Discover/Shared';
-import { DiscoverEvent } from 'teleport/services/userEvent';
+import {
+ DiscoverDiscoveryConfigMethod,
+ DiscoverEvent,
+} from 'teleport/services/userEvent';
+import cfg from 'teleport/config';
import { ResourceSpec, ServerLocation } from '../SelectResource';
@@ -31,6 +35,8 @@ import { EnrollEc2Instance } from './EnrollEc2Instance/EnrollEc2Instance';
import { CreateEc2Ice } from './CreateEc2Ice/CreateEc2Ice';
import { ServerWrapper } from './ServerWrapper';
+import { DiscoveryConfigSsm } from './DiscoveryConfigSsm/DiscoveryConfigSsm';
+import { ConfigureDiscoveryService } from './ConfigureDiscoveryService/ConfigureDiscoveryService';
export const ServerResource: ResourceViewConfig = {
kind: ResourceKind.Server,
@@ -51,7 +57,12 @@ export const ServerResource: ResourceViewConfig = {
views(resource) {
let configureResourceViews;
- if (resource && resource.nodeMeta?.location === ServerLocation.Aws) {
+ const { nodeMeta } = resource;
+ if (
+ nodeMeta?.location === ServerLocation.Aws &&
+ nodeMeta.discoveryConfigMethod ===
+ DiscoverDiscoveryConfigMethod.AwsEc2Eice
+ ) {
configureResourceViews = [
{
title: 'Connect AWS Account',
@@ -70,6 +81,35 @@ export const ServerResource: ResourceViewConfig = {
manuallyEmitSuccessEvent: true,
},
];
+ } else if (
+ nodeMeta?.location === ServerLocation.Aws &&
+ nodeMeta.discoveryConfigMethod === DiscoverDiscoveryConfigMethod.AwsEc2Ssm
+ ) {
+ configureResourceViews = [
+ {
+ title: 'Connect AWS Account',
+ component: AwsAccount,
+ eventName: DiscoverEvent.IntegrationAWSOIDCConnectEvent,
+ },
+ // Self hosted requires user to manually install a discovery service.
+ // Cloud already has a discovery service running, so this step is not required.
+ ...(!cfg.isCloud
+ ? [
+ {
+ title: 'Configure Discovery Service',
+ component: ConfigureDiscoveryService,
+ eventName: DiscoverEvent.DeployService,
+ },
+ ]
+ : []),
+ {
+ title: cfg.isCloud
+ ? 'Configure Auto Discovery Service'
+ : 'Create Discovery Config',
+ component: DiscoveryConfigSsm,
+ eventName: DiscoverEvent.CreateDiscoveryConfig,
+ },
+ ];
} else {
configureResourceViews = [
{
diff --git a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx b/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx
index ac21c13d20238..992dae8f82780 100644
--- a/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx
+++ b/web/packages/teleport/src/Discover/Shared/AutoDiscovery/SelfHostedAutoDiscoverDirections.tsx
@@ -38,10 +38,12 @@ export const SelfHostedAutoDiscoverDirections = ({
clusterPublicUrl,
discoveryGroupName,
setDiscoveryGroupName,
+ showSubHeader = true,
}: {
clusterPublicUrl: string;
discoveryGroupName: string;
setDiscoveryGroupName(n: string): void;
+ showSubHeader?: boolean;
}) => {
const yamlContent = `version: v3
teleport:
@@ -61,14 +63,18 @@ discovery_service:
return (
-
-
- Auto-enrolling requires you to configure a{' '}
- Discovery Service
-
-
-
-
+ {showSubHeader && (
+ <>
+
+
+ Auto-enrolling requires you to configure a{' '}
+ Discovery Service
+
+
+
+
+ >
+ )}
Step 1: Create a Join Token
diff --git a/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx b/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx
index 544e66fef4bba..bd4b2394fe1c9 100644
--- a/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx
+++ b/web/packages/teleport/src/Discover/Shared/AwsRegionSelector/AwsRegionSelector.tsx
@@ -30,7 +30,7 @@ export function AwsRegionSelector({
clear,
}: {
onFetch(region: Regions): void;
- onRefresh(): void;
+ onRefresh?(): void;
disableSelector: boolean;
clear(): void;
}) {
@@ -58,22 +58,24 @@ export function AwsRegionSelector({
isDisabled={disableSelector}
/>
-
-
-
+ {onRefresh && (
+
+
+
+ )}
);
diff --git a/web/packages/teleport/src/Discover/Shared/InfoIcon.tsx b/web/packages/teleport/src/Discover/Shared/InfoIcon.tsx
new file mode 100644
index 0000000000000..bdec4998f8f64
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Shared/InfoIcon.tsx
@@ -0,0 +1,29 @@
+/**
+ * Teleport
+ * Copyright (C) 2024 Gravitational, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+import styled from 'styled-components';
+import { Info } from 'design/Icon';
+
+export const InfoIcon = styled(Info)`
+ background-color: ${p => p.theme.colors.link};
+ border-radius: 100px;
+ height: 32px;
+ width: 32px;
+ color: ${p => p.theme.colors.text.primaryInverse};
+ margin-right: ${p => p.theme.space[2]}px;
+`;
diff --git a/web/packages/teleport/src/config.ts b/web/packages/teleport/src/config.ts
index b92d8d24285a4..d52c6cc2fc86c 100644
--- a/web/packages/teleport/src/config.ts
+++ b/web/packages/teleport/src/config.ts
@@ -319,6 +319,9 @@ const cfg = {
awsConfigureIamAppAccessPath:
'/v1/webapi/scripts/integrations/configure/aws-app-access-iam.sh?role=:iamRoleName',
+ awsConfigureIamEc2AutoDiscoverWithSsmPath:
+ '/v1/webapi/scripts/integrations/configure/ec2-ssm-iam.sh?role=:iamRoleName&awsRegion=:region&ssmDocument=:ssmDocument',
+
eksClustersListPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/eksclusters',
eksEnrollClustersPath:
@@ -1089,6 +1092,17 @@ const cfg = {
);
},
+ getAwsIamConfigureScriptEc2AutoDiscoverWithSsmUrl(
+ params: UrlAwsConfigureIamEc2AutoDiscoverWithSsmScriptParams
+ ) {
+ return (
+ cfg.baseUrl +
+ generatePath(cfg.api.awsConfigureIamEc2AutoDiscoverWithSsmPath, {
+ ...params,
+ })
+ );
+ },
+
getAwsConfigureIamScriptListDatabasesUrl(
params: UrlAwsConfigureIamScriptParams
) {
@@ -1248,6 +1262,12 @@ export interface UrlAwsConfigureIamScriptParams {
iamRoleName: string;
}
+export interface UrlAwsConfigureIamEc2AutoDiscoverWithSsmScriptParams {
+ region: Regions;
+ iamRoleName: string;
+ ssmDocument: string;
+}
+
export interface UrlGcpWorkforceConfigParam {
orgId: string;
poolName: string;
diff --git a/web/packages/teleport/src/services/discovery/discovery.ts b/web/packages/teleport/src/services/discovery/discovery.ts
index 3ce7042a353be..38a497080dde6 100644
--- a/web/packages/teleport/src/services/discovery/discovery.ts
+++ b/web/packages/teleport/src/services/discovery/discovery.ts
@@ -73,5 +73,13 @@ function makeAwsMatchersReq(inputMatchers: AwsMatcher[]) {
tags: a.tags || {},
integration: a.integration,
kube_app_discovery: !!a.kubeAppDiscovery,
+ ssm: a.ssm ? { document_name: a.ssm.documentName } : undefined,
+ install: a.install
+ ? {
+ enroll_mode: a.install.enrollMode,
+ install_teleport: a.install.installTeleport,
+ join_token: a.install.joinToken,
+ }
+ : undefined,
}));
}
diff --git a/web/packages/teleport/src/services/discovery/types.ts b/web/packages/teleport/src/services/discovery/types.ts
index b117c8d9d5778..70a82daf03a7e 100644
--- a/web/packages/teleport/src/services/discovery/types.ts
+++ b/web/packages/teleport/src/services/discovery/types.ts
@@ -29,6 +29,11 @@ export type DiscoveryConfig = {
type AwsMatcherTypes = 'rds' | 'eks' | 'ec2';
+export enum InstallParamEnrollMode {
+ Script = 1,
+ Eice = 2,
+}
+
// AWSMatcher matches AWS EC2 instances, AWS EKS clusters and AWS Databases
export type AwsMatcher = {
// types are AWS types to match, "ec2", "eks", "rds", "redshift", "elasticache",
@@ -43,6 +48,29 @@ export type AwsMatcher = {
integration: string;
// kubeAppDiscovery specifies if Kubernetes App Discovery should be enabled for a discovered cluster.
kubeAppDiscovery?: boolean;
+ /**
+ * install sets the join method when installing on
+ * discovered EC2 nodes
+ */
+ install?: {
+ /**
+ * enrollMode indicates the mode used to enroll the node into Teleport.
+ */
+ enrollMode: InstallParamEnrollMode;
+ /**
+ * installTeleport disables agentless discovery
+ */
+ installTeleport: boolean;
+ /**
+ * joinToken is the token to use when joining the cluster
+ */
+ joinToken: string;
+ };
+ /**
+ * ssm provides options to use when sending a document command to
+ * an EC2 node
+ */
+ ssm?: { documentName: string };
};
type Labels = Record;