From 91c5a55a8bfa92c1ebb53224f38ca589763a253c Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Fri, 14 Nov 2025 15:26:19 +1100 Subject: [PATCH 1/2] accessgraph sync: Add AWS IAM role for EKS audit logs Update the `teleport configure integration acces-graph aws-iam` command to add a permission to access EKS audit logs via CloudWatch Logs if the `--eks-audit-logs` flag is passed. This is necessary so that an integration can pull the EKS audit logs if so configured in a discovery access graph matcher. --- lib/cloud/aws/policy_statements.go | 12 ++++++++++++ lib/config/configuration.go | 2 ++ lib/integrations/awsoidc/accessgraph_sync.go | 6 ++++++ lib/integrations/awsoidc/accessgraph_sync_test.go | 1 + ...ssGraphAWSIAMConfigWithActivityCenterOuput.golden | 5 +++++ tool/teleport/common/integration_configure.go | 1 + tool/teleport/common/teleport.go | 1 + 7 files changed, 28 insertions(+) diff --git a/lib/cloud/aws/policy_statements.go b/lib/cloud/aws/policy_statements.go index 56a7469ac9e1a..00a1ce4bd3dbe 100644 --- a/lib/cloud/aws/policy_statements.go +++ b/lib/cloud/aws/policy_statements.go @@ -507,6 +507,18 @@ func StatementKMSDecrypt(kmsKeysARNs []string) *Statement { } } +// StatementEnableEKSAuditLogs returns the statement that allows fetching EKS +// API server audit logs from CloudWatch Logs. +func StatementAccessGraphAWSSyncEKSAuditLogs() *Statement { + return &Statement{ + Effect: EffectAllow, + Actions: []string{ + "logs:FilterLogEvents", + }, + Resources: []string{"arn:aws:logs:*:*:log-group:/aws/eks/*"}, + } +} + // StatementForAWSIdentityCenterAccess returns AWS IAM policy statement that grants // permissions required for Teleport identity center client. // TODO(sshah): make the roles more granular by restricting resources scoped to diff --git a/lib/config/configuration.go b/lib/config/configuration.go index f069968b551b5..6e39e021270fc 100644 --- a/lib/config/configuration.go +++ b/lib/config/configuration.go @@ -308,6 +308,8 @@ type IntegrationConfAccessGraphAWSSync struct { CloudTrailBucketARN string // KMSKeyARNs is the ARN of the KMS key to use for decrypting the Identity Security Activity Center data. KMSKeyARNs []string + // EnableEKSAuditLogs enables collection of EKS audit logs from CloudWatch logs. + EnableEKSAuditLogs bool } // IntegrationConfAccessGraphAzureSync contains the arguments of diff --git a/lib/integrations/awsoidc/accessgraph_sync.go b/lib/integrations/awsoidc/accessgraph_sync.go index 19cd8c9fb21d0..25a251c5a0e7c 100644 --- a/lib/integrations/awsoidc/accessgraph_sync.go +++ b/lib/integrations/awsoidc/accessgraph_sync.go @@ -63,6 +63,9 @@ type AccessGraphAWSIAMConfigureRequest struct { // KMSKeyARNs is the ARN of the KMS key to use for decrypting the Identity Security Activity Center data. KMSKeyARNs []string + // EnableEKSAuditLogs enables collection of EKS audit logs from CloudWatch logs. + EnableEKSAuditLogs bool + // stdout is used to override stdout output in tests. stdout io.Writer } @@ -181,6 +184,9 @@ func ConfigureAccessGraphSyncIAM(ctx context.Context, clt AccessGraphIAMConfigur statements = append(statements, awslib.StatementKMSDecrypt(req.KMSKeyARNs)) } + if req.EnableEKSAuditLogs { + statements = append(statements, awslib.StatementAccessGraphAWSSyncEKSAuditLogs()) + } policy := awslib.NewPolicyDocument( statements..., ) diff --git a/lib/integrations/awsoidc/accessgraph_sync_test.go b/lib/integrations/awsoidc/accessgraph_sync_test.go index 31a27cf0f3e40..884039410e782 100644 --- a/lib/integrations/awsoidc/accessgraph_sync_test.go +++ b/lib/integrations/awsoidc/accessgraph_sync_test.go @@ -286,6 +286,7 @@ func TestAccessGraphAWSIAMConfigWithActivityCenterOuput(t *testing.T) { SQSQueueURL: "https://sqs.us-west-2.amazonaws.com/123456789012/my-queue", CloudTrailBucketARN: "arn:aws:s3:::my-cloudtrail-bucket", KMSKeyARNs: []string{"arn:aws:kms:us-west-2:123456789012:key/my-kms-key"}, + EnableEKSAuditLogs: true, } clt := mockAccessGraphAWSAMConfigClient{ CallerIdentityGetter: mockSTSClient{accountID: req.AccountID}, diff --git a/lib/integrations/awsoidc/testdata/TestAccessGraphAWSIAMConfigWithActivityCenterOuput.golden b/lib/integrations/awsoidc/testdata/TestAccessGraphAWSIAMConfigWithActivityCenterOuput.golden index e4b441291c986..18a7f43810eb4 100644 --- a/lib/integrations/awsoidc/testdata/TestAccessGraphAWSIAMConfigWithActivityCenterOuput.golden +++ b/lib/integrations/awsoidc/testdata/TestAccessGraphAWSIAMConfigWithActivityCenterOuput.golden @@ -99,6 +99,11 @@ PutRolePolicy: { "kms:GenerateDataKeyWithoutPlaintext" ], "Resource": "arn:aws:kms:us-west-2:123456789012:key/my-kms-key" + }, + { + "Effect": "Allow", + "Action": "logs:FilterLogEvents", + "Resource": "arn:aws:logs:*:*:log-group:/aws/eks/*" } ] }, diff --git a/tool/teleport/common/integration_configure.go b/tool/teleport/common/integration_configure.go index 092e9a633e878..6e2a3d50f229f 100644 --- a/tool/teleport/common/integration_configure.go +++ b/tool/teleport/common/integration_configure.go @@ -223,6 +223,7 @@ func onIntegrationConfAccessGraphAWSSync(ctx context.Context, params config.Inte SQSQueueURL: params.SQSQueueURL, CloudTrailBucketARN: params.CloudTrailBucketARN, KMSKeyARNs: params.KMSKeyARNs, + EnableEKSAuditLogs: params.EnableEKSAuditLogs, } return trace.Wrap(awsoidc.ConfigureAccessGraphSyncIAM(ctx, clt, confReq)) } diff --git a/tool/teleport/common/teleport.go b/tool/teleport/common/teleport.go index b1f1c3f3c8262..2f9879fe1bd73 100644 --- a/tool/teleport/common/teleport.go +++ b/tool/teleport/common/teleport.go @@ -539,6 +539,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con integrationConfAccessGraphAWSSyncCmd.Flag("sqs-queue-url", "SQS Queue URL used to receive notifications from CloudTrail.").StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.SQSQueueURL) integrationConfAccessGraphAWSSyncCmd.Flag("cloud-trail-bucket", "ARN of the S3 bucket where CloudTrail writes events to.").StringVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.CloudTrailBucketARN) integrationConfAccessGraphAWSSyncCmd.Flag("kms-key", "List of KMS Keys used to decrypt SQS and S3 bucket data.").StringsVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.KMSKeyARNs) + integrationConfAccessGraphAWSSyncCmd.Flag("eks-audit-logs", "Enable collection of EKS audit logs").BoolVar(&ccf.IntegrationConfAccessGraphAWSSyncArguments.EnableEKSAuditLogs) integrationConfAccessGraphAzureSyncCmd := integrationConfAccessGraphCmd.Command("azure", "Adds required Azure permissions for syncing Azure resources into Access Graph service.") integrationConfAccessGraphAzureSyncCmd.Flag("managed-identity", "The ID of the managed identity to run the Discovery service.").Required().StringVar(&ccf.IntegrationConfAccessGraphAzureSyncArguments.ManagedIdentity) From f9a789af05e993e571b65c5c8b97a019a0431972 Mon Sep 17 00:00:00 2001 From: Cam Hutchison Date: Mon, 17 Nov 2025 14:47:59 +1100 Subject: [PATCH 2/2] web: Add web eksAuditLogs to integration configure endpoint Extend the web endpoint for the webscript for integrations configure access-graph-cloud-sync-iam.sh to add the `eksAuditLogs` query param to configure with EKS audit logs enabled. Add tests for this endpoint as there were none. --- lib/web/integrations_awsoidc.go | 14 +++ lib/web/integrations_awsoidc_test.go | 164 ++++++++++++++++++++++++++- 2 files changed, 176 insertions(+), 2 deletions(-) diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go index 5ee6a60f85a0f..4032e4dec683d 100644 --- a/lib/web/integrations_awsoidc.go +++ b/lib/web/integrations_awsoidc.go @@ -25,6 +25,7 @@ import ( "maps" "net/http" "slices" + "strconv" "strings" "github.com/aws/aws-sdk-go-v2/aws/arn" @@ -1380,6 +1381,19 @@ func (h *Handler) awsAccessGraphOIDCSync(w http.ResponseWriter, r *http.Request, } } + if eksAuditLogs := queryParams.Get("eksAuditLogs"); eksAuditLogs != "" { + enabled, err := strconv.ParseBool(eksAuditLogs) + if err != nil { + // The error returned by ParseBool contains no more information than this + // error. As we canot wrap both it and trace.BadParameter, we do the + // latter as a preferred error type. + return nil, trace.BadParameter("invalid boolean value for eksAuditLogs %q", eksAuditLogs) + } + if enabled { + argsList = append(argsList, "--eks-audit-logs") + } + } + script, err := oneoff.BuildScript(oneoff.OneOffScriptParams{ EntrypointArgs: strings.Join(argsList, " "), SuccessMessage: "Success! You can now go back to the Teleport Web UI to complete the Access Graph AWS Sync enrollment.", diff --git a/lib/web/integrations_awsoidc_test.go b/lib/web/integrations_awsoidc_test.go index 7508a65cf881b..6115b8e2f409d 100644 --- a/lib/web/integrations_awsoidc_test.go +++ b/lib/web/integrations_awsoidc_test.go @@ -308,13 +308,173 @@ func TestBuildEC2SSMIAMScript(t *testing.T) { } } +func TestBuildAccessGraphCloudSyncIAMScript(t *testing.T) { + t.Parallel() + isBadParamErrFn := func(tt require.TestingT, err error, i ...any) { + require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err) + } + + env := newWebPack(t, 1) + + // Unauthenticated client for script downloading. + anonymousHTTPClient := env.proxies[0].newClient(t) + pathVars := []string{ + "webapi", + "scripts", + "integrations", + "configure", + "access-graph-cloud-sync-iam.sh", + } + endpoint := anonymousHTTPClient.Endpoint(pathVars...) + + role := "myRole" + awsAccountID := "123456789012" + sqsUrl := "https://sqs.us-west-2.amazonaws.com/123456789012/queue-name" + cloudTrailS3Bucket := "arn:aws:s3:::bucket-name" + kmsKey1 := "arn:aws:kms:us-west-2:123456789012:key/00000000-1111-2222-3333-444444444444" + kmsKey2 := "arn:aws:kms:us-west-2:123456789012:key/55555555-6666-7777-8888-999999999999" + + tests := []struct { + name string + reqRelativeURL string + reqQuery url.Values + errCheck require.ErrorAssertionFunc + expectedTeleportArgs string + }{ + { + name: "valid", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{role}, + "awsAccountID": []string{awsAccountID}, + }, + errCheck: require.NoError, + expectedTeleportArgs: "integration configure access-graph aws-iam" + + " --role=" + role + + " --aws-account-id=" + awsAccountID, + }, + { + name: "valid with cloud trail", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{role}, + "awsAccountID": []string{awsAccountID}, + "sqsUrl": []string{sqsUrl}, + "cloudTrailS3Bucket": []string{cloudTrailS3Bucket}, + "kmsKeysARNs": []string{kmsKey1, kmsKey2}, + }, + errCheck: require.NoError, + expectedTeleportArgs: "integration configure access-graph aws-iam" + + " --role=" + role + + " --aws-account-id=" + awsAccountID + + " --sqs-queue-url=" + sqsUrl + + " --cloud-trail-bucket=" + cloudTrailS3Bucket + + " --kms-key=" + kmsKey1 + + " --kms-key=" + kmsKey2, + }, + { + name: "valid with eks audit logs", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{role}, + "awsAccountID": []string{awsAccountID}, + "eksAuditLogs": []string{"true"}, + }, + errCheck: require.NoError, + expectedTeleportArgs: "integration configure access-graph aws-iam" + + " --role=" + role + + " --aws-account-id=" + awsAccountID + + " --eks-audit-logs", + }, + { + name: "valid with cloud trail and eks audit logs", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{role}, + "awsAccountID": []string{awsAccountID}, + "sqsUrl": []string{sqsUrl}, + "cloudTrailS3Bucket": []string{cloudTrailS3Bucket}, + "kmsKeysARNs": []string{kmsKey1, kmsKey2}, + "eksAuditLogs": []string{"true"}, + }, + errCheck: require.NoError, + expectedTeleportArgs: "integration configure access-graph aws-iam" + + " --role=" + role + + " --aws-account-id=" + awsAccountID + + " --sqs-queue-url=" + sqsUrl + + " --cloud-trail-bucket=" + cloudTrailS3Bucket + + " --kms-key=" + kmsKey1 + + " --kms-key=" + kmsKey2 + + " --eks-audit-logs", + }, + { + name: "valid with symbols in role", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{"Test+1=2,3.4@5-6_7"}, + "awsAccountID": []string{"123456789012"}, + }, + errCheck: require.NoError, + expectedTeleportArgs: "integration configure access-graph aws-iam " + + "--role=Test\\+1=2,3.4\\@5-6_7 " + + "--aws-account-id=123456789012", + }, + { + name: "missing kind", + reqQuery: url.Values{ + "role": []string{"myRole"}, + "awsAccountID": []string{"123456789012"}, + }, + errCheck: isBadParamErrFn, + }, + { + name: "missing role", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "awsAccountID": []string{"123456789012"}, + }, + errCheck: isBadParamErrFn, + }, + { + name: "missing awsAccountID", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{"myRole"}, + }, + errCheck: isBadParamErrFn, + }, + { + name: "trying to inject escape sequence into query params", + reqQuery: url.Values{ + "kind": []string{"aws-iam"}, + "role": []string{"'; rm -rf /tmp/dir; echo '"}, + "awsAccountID": []string{"123456789012"}, + }, + errCheck: isBadParamErrFn, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + resp, err := anonymousHTTPClient.Get(t.Context(), endpoint, tc.reqQuery) + tc.errCheck(t, err) + if err != nil { + return + } + + require.Contains(t, string(resp.Bytes()), + fmt.Sprintf("entrypointArgs='%s'\n", tc.expectedTeleportArgs), + ) + }) + } +} + func TestBuildAWSAppAccessConfigureIAMScript(t *testing.T) { t.Parallel() isBadParamErrFn := func(tt require.TestingT, err error, i ...any) { require.True(tt, trace.IsBadParameter(err), "expected bad parameter, got %v", err) } - ctx := context.Background() env := newWebPack(t, 1) // Unauthenticated client for script downloading. @@ -370,7 +530,7 @@ func TestBuildAWSAppAccessConfigureIAMScript(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - resp, err := anonymousHTTPClient.Get(ctx, endpoint, tc.reqQuery) + resp, err := anonymousHTTPClient.Get(t.Context(), endpoint, tc.reqQuery) tc.errCheck(t, err) if err != nil { return