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

[v14] feat: External Audit Storage #34740

Merged
merged 10 commits into from
Nov 20, 2023
Prev Previous commit
Next Next commit
[v14] fix: correct IAM policies for BYOB
Backport #34484 to branch/v14

This commit fixes the IAM policies generated by the oneoff
externalcloudaudit bootstrap command based on manual testing, and brings
them more in line with the original RFD
https://github.com/gravitational/cloud/blob/master/rfd/0077-Bring-your-own-bucket.md
  • Loading branch information
nklaassen committed Nov 17, 2023
commit 6b77bcc52636eb82b60bbec4f1eac28b8990914c
38 changes: 16 additions & 22 deletions lib/cloud/aws/policy_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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")
Expand Down Expand Up @@ -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",
Expand Down
36 changes: 24 additions & 12 deletions lib/integrations/awsoidc/externalcloudaudit_iam_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
29 changes: 26 additions & 3 deletions lib/integrations/awsoidc/externalcloudaudit_iam_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"]))
})
}
}
Expand Down