Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[v16] Add tags to SSM Doc when configuring the AWS OIDC EC2 flow #44813

Merged
merged 1 commit into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions lib/config/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,12 @@ type IntegrationConfEC2SSMIAM struct {
// No trailing / is expected.
// Eg https://tenant.teleport.sh
ProxyPublicURL string
// ClusterName is the Teleport cluster name.
// Used for resource tagging.
ClusterName string
// IntegrationName is the Teleport AWS OIDC Integration name.
// Used for resource tagging.
IntegrationName string
}

// IntegrationConfEKSIAM contains the arguments of
Expand Down
19 changes: 19 additions & 0 deletions lib/integrations/awsoidc/ec2_ssm_iam_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/gravitational/trace"

awslib "github.com/gravitational/teleport/lib/cloud/aws"
"github.com/gravitational/teleport/lib/integrations/awsoidc/tags"
)

const (
Expand Down Expand Up @@ -60,6 +61,13 @@ type EC2SSMIAMConfigureRequest struct {
// No trailing / is expected.
// Eg https://tenant.teleport.sh
ProxyPublicURL string

// ClusterName is the Teleport cluster name.
// Used for resource tagging.
ClusterName string
// IntegrationName is the Teleport AWS OIDC Integration name.
// Used for resource tagging.
IntegrationName string
}

// CheckAndSetDefaults ensures the required fields are present.
Expand All @@ -84,6 +92,14 @@ func (r *EC2SSMIAMConfigureRequest) CheckAndSetDefaults() error {
return trace.BadParameter("proxy public url is required")
}

if r.ClusterName == "" {
return trace.BadParameter("cluster name is required")
}

if r.IntegrationName == "" {
return trace.BadParameter("integration name is required")
}

return nil
}

Expand Down Expand Up @@ -165,11 +181,14 @@ func ConfigureEC2SSM(ctx context.Context, clt EC2SSMConfigureClient, req EC2SSMI

slog.InfoContext(ctx, "IntegrationRole: IAM Policy added to Role", "policy", req.IntegrationRoleEC2SSMPolicy, "role", req.IntegrationRole)

ownershipTags := tags.DefaultResourceCreationTags(req.ClusterName, req.IntegrationName)

_, err = clt.CreateDocument(ctx, &ssm.CreateDocumentInput{
Name: aws.String(req.SSMDocumentName),
DocumentType: ssmtypes.DocumentTypeCommand,
DocumentFormat: ssmtypes.DocumentFormatYaml,
Content: aws.String(awslib.EC2DiscoverySSMDocument(req.ProxyPublicURL)),
Tags: ownershipTags.ToSSMTags(),
})
if err != nil {
var docAlreadyExistsError *ssmtypes.DocumentAlreadyExists
Expand Down
40 changes: 38 additions & 2 deletions lib/integrations/awsoidc/ec2_ssm_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ func TestEC2SSMIAMConfigReqDefaults(t *testing.T) {
IntegrationRole: "integrationrole",
SSMDocumentName: "MyDoc",
ProxyPublicURL: "https://proxy.example.com",
ClusterName: "my-cluster",
IntegrationName: "my-integration",
}
}

Expand All @@ -58,6 +60,8 @@ func TestEC2SSMIAMConfigReqDefaults(t *testing.T) {
IntegrationRoleEC2SSMPolicy: "EC2DiscoverWithSSM",
SSMDocumentName: "MyDoc",
ProxyPublicURL: "https://proxy.example.com",
ClusterName: "my-cluster",
IntegrationName: "my-integration",
},
},
{
Expand All @@ -78,6 +82,24 @@ func TestEC2SSMIAMConfigReqDefaults(t *testing.T) {
},
errCheck: badParameterCheck,
},
{
name: "missing integration name",
req: func() EC2SSMIAMConfigureRequest {
req := baseReq()
req.IntegrationName = ""
return req
},
errCheck: badParameterCheck,
},
{
name: "missing cluster name",
req: func() EC2SSMIAMConfigureRequest {
req := baseReq()
req.ClusterName = ""
return req
},
errCheck: badParameterCheck,
},
{
name: "missing ssm document",
req: func() EC2SSMIAMConfigureRequest {
Expand Down Expand Up @@ -118,6 +140,8 @@ func TestEC2SSMIAMConfig(t *testing.T) {
IntegrationRole: "integrationrole",
SSMDocumentName: "MyDoc",
ProxyPublicURL: "https://proxy.example.com",
ClusterName: "my-cluster",
IntegrationName: "my-integration",
}
}

Expand Down Expand Up @@ -157,13 +181,21 @@ func TestEC2SSMIAMConfig(t *testing.T) {

err := ConfigureEC2SSM(ctx, &clt, tt.req())
tt.errCheck(t, err)
if err == nil {
require.Contains(t, clt.existingDocs, tt.req().SSMDocumentName)
require.ElementsMatch(t, []ssmtypes.Tag{
{Key: aws.String("teleport.dev/cluster"), Value: aws.String("my-cluster")},
{Key: aws.String("teleport.dev/integration"), Value: aws.String("my-integration")},
{Key: aws.String("teleport.dev/origin"), Value: aws.String("integration_awsoidc")},
}, clt.existingDocs[tt.req().SSMDocumentName])
}
})
}
}

type mockEC2SSMIAMConfigClient struct {
existingRoles []string
existingDocs []string
existingDocs map[string][]ssmtypes.Tag
}

// PutRolePolicy creates or replaces a Policy by its name in a IAM Role.
Expand All @@ -179,8 +211,12 @@ func (m *mockEC2SSMIAMConfigClient) PutRolePolicy(ctx context.Context, params *i

// CreateDocument creates an SSM document.
func (m *mockEC2SSMIAMConfigClient) CreateDocument(ctx context.Context, params *ssm.CreateDocumentInput, optFns ...func(*ssm.Options)) (*ssm.CreateDocumentOutput, error) {
if slices.Contains(m.existingDocs, aws.ToString(params.Name)) {
if m.existingDocs == nil {
m.existingDocs = make(map[string][]ssmtypes.Tag)
}
if _, ok := m.existingDocs[aws.ToString(params.Name)]; ok {
return nil, &ssmtypes.DocumentAlreadyExists{}
}
m.existingDocs[aws.ToString(params.Name)] = params.Tags
return nil, nil
}
13 changes: 13 additions & 0 deletions lib/integrations/awsoidc/tags/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
s3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"

"github.com/gravitational/teleport/api/types"
)
Expand Down Expand Up @@ -151,6 +152,18 @@ func (d AWSTags) ToAthenaTags() []athenatypes.Tag {
return athenaTags
}

// ToSSMTags returns the default tags using the expected type for SSM resources: [ssmtypes.Tag]
func (d AWSTags) ToSSMTags() []ssmtypes.Tag {
ssmTags := make([]ssmtypes.Tag, 0, len(d))
for k, v := range d {
ssmTags = append(ssmTags, ssmtypes.Tag{
Key: &k,
Value: &v,
})
}
return ssmTags
}

// ToMap returns the default tags using the expected type for other aws resources.
// Eg Glue resources
func (d AWSTags) ToMap() map[string]string {
Expand Down
10 changes: 10 additions & 0 deletions lib/integrations/awsoidc/tags/tags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
ecstypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
iamtypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -67,6 +68,15 @@ func TestDefaultTags(t *testing.T) {
require.ElementsMatch(t, expectedEC2Tags, d.ToEC2Tags())
})

t.Run("ssm tags", func(t *testing.T) {
expectedTags := []ssmtypes.Tag{
{Key: aws.String("teleport.dev/cluster"), Value: aws.String("mycluster")},
{Key: aws.String("teleport.dev/integration"), Value: aws.String("myawsaccount")},
{Key: aws.String("teleport.dev/origin"), Value: aws.String("integration_awsoidc")},
}
require.ElementsMatch(t, expectedTags, d.ToSSMTags())
})

t.Run("resource is teleport managed", func(t *testing.T) {
t.Run("ECS Tags", func(t *testing.T) {
t.Run("all tags match", func(t *testing.T) {
Expand Down
12 changes: 12 additions & 0 deletions lib/web/integrations_awsoidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,11 @@ func (h *Handler) awsOIDCConfigureAWSAppAccessIAM(w http.ResponseWriter, r *http
func (h *Handler) awsOIDCConfigureEC2SSMIAM(w http.ResponseWriter, r *http.Request, p httprouter.Params) (any, error) {
queryParams := r.URL.Query()

integrationName := queryParams.Get("integrationName")
if len(integrationName) == 0 {
return nil, trace.BadParameter("missing integrationName param")
}

role := queryParams.Get("role")
if err := aws.IsValidIAMRoleName(role); err != nil {
return nil, trace.BadParameter("invalid role %q", role)
Expand All @@ -390,6 +395,11 @@ func (h *Handler) awsOIDCConfigureEC2SSMIAM(w http.ResponseWriter, r *http.Reque
proxyPublicURL = "https://" + proxyPublicURL
}

clusterName, err := h.GetProxyClient().GetDomainName(r.Context())
if err != nil {
return nil, trace.Wrap(err)
}

// The script must execute the following command:
// teleport integration configure ec2-ssm-iam
argsList := []string{
Expand All @@ -398,6 +408,8 @@ func (h *Handler) awsOIDCConfigureEC2SSMIAM(w http.ResponseWriter, r *http.Reque
fmt.Sprintf("--aws-region=%s", shsprintf.EscapeDefaultContext(region)),
fmt.Sprintf("--ssm-document-name=%s", shsprintf.EscapeDefaultContext(ssmDocumentName)),
fmt.Sprintf("--proxy-public-url=%s", shsprintf.EscapeDefaultContext(proxyPublicURL)),
fmt.Sprintf("--cluster=%s", shsprintf.EscapeDefaultContext(clusterName)),
fmt.Sprintf("--name=%s", shsprintf.EscapeDefaultContext(integrationName)),
}
script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{
TeleportArgs: strings.Join(argsList, " "),
Expand Down
22 changes: 14 additions & 8 deletions lib/web/integrations_awsoidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,30 +284,36 @@ func TestBuildEC2SSMIAMScript(t *testing.T) {
{
name: "valid",
reqQuery: url.Values{
"awsRegion": []string{"us-east-1"},
"role": []string{"myRole"},
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
"awsRegion": []string{"us-east-1"},
"role": []string{"myRole"},
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
"integrationName": []string{"my-integration"},
},
errCheck: require.NoError,
expectedTeleportArgs: "integration configure ec2-ssm-iam " +
"--role=myRole " +
"--aws-region=us-east-1 " +
"--ssm-document-name=TeleportDiscoveryInstallerTest " +
"--proxy-public-url=" + proxyPublicURL,
"--proxy-public-url=" + proxyPublicURL + " " +
"--cluster=localhost " +
"--name=my-integration",
},
{
name: "valid with symbols in role",
reqQuery: url.Values{
"awsRegion": []string{"us-east-1"},
"role": []string{"Test+1=2,3.4@5-6_7"},
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
"awsRegion": []string{"us-east-1"},
"role": []string{"Test+1=2,3.4@5-6_7"},
"ssmDocument": []string{"TeleportDiscoveryInstallerTest"},
"integrationName": []string{"my-integration"},
},
errCheck: require.NoError,
expectedTeleportArgs: "integration configure ec2-ssm-iam " +
"--role=Test\\+1=2,3.4\\@5-6_7 " +
"--aws-region=us-east-1 " +
"--ssm-document-name=TeleportDiscoveryInstallerTest " +
"--proxy-public-url=" + proxyPublicURL,
"--proxy-public-url=" + proxyPublicURL + " " +
"--cluster=localhost " +
"--name=my-integration",
},
{
name: "missing aws-region",
Expand Down
2 changes: 2 additions & 0 deletions tool/teleport/common/integration_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ func onIntegrationConfEC2SSMIAM(ctx context.Context, params config.IntegrationCo
IntegrationRole: params.RoleName,
SSMDocumentName: params.SSMDocumentName,
ProxyPublicURL: params.ProxyPublicURL,
ClusterName: params.ClusterName,
IntegrationName: params.IntegrationName,
}
return trace.Wrap(awsoidc.ConfigureEC2SSM(ctx, awsClt, confReq))
}
Expand Down
2 changes: 2 additions & 0 deletions tool/teleport/common/teleport.go
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,8 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
integrationConfEC2SSMCmd.Flag("ssm-document-name", "The AWS SSM Document name to create that will be used to install teleport.").Required().StringVar(&ccf.IntegrationConfEC2SSMIAMArguments.SSMDocumentName)
integrationConfEC2SSMCmd.Flag("proxy-public-url", "Proxy Public URL (eg https://mytenant.teleport.sh).").StringVar(&ccf.
IntegrationConfEC2SSMIAMArguments.ProxyPublicURL)
integrationConfEC2SSMCmd.Flag("cluster", "Teleport Cluster's name.").Required().StringVar(&ccf.IntegrationConfEC2SSMIAMArguments.ClusterName)
integrationConfEC2SSMCmd.Flag("name", "Integration name.").Required().StringVar(&ccf.IntegrationConfEC2SSMIAMArguments.IntegrationName)

integrationConfAWSAppAccessCmd := integrationConfigureCmd.Command("aws-app-access-iam", "Adds required IAM permissions to connect to AWS using App Access.")
integrationConfAWSAppAccessCmd.Flag("role", "The AWS Role name used by the AWS OIDC Integration.").Required().StringVar(&ccf.IntegrationConfAWSAppAccessIAMArguments.RoleName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export function DiscoveryConfigSsm() {
iamRoleName: arnResourceName,
region: selectedRegion,
ssmDocument: ssmDocumentName,
integrationName: agentMeta.awsIntegration.name,
});
setScriptUrl(scriptUrl);
}
Expand Down
3 changes: 2 additions & 1 deletion web/packages/teleport/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ const cfg = {
'/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',
'/v1/webapi/scripts/integrations/configure/ec2-ssm-iam.sh?role=:iamRoleName&awsRegion=:region&ssmDocument=:ssmDocument&integrationName=:integrationName',

eksClustersListPath:
'/v1/webapi/sites/:clusterId/integrations/aws-oidc/:name/eksclusters',
Expand Down Expand Up @@ -1220,6 +1220,7 @@ export interface UrlAwsConfigureIamEc2AutoDiscoverWithSsmScriptParams {
region: Regions;
iamRoleName: string;
ssmDocument: string;
integrationName: string;
}

export interface UrlGcpWorkforceConfigParam {
Expand Down
Loading