diff --git a/lib/cloud/aws/policy_statements.go b/lib/cloud/aws/policy_statements.go index 801b826ea1da7..7fa5af67b82ed 100644 --- a/lib/cloud/aws/policy_statements.go +++ b/lib/cloud/aws/policy_statements.go @@ -175,15 +175,11 @@ type ExternalCloudAuditPolicyConfig struct { Region string // Account is the AWS account ID to use. Account string - // AuditEventsARN is the S3 resource ARN where audit events are stored, - // including the bucket name, (optional) prefix, and a trailing wildcard - AuditEventsARN string - // SessionRecordingsARN is the S3 resource ARN where session recordings are stored, - // including the bucket name, (optional) prefix, and a trailing wildcard - SessionRecordingsARN string - // AthenaResultsARN is the S3 resource ARN where athena results are stored, - // including the bucket name, (optional) prefix, and a trailing wildcard - AthenaResultsARN string + // S3ARNs is a list of all S3 resource ARNs used for audit events, session + // recordings, and Athena query results. For each location, it should include an ARN for the + // base bucket and another wildcard ARN for all objects within the bucket + // and an optional path/prefix. + S3ARNs []string // AthenaWorkgroupName is the name of the Athena workgroup used for queries. AthenaWorkgroupName string // GlueDatabaseName is the name of the AWS Glue database. @@ -202,14 +198,8 @@ func (c *ExternalCloudAuditPolicyConfig) CheckAndSetDefaults() error { if len(c.Account) == 0 { return trace.BadParameter("account is required") } - if len(c.AuditEventsARN) == 0 { - return trace.BadParameter("audit events ARN is required") - } - if len(c.SessionRecordingsARN) == 0 { - return trace.BadParameter("session recordings ARN is required") - } - if len(c.AthenaResultsARN) == 0 { - return trace.BadParameter("athena results ARN is required") + if len(c.S3ARNs) < 2 { + return trace.BadParameter("at least two distinct S3 ARNs are required") } if len(c.AthenaWorkgroupName) == 0 { return trace.BadParameter("athena workgroup name is required") @@ -241,12 +231,16 @@ func PolicyDocumentForExternalCloudAudit(cfg *ExternalCloudAuditPolicyConfig) (* "s3:GetObjectVersion", "s3:ListMultipartUploadParts", "s3:AbortMultipartUpload", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:ListBucketMultipartUploads", + "s3:GetBucketOwnershipControls", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketVersioning", + "s3:GetBucketLocation", }, - Resources: []string{ - cfg.AuditEventsARN, - cfg.SessionRecordingsARN, - cfg.AthenaResultsARN, - }, + Resources: cfg.S3ARNs, }, &Statement{ StatementID: "AllowAthenaQuery", diff --git a/lib/integrations/awsoidc/externalcloudaudit_iam_config.go b/lib/integrations/awsoidc/externalcloudaudit_iam_config.go index 852523237b6d7..7434bf783fc5b 100644 --- a/lib/integrations/awsoidc/externalcloudaudit_iam_config.go +++ b/lib/integrations/awsoidc/externalcloudaudit_iam_config.go @@ -25,6 +25,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/gravitational/trace" + "github.com/gravitational/teleport/api/utils" awslib "github.com/gravitational/teleport/lib/cloud/aws" "github.com/gravitational/teleport/lib/config" ) @@ -72,18 +73,22 @@ func ConfigureExternalCloudAudit( } var err error - policyCfg.AuditEventsARN, err = s3URIToObjectWildcardARN(params.Partition, params.AuditEventsURI) + bucketARN, wildcardARN, err := s3URIToResourceARNs(params.Partition, params.AuditEventsURI) if err != nil { return trace.Wrap(err, "parsing audit events URI") } - policyCfg.SessionRecordingsARN, err = s3URIToObjectWildcardARN(params.Partition, params.SessionRecordingsURI) + policyCfg.S3ARNs = append(policyCfg.S3ARNs, bucketARN, wildcardARN) + bucketARN, wildcardARN, err = s3URIToResourceARNs(params.Partition, params.SessionRecordingsURI) if err != nil { return trace.Wrap(err, "parsing session recordings URI") } - policyCfg.AthenaResultsARN, err = s3URIToObjectWildcardARN(params.Partition, params.AthenaResultsURI) + policyCfg.S3ARNs = append(policyCfg.S3ARNs, bucketARN, wildcardARN) + bucketARN, wildcardARN, err = s3URIToResourceARNs(params.Partition, params.AthenaResultsURI) if err != nil { return trace.Wrap(err, "parsing athena results URI") } + policyCfg.S3ARNs = append(policyCfg.S3ARNs, bucketARN, wildcardARN) + policyCfg.S3ARNs = utils.Deduplicate(policyCfg.S3ARNs) stsResp, err := clt.GetCallerIdentity(ctx, nil) if err != nil { @@ -116,31 +121,38 @@ func ConfigureExternalCloudAudit( return nil } -// s3URIToObjectWildcardARN takes a URI for an s3 bucket with an optional path -// prefix (folder) and returns a wildcard ARN to match all objects in that -// bucket (within the prefix). -// E.g. s3://bucketname/folder -> arn:aws:s3:::bucketname/folder/* -func s3URIToObjectWildcardARN(partition, uri string) (string, error) { +// s3URIToResourceARNs takes a URI for an s3 bucket with an optional path +// prefix, and returns two AWS s3 resource ARNS. The first is the ARN of the +// bucket, the second is a wildcard ARN matching all objects within the bucket +// and prefix. +// E.g. s3://bucketname/folder -> arn:aws:s3:::bucketname, arn:aws:s3:::bucketname/folder/* +func s3URIToResourceARNs(partition, uri string) (string, string, error) { u, err := url.Parse(uri) if err != nil { - return "", trace.BadParameter("parsing S3 URI: %v", err) + return "", "", trace.BadParameter("parsing S3 URI: %v", err) } if u.Scheme != "s3" { - return "", trace.BadParameter("URI scheme must be s3") + return "", "", trace.BadParameter("URI scheme must be s3") } bucket := u.Host + bucketARN := arn.ARN{ + Partition: partition, + Service: "s3", + Resource: bucket, + } resourcePath := bucket if folder := strings.Trim(u.Path, "/"); len(folder) > 0 { resourcePath += "/" + folder } resourcePath += "/*" - arn := arn.ARN{ + wildcardARN := arn.ARN{ Partition: partition, Service: "s3", Resource: resourcePath, } - return arn.String(), nil + + return bucketARN.String(), wildcardARN.String(), nil } diff --git a/lib/integrations/awsoidc/externalcloudaudit_iam_config_test.go b/lib/integrations/awsoidc/externalcloudaudit_iam_config_test.go index b95d676a03cc9..132e60491e446 100644 --- a/lib/integrations/awsoidc/externalcloudaudit_iam_config_test.go +++ b/lib/integrations/awsoidc/externalcloudaudit_iam_config_test.go @@ -23,6 +23,7 @@ import ( "github.com/aws/aws-sdk-go-v2/service/iam" iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/google/go-cmp/cmp" "github.com/gravitational/trace" "github.com/stretchr/testify/require" @@ -74,11 +75,22 @@ func TestConfigureExternalCloudAudit(t *testing.T) { "s3:GetObject", "s3:GetObjectVersion", "s3:ListMultipartUploadParts", - "s3:AbortMultipartUpload" + "s3:AbortMultipartUpload", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:ListBucketMultipartUploads", + "s3:GetBucketOwnershipControls", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketVersioning", + "s3:GetBucketLocation" ], "Resource": [ + "arn:aws:s3:::testbucket_noprefix", "arn:aws:s3:::testbucket_noprefix/*", + "arn:aws:s3:::testbucket", "arn:aws:s3:::testbucket/prefix/*", + "arn:aws:s3:::transientbucket", "arn:aws:s3:::transientbucket/results/*" ], "Sid": "ReadWriteSessionsAndEvents" @@ -143,11 +155,22 @@ func TestConfigureExternalCloudAudit(t *testing.T) { "s3:GetObject", "s3:GetObjectVersion", "s3:ListMultipartUploadParts", - "s3:AbortMultipartUpload" + "s3:AbortMultipartUpload", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:ListBucketMultipartUploads", + "s3:GetBucketOwnershipControls", + "s3:GetBucketPublicAccessBlock", + "s3:GetBucketObjectLockConfiguration", + "s3:GetBucketVersioning", + "s3:GetBucketLocation" ], "Resource": [ + "arn:aws-cn:s3:::testbucket_noprefix", "arn:aws-cn:s3:::testbucket_noprefix/*", + "arn:aws-cn:s3:::testbucket", "arn:aws-cn:s3:::testbucket/prefix/*", + "arn:aws-cn:s3:::transientbucket", "arn:aws-cn:s3:::transientbucket/results/*" ], "Sid": "ReadWriteSessionsAndEvents" @@ -237,7 +260,7 @@ func TestConfigureExternalCloudAudit(t *testing.T) { return } require.NoError(t, err, trace.DebugReport(err)) - require.Equal(t, tc.expectedRolePolicies, currentRolePolicies) + require.Equal(t, tc.expectedRolePolicies, currentRolePolicies, cmp.Diff(tc.expectedRolePolicies["test-role"]["test-policy"], currentRolePolicies["test-role"]["test-policy"])) }) } }