Repository: gravitational/teleport
Pull request #54866: Add tags input to EC2 discovery config
Author: login: avatus
Labels: name: ui
name: backport-required
name: size/md
name: no-changelog
description: Indicates that a PR does not require a changelog entry
name: backport/branch/v17
Created: 2025-05-15T21:45:39Z | Merged: 2025-05-20T22:55:34Z

This adds an optional field to populate the `tags` field in the discovery config for ec2 instances. if no tags are added, we keep the wildcard tag that already exists.

![Screenshot 2025-05-15 at 4 44 13 PM](https://github.com/user-attachments/assets/0ae00a08-a10a-4dcc-9e25-81d22849d0c5)


![Screenshot 2025-05-15 at 4 45 11 PM](https://github.com/user-attachments/assets/1c7b3ac4-78d8-4737-9249-6f00c791de2b)



Diff:
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.test.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.test.tsx
new file mode 100644
index 0000000000000..43c3943947f59
--- /dev/null
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.test.tsx
@@ -0,0 +1,178 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
+ */
+
+import { Info } from 'design/Alert';
+import { fireEvent, render, screen, waitFor } from 'design/utils/testing';
+
+import cfg from 'teleport/config';
+import {
+  RequiredDiscoverProviders,
+  resourceSpecAwsEc2Ssm,
+} from 'teleport/Discover/Fixtures/fixtures';
+import { AgentMeta, AutoDiscovery } from 'teleport/Discover/useDiscover';
+import * as discoveryService from 'teleport/services/discovery/discovery';
+import {
+  IntegrationKind,
+  IntegrationStatusCode,
+} from 'teleport/services/integrations';
+import JoinTokenService from 'teleport/services/joinToken';
+import { userEventService } from 'teleport/services/userEvent';
+import TeleportContext from 'teleport/teleportContext';
+
+import { DiscoveryConfigSsm } from './DiscoveryConfigSsm';
+
+async function submitConfig() {
+  fireEvent.click(screen.getByTestId('action-next'));
+  await waitFor(() => {
+    expect(screen.getByTestId('action-next')).toBeDisabled();
+  });
+  expect(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1);
+}
+
+const defaultIsCloud = cfg.isCloud;
+
+async function completeForm() {
+  expect(
+    screen.getByText('Setup Discovery Config for Teleport Discovery Service')
+  ).toBeInTheDocument();
+  let selectEl = screen.getByLabelText(/aws region/i);
+  fireEvent.focus(selectEl);
+  fireEvent.keyDown(selectEl, { key: 'ArrowDown' });
+  fireEvent.click(screen.getByText('us-east-2'));
+  fireEvent.click(screen.getByTestId('region-next'));
+  fireEvent.click(screen.getByTestId('script-next'));
+  await waitFor(() => {
+    expect(
+      screen.getByText(/You can filter for EC2 instances by their tags/i)
+    ).toBeInTheDocument();
+  });
+}
+
+describe('DiscoveryConfigSsm', () => {
+  beforeEach(() => {
+    cfg.isCloud = true;
+
+    jest
+      .spyOn(userEventService, 'captureDiscoverEvent')
+      .mockResolvedValue(undefined as never);
+    jest.spyOn(discoveryService, 'createDiscoveryConfig').mockResolvedValue({
+      name: '',
+      discoveryGroup: '',
+      aws: [],
+    });
+
+    jest
+      .spyOn(JoinTokenService.prototype, 'fetchJoinTokenV2')
+      .mockResolvedValue(tokenResp);
+  });
+
+  afterEach(() => {
+    jest.clearAllMocks();
+    jest.resetAllMocks();
+    cfg.isCloud = defaultIsCloud;
+  });
+
+  test('calls with wildcard tag', async () => {
+    render(<Component />);
+    await completeForm();
+    await submitConfig();
+    const [[, configArg]] = (
+      discoveryService.createDiscoveryConfig as jest.Mock
+    ).mock.calls;
+    expect(configArg.aws[0].tags).toEqual({ '*': ['*'] });
+  });
+
+  test('calls with entered tags and duplicate tags add to multivalue', async () => {
+    render(<Component />);
+    await completeForm();
+    // add tags
+
+    fireEvent.click(screen.getByText(/add a tag/i));
+    const keyInput = screen.getByPlaceholderText('label key');
+    const valInput = screen.getByPlaceholderText('label value');
+
+    fireEvent.change(keyInput, { target: { value: 'asdf' } });
+    fireEvent.change(valInput, { target: { value: 'fdsa' } });
+    fireEvent.click(screen.getByText(/add another tag/i));
+
+    const keyInput2 = screen.getAllByPlaceholderText('label key')[1];
+    const valInput2 = screen.getAllByPlaceholderText('label value')[1];
+    fireEvent.change(keyInput2, {
+      target: { value: 'asdf' },
+    });
+    fireEvent.change(valInput2, {
+      target: { value: 'ffff' },
+    });
+
+    await submitConfig();
+    const [[, configArg]] = (
+      discoveryService.createDiscoveryConfig as jest.Mock
+    ).mock.calls;
+    expect(configArg.aws[0].tags).toEqual({ asdf: ['fdsa', 'ffff'] });
+  });
+});
+
+const Component = ({
+  autoDiscovery = undefined,
+}: {
+  autoDiscovery?: AutoDiscovery;
+}) => {
+  const ctx = new TeleportContext();
+  const agentMeta: 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,
+    },
+    autoDiscovery,
+  };
+
+  return (
+    <RequiredDiscoverProviders
+      agentMeta={agentMeta}
+      resourceSpec={resourceSpecAwsEc2Ssm}
+      teleportCtx={ctx}
+    >
+      <Info>Devs: Click next to see next state</Info>
+      <DiscoveryConfigSsm />
+    </RequiredDiscoverProviders>
+  );
+};
+const tokenResp = {
+  allow: undefined,
+  bot_name: undefined,
+  content: undefined,
+  expiry: null,
+  expiryText: '',
+  gcp: undefined,
+  id: undefined,
+  isStatic: undefined,
+  method: undefined,
+  internalResourceId: 'abc',
+  roles: ['Application'],
+  safeName: undefined,
+  suggestedLabels: [],
+};
diff --git a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
index 073c5fb598ea5..f49b34461fd03 100644
--- a/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
+++ b/web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
@@ -39,6 +39,7 @@ import Validation, { Validator } from 'shared/components/Validation';
 import { Rule } from 'shared/components/Validation/rules';
 import { makeEmptyAttempt, useAsync } from 'shared/hooks/useAsync';
 
+import { LabelsInput } from 'teleport/components/LabelsInput';
 import cfg from 'teleport/config';
 import { AwsRegionSelector } from 'teleport/Discover/Shared/AwsRegionSelector';
 import { useDiscover } from 'teleport/Discover/useDiscover';
@@ -46,6 +47,7 @@ import {
   createDiscoveryConfig,
   DISCOVERY_GROUP_CLOUD,
   InstallParamEnrollMode,
+  Labels,
 } from 'teleport/services/discovery';
 import { Regions } from 'teleport/services/integrations';
 import { splitAwsIamArn } from 'teleport/services/integrations/aws';
@@ -62,6 +64,35 @@ import { DiscoveryConfigCreatedDialog } from './DiscoveryConfigCreatedDialog';
 
 const IAM_POLICY_NAME = 'EC2DiscoverWithSSM';
 
+type AWSLabel = {
+  name: string;
+  value: string;
+};
+
+type AWSLabels = AWSLabel[];
+
+function makeTags(labels: AWSLabels): Labels {
+  if (labels.length === 0) {
+    return { '*': ['*'] };
+  }
+  const output: Labels = {};
+
+  labels.forEach(label => {
+    if (label.name === '' || label.value === '') {
+      return;
+    }
+
+    if (output[label.name]) {
+      const labelSet = new Set(output[label.name]);
+      labelSet.add(label.value);
+      output[label.name] = Array.from(labelSet);
+      return;
+    }
+    output[label.name] = [label.value];
+  });
+  return output;
+}
+
 export function DiscoveryConfigSsm() {
   const {
     agentMeta,
@@ -84,6 +115,7 @@ export function DiscoveryConfigSsm() {
   );
   const [scriptUrl, setScriptUrl] = useState('');
   const joinTokenRef = useRef<JoinToken>(undefined);
+  const [tags, setTags] = useState<AWSLabels>([]);
   const [showRestOfSteps, setShowRestOfSteps] = useState(false);
 
   const [attempt, createJoinTokenAndDiscoveryConfig, setAttempt] = useAsync(
@@ -110,7 +142,7 @@ export function DiscoveryConfigSsm() {
             {
               types: ['ec2'],
               regions: [selectedRegion],
-              tags: { '*': ['*'] },
+              tags: makeTags(tags),
               integration: agentMeta.awsIntegration.name,
               ssm: { documentName: ssmDocumentName },
               install: {
@@ -174,94 +206,110 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will enroll all EC2 instances.
+                </P>
+                <LabelsInput
+                  adjective="tag"
+                  labels={tags}
+                  setLabels={setTags}
+                />
+              </Box>
+            )}
+            {!showRestOfSteps && (
+              <ButtonSecondary
+                onClick={() => setShowRestOfSteps(true)}
+                disabled={!selectedRegion}
+                data-testid="region-next"
+                mt={3}
               >
-                Nodes Manager dashboard
-              </ExternalLink>{' '}
-              will list all instances that have SSM agent already running.
-              Ensure ping statuses are <Mark>Online</Mark>.
-            </P>
-            <Info mt={3} mb={0}>
-              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.
-            </Info>
+                Next
+              </ButtonSecondary>
+            )}
+            {scriptUrl && (
+              <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
+                Edit
+              </ButtonSecondary>
+            )}
           </StyledBox>
-          <Validation>
-            {({ validator }) => (
+          {showRestOfSteps && (
+            <>
+              <StyledBox mt={4}>
+                <H3>Step 2</H3>
+                <P>
+                  Attach AWS managed{' '}
+                  <ExternalLink
+                    target="_blank"
+                    href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
+                  >
+                    AmazonSSMManagedInstanceCore
+                  </ExternalLink>{' '}
+                  policy to EC2 instances IAM profile. The policy enables EC2
+                  instances to use SSM core functionality.
+                </P>
+              </StyledBox>
+              <StyledBox mt={4}>
+                <H3>Step 3</H3>
+                <P>
+                  Each EC2 instance requires{' '}
+                  <ExternalLink
+                    target="_blank"
+                    href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
+                  >
+                    SSM Agent
+                  </ExternalLink>{' '}
+                  to be running. The SSM{' '}
+                  <ExternalLink
+                    target="_blank"
+                    href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+                  >
+                    Nodes Manager dashboard
+                  </ExternalLink>{' '}
+                  will list all instances that have SSM agent already running.
+                  Ensure ping statuses are <Mark>Online</Mark>.
+                </P>
+                <Info mt={3} mb={0}>
+                  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.
+                </Info>
+              </StyledBox>
               <form>
                 <StyledBox mt={4}>
                   <H3>Step 4</H3>
@@ -290,64 +338,65 @@ export function DiscoveryConfigSsm() {
                     type="submit"
                     onClick={e => handleOnSubmit(e, validator)}
                     disabled={!selectedRegion}
+                    data-testid="script-next"
                   >
                     {scriptUrl ? 'Edit' : 'Next'}
                   </ButtonSecondary>
                 </StyledBox>
               </form>
-            )}
-          </Validation>
-          {scriptUrl && (
-            <StyledBox mt={4}>
-              <H3>Step 5</H3>
-              <Flex alignItems="center" gap={1} mb={2}>
-                <P>
-                  Run the command below on your{' '}
-                  <ExternalLink
-                    href="https://console.aws.amazon.com/cloudshell/home"
-                    target="_blank"
-                  >
-                    AWS CloudShell
-                  </ExternalLink>{' '}
-                  to configure your IAM permissions.
-                </P>
-                <IconTooltip sticky={true} maxWidth={450}>
-                  The following IAM permissions will be added as an inline
-                  policy named <Mark>{IAM_POLICY_NAME}</Mark> to IAM role{' '}
-                  <Mark>{arnResourceName}</Mark>
-                  <Box mb={2}>
-                    <EditorWrapper $height={350}>
-                      <TextEditor
-                        readOnly={true}
-                        data={[{ content: inlinePolicyJson, type: 'json' }]}
-                        bg="levels.deep"
-                      />
-                    </EditorWrapper>
-                  </Box>
-                </IconTooltip>
-              </Flex>
-              <TextSelectCopyMulti
-                lines={[{ text: `bash -c "$(curl '${scriptUrl}')"` }]}
-              />
-            </StyledBox>
+              {scriptUrl && (
+                <StyledBox mt={4}>
+                  <H3>Step 5</H3>
+                  <Flex alignItems="center" gap={1} mb={2}>
+                    <P>
+                      Run the command below on your{' '}
+                      <ExternalLink
+                        href="https://console.aws.amazon.com/cloudshell/home"
+                        target="_blank"
+                      >
+                        AWS CloudShell
+                      </ExternalLink>{' '}
+                      to configure your IAM permissions.
+                    </P>
+                    <IconTooltip sticky={true} maxWidth={450}>
+                      The following IAM permissions will be added as an inline
+                      policy named <Mark>{IAM_POLICY_NAME}</Mark> to IAM role{' '}
+                      <Mark>{arnResourceName}</Mark>
+                      <Box mb={2}>
+                        <EditorWrapper $height={350}>
+                          <TextEditor
+                            readOnly={true}
+                            data={[{ content: inlinePolicyJson, type: 'json' }]}
+                            bg="levels.deep"
+                          />
+                        </EditorWrapper>
+                      </Box>
+                    </IconTooltip>
+                  </Flex>
+                  <TextSelectCopyMulti
+                    lines={[{ text: `bash -c "$(curl '${scriptUrl}')"` }]}
+                  />
+                </StyledBox>
+              )}
+            </>
           )}
-        </>
-      )}
 
-      {attempt.status === 'success' && (
-        <DiscoveryConfigCreatedDialog toNextStep={nextStep} />
-      )}
+          {attempt.status === 'success' && (
+            <DiscoveryConfigCreatedDialog toNextStep={nextStep} />
+          )}
 
-      {attempt.status === 'error' && (
-        <Danger mt={3}>{attempt.statusText}</Danger>
-      )}
+          {attempt.status === 'error' && (
+            <Danger mt={3}>{attempt.statusText}</Danger>
+          )}
 
-      <ActionButtons
-        onProceed={createJoinTokenAndDiscoveryConfig}
-        onPrev={prevStep}
-        disableProceed={attempt.status === 'processing' || !scriptUrl}
-      />
-    </>
+          <ActionButtons
+            onProceed={createJoinTokenAndDiscoveryConfig}
+            onPrev={prevStep}
+            disableProceed={attempt.status === 'processing' || !scriptUrl}
+          />
+        </>
+      )}
+    </Validation>
   );
 }
 
diff --git a/web/packages/teleport/src/Discover/Shared/ActionButtons.tsx b/web/packages/teleport/src/Discover/Shared/ActionButtons.tsx
index 6b0ae7b297b31..8ba37e86395b6 100644
--- a/web/packages/teleport/src/Discover/Shared/ActionButtons.tsx
+++ b/web/packages/teleport/src/Discover/Shared/ActionButtons.tsx
@@ -57,6 +57,7 @@ export const ActionButtons = ({
         <ButtonPrimary
           width="165px"
           onClick={onProceed}
+          data-testid="action-next"
           mr={3}
           disabled={disableProceed}
         >
diff --git a/web/packages/teleport/src/services/discovery/types.ts b/web/packages/teleport/src/services/discovery/types.ts
index 2c37c33d025f2..fe2c61a614cb4 100644
--- a/web/packages/teleport/src/services/discovery/types.ts
+++ b/web/packages/teleport/src/services/discovery/types.ts
@@ -74,4 +74,4 @@ export type AwsMatcher = {
   ssm?: { documentName: string };
 };
 
-type Labels = Record<string, string[]>;
+export type Labels = Record<string, string[]>;


Reviews:
user: login: avatus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T21:47:18Z
user: login: marcoandredinis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T08:35:56Z
user: login: avatus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T12:46:25Z
user: login: marcoandredinis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T13:04:29Z
user: login: kimlisa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T03:32:27Z
user: login: avatus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:00:55Z
user: login: avatus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:01:03Z
user: login: avatus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:03:00Z
user: login: avatus
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T14:42:04Z
user: login: kimlisa
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:36:12Z
user: login: marcoandredinis
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T15:38:28Z

Review comments:
diff_hunk: @@ -174,94 +196,111 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will use a wildcard to enroll all EC2
+                  instances.
+                </P>
+                <LabelsInput
+                  adjective="tag"
+                  labels={tags}
+                  setLabels={setTags}
+                />
+              </Box>
+            )}
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: avatus
body: this is the only change to this form. i moved the validator higher which updated the indentation so a lot of diff here that isnt really diff
created_at: 2025-05-15T21:47:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,94 +196,111 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will use a wildcard to enroll all EC2
+                  instances.
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: marcoandredinis
body: There's a couple of non-obvious things about label matchers:
- there's the special `*: [*]`
- you can use the a wildcard for any tag key: `project: *`
- you can have multiple values for the same tag key: `project: a, b`

The 1st is abstracted by the UI, by explicitly saying that removing everything will match every resource.
I'm not sure we should be explicit about the underlying representation (`*`/ _wildcard_). I would just say this:

```suggestion
                  You can filter for EC2 instances by their tags. If no tags are
                  added, Teleport will enroll all EC2 instances.
```

For the 2nd and 3rd, explaining all of this in this screen might be too complex/boring.

Either we ignore those, or we might add a informational panel explaining them:
it stays out of the user's way, but if they are interested in advanced customization, they can read the details in the informational panel.
created_at: 2025-05-16T08:25:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,94 +196,111 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will use a wildcard to enroll all EC2
+                  instances.
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: avatus
body: Hmmm interesting. Because the web UI already didn't allow any customization at all, an option would be to just let the web UI now do the "basics" like we have here and if they need more fine grained control over the tags, we send them off to tctl (we do this we join tokens for example. They can only create the basic-ish tokens there)

What are your thoughts on that path? 
created_at: 2025-05-16T12:46:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,94 +196,111 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will use a wildcard to enroll all EC2
+                  instances.
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: marcoandredinis
body: Sounds good 👍 
Keeping it simple looks good to me.
created_at: 2025-05-16T13:04:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,94 +196,111 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will use a wildcard to enroll all EC2
+                  instances.
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: kimlisa
body: i think we agreed to change something here, but i'm not sure i'm seeing it 🤔 
created_at: 2025-05-20T03:30:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
+ */
+
+import { Info } from 'design/Alert';
+import { fireEvent, render, screen, waitFor } from 'design/utils/testing';
+
+import cfg from 'teleport/config';
+import {
+  RequiredDiscoverProviders,
+  resourceSpecAwsEc2Ssm,
+} from 'teleport/Discover/Fixtures/fixtures';
+import { AgentMeta, AutoDiscovery } from 'teleport/Discover/useDiscover';
+import * as discoveryService from 'teleport/services/discovery/discovery';
+import {
+  IntegrationKind,
+  IntegrationStatusCode,
+} from 'teleport/services/integrations';
+import JoinTokenService from 'teleport/services/joinToken';
+import TeleportContext from 'teleport/teleportContext';
+
+import { DiscoveryConfigSsm } from './DiscoveryConfigSsm';
+
+function completeForm() {
+  expect(
+    screen.getByText('Setup Discovery Config for Teleport Discovery Service')
+  ).toBeInTheDocument();
+  let selectEl = screen.getByLabelText(/aws region/i);
+  fireEvent.focus(selectEl);
+  fireEvent.keyDown(selectEl, { key: 'ArrowDown' });
+  fireEvent.click(screen.getByText('us-east-2'));
+  fireEvent.click(screen.getByTestId('region-next'));
+  fireEvent.click(screen.getByTestId('script-next'));
+  expect(
+    screen.getByText(/You can filter for EC2 instances by their tags/i)
+  ).toBeInTheDocument();
+}
+
+async function submitConfig() {
+  fireEvent.click(screen.getByTestId('action-next'));
+  await waitFor(() => {
+    expect(screen.getByTestId('action-next')).toBeDisabled();
+  });
+  expect(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1);
+}
+
+const defaultIsCloud = cfg.isCloud;
+
+beforeAll(() => {
+  cfg.isCloud = true;
+});
+beforeEach(() => {
+  jest.spyOn(discoveryService, 'createDiscoveryConfig');
+  jest
+    .spyOn(JoinTokenService.prototype, 'fetchJoinTokenV2')
+    .mockResolvedValue(tokenResp);
+});
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+afterAll(() => {
+  cfg.isCloud = defaultIsCloud;
+});
+
+test('calls with wildcard tag', async () => {
+  const ctx = new TeleportContext();
+
+  render(<Component ctx={ctx} />);
+  completeForm();
+  await submitConfig();
+  const [[, configArg]] = (discoveryService.createDiscoveryConfig as jest.Mock)
+    .mock.calls;
+  expect(configArg.aws[0].tags).toEqual({ '*': ['*'] });
+});
+
+test('calls with entered tags', async () => {
+  const ctx = new TeleportContext();
+
+  render(<Component ctx={ctx} />);
+  completeForm();
+  // add tags
+
+  fireEvent.click(screen.getByText(/add a tag/i));
+  const keyInput = screen.getByPlaceholderText('label key');
+  const valInput = screen.getByPlaceholderText('label value');
+
+  fireEvent.change(keyInput, { target: { value: 'asdf' } });
+  fireEvent.change(valInput, { target: { value: 'fdsa' } });
+
+  await submitConfig();
+  const [[, configArg]] = (discoveryService.createDiscoveryConfig as jest.Mock)
+    .mock.calls;
+  expect(configArg.aws[0].tags).toEqual({ asdf: ['fdsa'] });
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.test.tsx
user: login: kimlisa
body: can we also add a test for `same key, multi value` as well?
created_at: 2025-05-20T03:31:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,25 @@ import { DiscoveryConfigCreatedDialog } from './DiscoveryConfigCreatedDialog';
 
 const IAM_POLICY_NAME = 'EC2DiscoverWithSSM';
 
+type AWSLabel = {
+  name: string;
+  value: string;
+};
+
+type AWSLabels = AWSLabel[];
+
+function makeTags(labels: AWSLabels): Labels {
+  if (labels.length === 0) {
+    return { '*': ['*'] };
+  }
+  const output: Labels = {};
+
+  labels.forEach(label => {
+    output[label.name] = [label.value];
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: kimlisa
body: does this support multi value array? (it just looks like we are replacing the array instead of appending)
created_at: 2025-05-20T03:32:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
+ */
+
+import { Info } from 'design/Alert';
+import { fireEvent, render, screen, waitFor } from 'design/utils/testing';
+
+import cfg from 'teleport/config';
+import {
+  RequiredDiscoverProviders,
+  resourceSpecAwsEc2Ssm,
+} from 'teleport/Discover/Fixtures/fixtures';
+import { AgentMeta, AutoDiscovery } from 'teleport/Discover/useDiscover';
+import * as discoveryService from 'teleport/services/discovery/discovery';
+import {
+  IntegrationKind,
+  IntegrationStatusCode,
+} from 'teleport/services/integrations';
+import JoinTokenService from 'teleport/services/joinToken';
+import TeleportContext from 'teleport/teleportContext';
+
+import { DiscoveryConfigSsm } from './DiscoveryConfigSsm';
+
+function completeForm() {
+  expect(
+    screen.getByText('Setup Discovery Config for Teleport Discovery Service')
+  ).toBeInTheDocument();
+  let selectEl = screen.getByLabelText(/aws region/i);
+  fireEvent.focus(selectEl);
+  fireEvent.keyDown(selectEl, { key: 'ArrowDown' });
+  fireEvent.click(screen.getByText('us-east-2'));
+  fireEvent.click(screen.getByTestId('region-next'));
+  fireEvent.click(screen.getByTestId('script-next'));
+  expect(
+    screen.getByText(/You can filter for EC2 instances by their tags/i)
+  ).toBeInTheDocument();
+}
+
+async function submitConfig() {
+  fireEvent.click(screen.getByTestId('action-next'));
+  await waitFor(() => {
+    expect(screen.getByTestId('action-next')).toBeDisabled();
+  });
+  expect(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1);
+}
+
+const defaultIsCloud = cfg.isCloud;
+
+beforeAll(() => {
+  cfg.isCloud = true;
+});
+beforeEach(() => {
+  jest.spyOn(discoveryService, 'createDiscoveryConfig');
+  jest
+    .spyOn(JoinTokenService.prototype, 'fetchJoinTokenV2')
+    .mockResolvedValue(tokenResp);
+});
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+afterAll(() => {
+  cfg.isCloud = defaultIsCloud;
+});
+
+test('calls with wildcard tag', async () => {
+  const ctx = new TeleportContext();
+
+  render(<Component ctx={ctx} />);
+  completeForm();
+  await submitConfig();
+  const [[, configArg]] = (discoveryService.createDiscoveryConfig as jest.Mock)
+    .mock.calls;
+  expect(configArg.aws[0].tags).toEqual({ '*': ['*'] });
+});
+
+test('calls with entered tags', async () => {
+  const ctx = new TeleportContext();
+
+  render(<Component ctx={ctx} />);
+  completeForm();
+  // add tags
+
+  fireEvent.click(screen.getByText(/add a tag/i));
+  const keyInput = screen.getByPlaceholderText('label key');
+  const valInput = screen.getByPlaceholderText('label value');
+
+  fireEvent.change(keyInput, { target: { value: 'asdf' } });
+  fireEvent.change(valInput, { target: { value: 'fdsa' } });
+
+  await submitConfig();
+  const [[, configArg]] = (discoveryService.createDiscoveryConfig as jest.Mock)
+    .mock.calls;
+  expect(configArg.aws[0].tags).toEqual({ asdf: ['fdsa'] });
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.test.tsx
user: login: avatus
body: i can add for same key, but we do not provide multivalue support yet (there is only one value per key in the UI, but we send it as an array of 1)
created_at: 2025-05-20T14:00:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -62,6 +64,25 @@ import { DiscoveryConfigCreatedDialog } from './DiscoveryConfigCreatedDialog';
 
 const IAM_POLICY_NAME = 'EC2DiscoverWithSSM';
 
+type AWSLabel = {
+  name: string;
+  value: string;
+};
+
+type AWSLabels = AWSLabel[];
+
+function makeTags(labels: AWSLabels): Labels {
+  if (labels.length === 0) {
+    return { '*': ['*'] };
+  }
+  const output: Labels = {};
+
+  labels.forEach(label => {
+    output[label.name] = [label.value];
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: avatus
body: ill append
created_at: 2025-05-20T14:01:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -174,94 +196,111 @@ export function DiscoveryConfigSsm() {
   }
 
   return (
-    <>
-      <Header>Setup Discovery Config for Teleport Discovery Service</Header>
-      {cfg.isCloud ? (
-        <Text>
-          The Teleport Discovery Service can connect to Amazon EC2 and
-          automatically discover and enroll EC2 instances. <SsmInfoHeaderText />
-        </Text>
-      ) : (
-        <Text>
-          Discovery config defines the setup that enables Teleport to
-          automatically discover and register instances. <SsmInfoHeaderText />
-        </Text>
-      )}
-      {cfg.isCloud && <SingleEc2InstanceInstallation />}
-      <StyledBox mt={4}>
-        <header>
-          <H3>Step 1</H3>
-          <Subtitle3>
-            Select the AWS Region that contains the EC2 instances that you would
-            like to enroll
-          </Subtitle3>
-        </header>
-        <Box mb={-5}>
-          <AwsRegionSelector
-            onFetch={(region: Regions) => setSelectedRegion(region)}
-            clear={clear}
-            disableSelector={!!scriptUrl}
-          />
-        </Box>
-        {!showRestOfSteps && (
-          <ButtonSecondary
-            onClick={() => setShowRestOfSteps(true)}
-            disabled={!selectedRegion}
-            mt={3}
-          >
-            Next
-          </ButtonSecondary>
-        )}
-        {scriptUrl && (
-          <ButtonSecondary onClick={() => setScriptUrl('')} mt={3}>
-            Edit
-          </ButtonSecondary>
-        )}
-      </StyledBox>
-      {showRestOfSteps && (
+    <Validation>
+      {({ validator }) => (
         <>
+          <Header>Setup Discovery Config for Teleport Discovery Service</Header>
+          {cfg.isCloud ? (
+            <Text>
+              The Teleport Discovery Service can connect to Amazon EC2 and
+              automatically discover and enroll EC2 instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          ) : (
+            <Text>
+              Discovery config defines the setup that enables Teleport to
+              automatically discover and register instances.{' '}
+              <SsmInfoHeaderText />
+            </Text>
+          )}
+          {cfg.isCloud && <SingleEc2InstanceInstallation />}
           <StyledBox mt={4}>
-            <H3>Step 2</H3>
-            <P>
-              Attach AWS managed{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/aws-managed-policy/latest/reference/AmazonSSMManagedInstanceCore.html"
-              >
-                AmazonSSMManagedInstanceCore
-              </ExternalLink>{' '}
-              policy to EC2 instances IAM profile. The policy enables EC2
-              instances to use SSM core functionality.
-            </P>
-          </StyledBox>
-          <StyledBox mt={4}>
-            <H3>Step 3</H3>
-            <P>
-              Each EC2 instance requires{' '}
-              <ExternalLink
-                target="_blank"
-                href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ssm-agent-status-and-restart.html"
-              >
-                SSM Agent
-              </ExternalLink>{' '}
-              to be running. The SSM{' '}
-              <ExternalLink
-                target="_blank"
-                href={`https://${selectedRegion}.console.aws.amazon.com/systems-manager/fleet-manager/managed-nodes?region=${selectedRegion}`}
+            <header>
+              <H3>Step 1</H3>
+              <Subtitle3>
+                Select the AWS Region that contains the EC2 instances that you
+                would like to enroll
+              </Subtitle3>
+            </header>
+            <Box mb={-5}>
+              <AwsRegionSelector
+                onFetch={(region: Regions) => setSelectedRegion(region)}
+                clear={clear}
+                disableSelector={!!scriptUrl}
+              />
+            </Box>
+            {selectedRegion && (
+              <Box mb={3} mt={5}>
+                <P>
+                  You can filter for EC2 instances by their tags. If no tags are
+                  added, Teleport will use a wildcard to enroll all EC2
+                  instances.
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.tsx
user: login: avatus
body: Its there now @kimlisa !
created_at: 2025-05-20T14:03:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,161 @@
+/**
+ * Teleport
+ * Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
+ */
+
+import { Info } from 'design/Alert';
+import { fireEvent, render, screen, waitFor } from 'design/utils/testing';
+
+import cfg from 'teleport/config';
+import {
+  RequiredDiscoverProviders,
+  resourceSpecAwsEc2Ssm,
+} from 'teleport/Discover/Fixtures/fixtures';
+import { AgentMeta, AutoDiscovery } from 'teleport/Discover/useDiscover';
+import * as discoveryService from 'teleport/services/discovery/discovery';
+import {
+  IntegrationKind,
+  IntegrationStatusCode,
+} from 'teleport/services/integrations';
+import JoinTokenService from 'teleport/services/joinToken';
+import TeleportContext from 'teleport/teleportContext';
+
+import { DiscoveryConfigSsm } from './DiscoveryConfigSsm';
+
+function completeForm() {
+  expect(
+    screen.getByText('Setup Discovery Config for Teleport Discovery Service')
+  ).toBeInTheDocument();
+  let selectEl = screen.getByLabelText(/aws region/i);
+  fireEvent.focus(selectEl);
+  fireEvent.keyDown(selectEl, { key: 'ArrowDown' });
+  fireEvent.click(screen.getByText('us-east-2'));
+  fireEvent.click(screen.getByTestId('region-next'));
+  fireEvent.click(screen.getByTestId('script-next'));
+  expect(
+    screen.getByText(/You can filter for EC2 instances by their tags/i)
+  ).toBeInTheDocument();
+}
+
+async function submitConfig() {
+  fireEvent.click(screen.getByTestId('action-next'));
+  await waitFor(() => {
+    expect(screen.getByTestId('action-next')).toBeDisabled();
+  });
+  expect(discoveryService.createDiscoveryConfig).toHaveBeenCalledTimes(1);
+}
+
+const defaultIsCloud = cfg.isCloud;
+
+beforeAll(() => {
+  cfg.isCloud = true;
+});
+beforeEach(() => {
+  jest.spyOn(discoveryService, 'createDiscoveryConfig');
+  jest
+    .spyOn(JoinTokenService.prototype, 'fetchJoinTokenV2')
+    .mockResolvedValue(tokenResp);
+});
+
+afterEach(() => {
+  jest.clearAllMocks();
+});
+
+afterAll(() => {
+  cfg.isCloud = defaultIsCloud;
+});
+
+test('calls with wildcard tag', async () => {
+  const ctx = new TeleportContext();
+
+  render(<Component ctx={ctx} />);
+  completeForm();
+  await submitConfig();
+  const [[, configArg]] = (discoveryService.createDiscoveryConfig as jest.Mock)
+    .mock.calls;
+  expect(configArg.aws[0].tags).toEqual({ '*': ['*'] });
+});
+
+test('calls with entered tags', async () => {
+  const ctx = new TeleportContext();
+
+  render(<Component ctx={ctx} />);
+  completeForm();
+  // add tags
+
+  fireEvent.click(screen.getByText(/add a tag/i));
+  const keyInput = screen.getByPlaceholderText('label key');
+  const valInput = screen.getByPlaceholderText('label value');
+
+  fireEvent.change(keyInput, { target: { value: 'asdf' } });
+  fireEvent.change(valInput, { target: { value: 'fdsa' } });
+
+  await submitConfig();
+  const [[, configArg]] = (discoveryService.createDiscoveryConfig as jest.Mock)
+    .mock.calls;
+  expect(configArg.aws[0].tags).toEqual({ asdf: ['fdsa'] });
path: web/packages/teleport/src/Discover/Server/DiscoveryConfigSsm/DiscoveryConfigSsm.test.tsx
user: login: avatus
body: actually, we can add multivalue to the API call and still use the "single key:value" as the input (if a duplicate key is added, append the values into a set that is returned with `makeTags`). ill do that instead and add tests
created_at: 2025-05-20T14:42:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: avatus
created_at: 2025-05-20T00:49:36Z
body: @kimlisa take a look when you get a chance please 🙏 
author_association: CONTRIBUTOR
user: login: backport-bot-workflows[bot]
created_at: 2025-05-20T22:56:52Z
body: 
@avatus See the table below for backport results.

| Branch | Result |
|--------|--------|
| branch/v17 | [Failed](https://github.com/gravitational/teleport/runs/42592764482?check_suite_focus=true) |

author_association: CONTRIBUTOR