Skip to content

Commit

Permalink
Add tags to SSM Doc when configuring the AWS OIDC EC2 flow (#44812)
Browse files Browse the repository at this point in the history
  • Loading branch information
marcoandredinis authored Aug 12, 2024
1 parent 8151bba commit fba36e5
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 11 deletions.
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
}
14 changes: 14 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 @@ -157,6 +158,19 @@ 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 {
k, v := k, v
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 @@ -474,6 +474,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 @@ -322,7 +322,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 @@ -1261,6 +1261,7 @@ export interface UrlAwsConfigureIamEc2AutoDiscoverWithSsmScriptParams {
region: Regions;
iamRoleName: string;
ssmDocument: string;
integrationName: string;
}

export interface UrlGcpWorkforceConfigParam {
Expand Down

0 comments on commit fba36e5

Please sign in to comment.