From 91270d484144a045694eda65246121d6f6e4c672 Mon Sep 17 00:00:00 2001 From: andylim0221 Date: Tue, 26 Aug 2025 12:18:46 +0800 Subject: [PATCH] Add support for externalId in cross-account EFS configuration --- .../kubernetes/cross_account_mount/README.md | 2 +- pkg/cloud/cloud.go | 24 ++++++++++++------- pkg/driver/controller.go | 19 ++++++++++++--- pkg/driver/controller_test.go | 2 ++ 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/examples/kubernetes/cross_account_mount/README.md b/examples/kubernetes/cross_account_mount/README.md index ae90a4ed7..26dd3b596 100644 --- a/examples/kubernetes/cross_account_mount/README.md +++ b/examples/kubernetes/cross_account_mount/README.md @@ -30,7 +30,7 @@ Lets say you have an EKS cluster in aws account `A` & you wish to mount your fil 1. Perform [vpc-peering](https://docs.aws.amazon.com/vpc/latest/peering/working-with-vpc-peering.html) between EKS cluster `vpc` in aws account `A` and EFS `vpc` in another aws account `B`. 2. Create an IAM role, say `EFSCrossAccountAccessRole` in Account `B` which has a [trust relationship](./iam-policy-examples/trust-relationship-example.json) with Account `A` and add an inline EFS policy with [permissions](./iam-policy-examples/describe-mount-target-example.json) to call `DescribeMountTargets`. This role will be used by CSI-Driver's Controller service running on EKS cluster in account `A` to determine the mount targets for your file system in account `B`. 3. In aws account `A`, attach an inline policy to IAM role of efs-csi-driver's controller service account with necessary [permissions](./iam-policy-examples/cross-account-assume-policy-example.json) to perform `sts assume role` on the IAM role created in step 2. -4. Create a kubernetes secret with `awsRoleArn` as the key and the role from step 2 as the value. For example, `kubectl create secret generic x-account --namespace=default --from-literal=awsRoleArn='arn:aws:iam::123456789012:role/EFSCrossAccountAccessRole'`. If you would like to ensure that your EFS Mount Target is in the same availability zone as your EKS Node, then ensure you have completed the [prerequisites for cross-account DNS resolution](https://github.com/aws/efs-utils?tab=readme-ov-file#crossaccount-option-prerequisites) and include the `crossaccount` key with value `true`. For example, `kubectl create secret generic x-account --namespace=kube-system --from-literal=awsRoleArn='arn:aws:iam::123456789012:role/EFSCrossAccountAccessRole' --from-literal=crossaccount='true'` instead. +4. Create a kubernetes secret with `awsRoleArn` as the key and the role from step 2 as the value. For example, `kubectl create secret generic x-account --namespace=default --from-literal=awsRoleArn='arn:aws:iam::123456789012:role/EFSCrossAccountAccessRole'`.If your IAM role ARN requires externalId as validation, include `externalId` key with the value. For example, `kubectl create secret generic x-account --namespace=kube-system --from-literal=awsRoleArn='arn:aws:iam::123456789012:role/EFSCrossAccountAccessRole --from-literal=externalId="external-id"`. If you would like to ensure that your EFS Mount Target is in the same availability zone as your EKS Node, then ensure you have completed the [prerequisites for cross-account DNS resolution](https://github.com/aws/efs-utils?tab=readme-ov-file#crossaccount-option-prerequisites) and include the `crossaccount` key with value `true`. For example, `kubectl create secret generic x-account --namespace=kube-system --from-literal=awsRoleArn='arn:aws:iam::123456789012:role/EFSCrossAccountAccessRole' --from-literal=crossaccount='true'` instead. 5. Create an IAM role for service accounts for EKS cluster in account `A` with required [permissions](./iam-policy-examples/node-deamonset-iam-policy-example.json) for EFS client mount. Alternatively, you can find this policy under AWS managed policy as `AmazonElasticFileSystemClientFullAccess`. 6. Attach the service account from step 5 to node daemonset. 7. Create a [file system policy](https://docs.aws.amazon.com/efs/latest/ug/iam-access-control-nfs-efs.html#file-sys-policy-examples) for file system in account `B` which allows account `A` to perform mount on it. diff --git a/pkg/cloud/cloud.go b/pkg/cloud/cloud.go index f0bd50add..71b9e9eec 100644 --- a/pkg/cloud/cloud.go +++ b/pkg/cloud/cloud.go @@ -21,11 +21,12 @@ import ( "errors" "fmt" - "github.com/aws/smithy-go" "math/rand" "os" "time" + "github.com/aws/smithy-go" + "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials/stscreds" @@ -118,16 +119,16 @@ type cloud struct { // NewCloud returns a new instance of AWS cloud // It panics if session is invalid func NewCloud(adaptiveRetryMode bool) (Cloud, error) { - return createCloud("", adaptiveRetryMode) + return createCloud("", "", adaptiveRetryMode) } // NewCloudWithRole returns a new instance of AWS cloud after assuming an aws role // It panics if driver does not have permissions to assume role. -func NewCloudWithRole(awsRoleArn string, adaptiveRetryMode bool) (Cloud, error) { - return createCloud(awsRoleArn, adaptiveRetryMode) +func NewCloudWithRole(awsRoleArn string, externalId string, adaptiveRetryMode bool) (Cloud, error) { + return createCloud(awsRoleArn, externalId, adaptiveRetryMode) } -func createCloud(awsRoleArn string, adaptiveRetryMode bool) (Cloud, error) { +func createCloud(awsRoleArn string, externalId string, adaptiveRetryMode bool) (Cloud, error) { cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { klog.Warningf("Could not load config: %v", err) @@ -152,7 +153,7 @@ func createCloud(awsRoleArn string, adaptiveRetryMode bool) (Cloud, error) { rm := newRetryManager(adaptiveRetryMode) - efs_client := createEfsClient(awsRoleArn, metadata) + efs_client := createEfsClient(awsRoleArn, externalId, metadata) klog.V(5).Infof("EFS Client created using the following endpoint: %+v", cfg.BaseEndpoint) return &cloud{ @@ -162,11 +163,18 @@ func createCloud(awsRoleArn string, adaptiveRetryMode bool) (Cloud, error) { }, nil } -func createEfsClient(awsRoleArn string, metadata MetadataService) Efs { +func createEfsClient(awsRoleArn string, externalId string, metadata MetadataService) Efs { cfg, _ := config.LoadDefaultConfig(context.TODO(), config.WithRegion(metadata.GetRegion())) if awsRoleArn != "" { stsClient := sts.NewFromConfig(cfg) - roleProvider := stscreds.NewAssumeRoleProvider(stsClient, awsRoleArn) + var roleProvider aws.CredentialsProvider + if externalId != "" { + roleProvider = stscreds.NewAssumeRoleProvider(stsClient, awsRoleArn, func(o *stscreds.AssumeRoleOptions) { + o.ExternalID = &externalId + }) + } else { + roleProvider = stscreds.NewAssumeRoleProvider(stsClient, awsRoleArn) + } cfg.Credentials = aws.NewCredentialsCache(roleProvider) } return efs.NewFromConfig(cfg) diff --git a/pkg/driver/controller.go b/pkg/driver/controller.go index cfb4a9b4c..44b987134 100644 --- a/pkg/driver/controller.go +++ b/pkg/driver/controller.go @@ -47,6 +47,7 @@ const ( DefaultTagValue = "true" DirectoryPerms = "directoryPerms" EnsureUniqueDirectory = "ensureUniqueDirectory" + ExternalId = "externalId" FsId = "fileSystemId" Gid = "gid" GidMin = "gidRangeStart" @@ -644,6 +645,7 @@ func getCloud(secrets map[string]string, driver *Driver) (cloud.Cloud, string, b var localCloud cloud.Cloud var roleArn string + var externalId string var crossAccountDNSEnabled bool var err error @@ -652,6 +654,10 @@ func getCloud(secrets map[string]string, driver *Driver) (cloud.Cloud, string, b if value, ok := secrets[RoleArn]; ok { roleArn = value } + if value, ok := secrets[ExternalId]; ok { + externalId = value + } + if value, ok := secrets[CrossAccount]; ok { crossAccountDNSEnabled, err = strconv.ParseBool(value) if err != nil { @@ -662,9 +668,16 @@ func getCloud(secrets map[string]string, driver *Driver) (cloud.Cloud, string, b } if roleArn != "" { - localCloud, err = cloud.NewCloudWithRole(roleArn, driver.adaptiveRetryMode) - if err != nil { - return nil, "", false, status.Errorf(codes.Unauthenticated, "Unable to initialize aws cloud: %v. Please verify role has the correct AWS permissions for cross account mount", err) + if externalId != "" { + localCloud, err = cloud.NewCloudWithRole(roleArn, externalId, driver.adaptiveRetryMode) + if err != nil { + return nil, "", false, status.Errorf(codes.Unauthenticated, "Unable to initialize aws cloud: %v. Please verify role has the correct AWS permissions for cross account mount", err) + } + } else { + localCloud, err = cloud.NewCloudWithRole(roleArn, "", driver.adaptiveRetryMode) + if err != nil { + return nil, "", false, status.Errorf(codes.Unauthenticated, "Unable to initialize aws cloud: %v. Please verify role has the correct AWS permissions for cross account mount", err) + } } } else { localCloud = driver.cloud diff --git a/pkg/driver/controller_test.go b/pkg/driver/controller_test.go index 4413234f6..026915aa5 100644 --- a/pkg/driver/controller_test.go +++ b/pkg/driver/controller_test.go @@ -2888,6 +2888,7 @@ func TestCreateVolume(t *testing.T) { secrets := map[string]string{} secrets["awsRoleArn"] = "arn:aws:iam::1234567890:role/EFSCrossAccountRole" + secrets["externalId"] = "external-id" secrets["crossaccount"] = "true" req := &csi.CreateVolumeRequest{ @@ -4091,6 +4092,7 @@ func TestDeleteVolume(t *testing.T) { secrets := map[string]string{} secrets["awsRoleArn"] = "arn:aws:iam::1234567890:role/EFSCrossAccountRole" + secrets["externalId"] = "external-id" req := &csi.DeleteVolumeRequest{ VolumeId: volumeId,