Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ RUN echo "Running on ${BUILDPLATFORM}, building for ${TARGETPLATFORM}."

WORKDIR /workdir

RUN apk add git build-base
RUN go env -w GOPROXY=direct
RUN apk add --no-cache git build-base ca-certificates
RUN update-ca-certificates || true
RUN go env -w GOPROXY=https://proxy.golang.org,direct

COPY go.mod .
COPY go.sum .
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,41 @@ The primary objects field of the SecretProviderClass can contain the following s
You may pass an additional sub-field to specify the file permission:
* filePermission: This optional field expects a 4 digit string which specifies the file permission for the secret that will be mounted. When not specified this will default to the parent object's file permission.

## Assume Role parameters

The provider supports an optional assume-role flow where the base credentials (from IRSA or Pod Identity) are used to call STS AssumeRole and obtain short-lived credentials for a different role.

Add the following parameters to the `parameters:` block in your `SecretProviderClass` to enable assume-role behavior:

- `assumeRoleArn`: The ARN of the IAM role to assume. When provided, the provider will call STS AssumeRole and use the resulting credentials for subsequent AWS API calls.
- `assumeRoleDurationSeconds`: Optional. The requested session duration in seconds for the assumed role. The provider accepts a numeric string and validates it; out-of-range or invalid values are ignored and a warning is emitted. A sensible upper bound is applied by the provider (12 hours / 43200s).
- `assumeRoleExternalId`: Optional. An external ID string to pass to STS AssumeRole for additional cross-account security.

Example `SecretProviderClass` snippet enabling assume-role (Pod Identity mode):

```yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: example-assume-role
spec:
provider: aws
parameters:
region: us-west-2
usePodIdentity: "true"
assumeRoleArn: "arn:aws:iam::123456789012:role/MyAssumedRole"
assumeRoleDurationSeconds: "3600"
assumeRoleExternalId: "external-id-value"
objects: |
- objectName: "MySecret"
objectType: "secretsmanager"
```

Notes:
- Ensure the base credentials obtained via Pod Identity or IRSA have permission to call `sts:AssumeRole` on the target role.
- For Pod Identity, ensure the pod-identity agent is deployed and reachable from your pods.
- If using cross-account assume-role, confirm the target role's trust policy allows the principal used by the base credentials.

## Additional Considerations

### Rotation
Expand Down
61 changes: 51 additions & 10 deletions auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package auth

import (
"context"
"strconv"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
Expand Down Expand Up @@ -41,6 +42,9 @@ type Auth struct {
podIdentityHttpTimeout *time.Duration
k8sClient k8sv1.CoreV1Interface
stsClient stscreds.AssumeRoleWithWebIdentityAPIClient
assumeRoleArn string
assumeRoleDurationSeconds string
assumeRoleExternalId string
}

// NewAuth creates an Auth object for an incoming mount request.
Expand All @@ -49,6 +53,9 @@ func NewAuth(
usePodIdentity bool,
podIdentityHttpTimeout *time.Duration,
k8sClient k8sv1.CoreV1Interface,
assumeRoleArn string,
assumeRoleDurationSeconds string,
assumeRoleExternalId string,
) (auth *Auth, e error) {
var stsClient *sts.Client

Expand All @@ -65,16 +72,19 @@ func NewAuth(
}

return &Auth{
region: region,
nameSpace: nameSpace,
svcAcc: svcAcc,
podName: podName,
preferredAddressType: preferredAddressType,
eksAddonVersion: eksAddonVersion,
usePodIdentity: usePodIdentity,
podIdentityHttpTimeout: podIdentityHttpTimeout,
k8sClient: k8sClient,
stsClient: stsClient,
region: region,
nameSpace: nameSpace,
svcAcc: svcAcc,
podName: podName,
preferredAddressType: preferredAddressType,
eksAddonVersion: eksAddonVersion,
usePodIdentity: usePodIdentity,
podIdentityHttpTimeout: podIdentityHttpTimeout,
k8sClient: k8sClient,
stsClient: stsClient,
assumeRoleArn: assumeRoleArn,
assumeRoleDurationSeconds: assumeRoleDurationSeconds,
assumeRoleExternalId: assumeRoleExternalId,
}, nil

}
Expand Down Expand Up @@ -105,6 +115,37 @@ func (p Auth) GetAWSConfig(ctx context.Context) (aws.Config, error) {
return aws.Config{}, err
}

// If an assumeRoleArn was provided, create an AssumeRole provider using the
// base credentials (from cfg) and wrap the config's Credentials with the
// resulting credentials cache so subsequent AWS calls use the assumed role.
if p.assumeRoleArn != "" {
stsClient := sts.NewFromConfig(cfg)
var optFns []func(*stscreds.AssumeRoleOptions)
if p.assumeRoleDurationSeconds != "" {
// Parse the provided duration in seconds and validate it.
if secs64, err := strconv.ParseInt(p.assumeRoleDurationSeconds, 10, 32); err == nil {
secs := int(secs64)
// Validate positive and reasonable upper bound (43200 seconds = 12 hours)
const maxSessionDuration = 43200
if secs > 0 && secs <= maxSessionDuration {
optFns = append(optFns, func(o *stscreds.AssumeRoleOptions) { o.Duration = time.Duration(secs) * time.Second })
} else {
klog.Warningf("assumeRoleDurationSeconds out of range: %d", secs)
}
} else {
klog.Warningf("Invalid assumeRoleDurationSeconds value: %s", p.assumeRoleDurationSeconds)
}
}
if p.assumeRoleExternalId != "" {
external := p.assumeRoleExternalId
optFns = append(optFns, func(o *stscreds.AssumeRoleOptions) { o.ExternalID = &external })
}

assumeProv := stscreds.NewAssumeRoleProvider(stsClient, p.assumeRoleArn, optFns...)
cfg.Credentials = aws.NewCredentialsCache(assumeProv)
klog.Infof("Using assumed role %s for AWS calls", p.assumeRoleArn)
}

// Add the user agent to the config
cfg.APIOptions = append(cfg.APIOptions, func(stack *middleware.Stack) error {
return stack.Build.Add(&userAgentMiddleware{
Expand Down
53 changes: 53 additions & 0 deletions auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ func TestNewAuth(t *testing.T) {
tt.usePodIdentity,
&tt.podIdentityHttpTimeout,
k8sClient,
"",
"",
"",
)

if tt.expectError && err == nil {
Expand Down Expand Up @@ -180,6 +183,56 @@ func TestGetAWSConfig(t *testing.T) {
}
}

func TestGetAWSConfig_AssumeRole(t *testing.T) {
timeout := 100 * time.Millisecond

auth := &Auth{
region: "someRegion",
nameSpace: "someNamespace",
svcAcc: "someSvcAcc",
podName: "somepod",
usePodIdentity: true,
podIdentityHttpTimeout: &timeout,
k8sClient: fake.NewSimpleClientset().CoreV1(),
stsClient: &mockSTS{},
assumeRoleArn: "arn:aws:iam::123456789012:role/TestRole",
assumeRoleDurationSeconds: "900",
}

cfg, err := auth.GetAWSConfig(context.Background())
if err != nil {
t.Fatalf("Unexpected error from GetAWSConfig with assume role: %v", err)
}
if cfg.Credentials == nil {
t.Fatalf("Expected credentials to be set when assume role is configured")
}
}

func TestGetAWSConfig_InvalidDuration(t *testing.T) {
timeout := 100 * time.Millisecond

auth := &Auth{
region: "someRegion",
nameSpace: "someNamespace",
svcAcc: "someSvcAcc",
podName: "somepod",
usePodIdentity: true,
podIdentityHttpTimeout: &timeout,
k8sClient: fake.NewSimpleClientset().CoreV1(),
stsClient: &mockSTS{},
assumeRoleArn: "arn:aws:iam::123456789012:role/TestRole",
assumeRoleDurationSeconds: "not-a-number",
}

cfg, err := auth.GetAWSConfig(context.Background())
if err != nil {
t.Fatalf("Unexpected error from GetAWSConfig with invalid duration: %v", err)
}
if cfg.Credentials == nil {
t.Fatalf("Expected credentials to be set even if duration is invalid")
}
}

func TestUserAgentMiddleware_ID(t *testing.T) {
middleware := &userAgentMiddleware{
providerName: "test-provider",
Expand Down
9 changes: 6 additions & 3 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.Mount
translate := attrib[transAttrib]
failoverRegion := attrib[failoverRegionAttrib]
usePodIdentityStr := attrib[usePodIdentityAttrib]
assumeRoleArn := attrib["assumeRoleArn"]
assumeRoleDuration := attrib["assumeRoleDurationSeconds"]
assumeRoleExternalId := attrib["assumeRoleExternalId"]
preferredAddressType := attrib[preferredAddressTypeAttrib]

// Validate preferred address type
Expand Down Expand Up @@ -155,7 +158,7 @@ func (s *CSIDriverProviderServer) Mount(ctx context.Context, req *v1alpha1.Mount
}
}

awsConfigs, err := s.getAwsConfigs(ctx, nameSpace, svcAcct, s.eksAddonVersion, regions, usePodIdentity, podName, preferredAddressType, s.podIdentityHttpTimeout)
awsConfigs, err := s.getAwsConfigs(ctx, nameSpace, svcAcct, s.eksAddonVersion, regions, usePodIdentity, podName, preferredAddressType, s.podIdentityHttpTimeout, assumeRoleArn, assumeRoleDuration, assumeRoleExternalId)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -239,12 +242,12 @@ func (s *CSIDriverProviderServer) getAwsRegions(ctx context.Context, region, bac
// Gets the pod's AWS creds for each lookup region
// Establishes the connection using Aws cred for each lookup region
// If at least one config is not created, error will be thrown
func (s *CSIDriverProviderServer) getAwsConfigs(ctx context.Context, nameSpace, svcAcct, eksAddonVersion string, lookupRegionList []string, usePodIdentity bool, podName string, preferredAddressType string, podIdentityHttpTimeout *time.Duration) (response []aws.Config, err error) {
func (s *CSIDriverProviderServer) getAwsConfigs(ctx context.Context, nameSpace, svcAcct, eksAddonVersion string, lookupRegionList []string, usePodIdentity bool, podName string, preferredAddressType string, podIdentityHttpTimeout *time.Duration, assumeRoleArn string, assumeRoleDuration string, assumeRoleExternalId string) (response []aws.Config, err error) {
// Get the pod's AWS creds for each lookup region.
var awsConfigsList []aws.Config

for _, region := range lookupRegionList {
awsAuth, err := auth.NewAuth(region, nameSpace, svcAcct, podName, preferredAddressType, eksAddonVersion, usePodIdentity, podIdentityHttpTimeout, s.k8sClient)
awsAuth, err := auth.NewAuth(region, nameSpace, svcAcct, podName, preferredAddressType, eksAddonVersion, usePodIdentity, podIdentityHttpTimeout, s.k8sClient, assumeRoleArn, assumeRoleDuration, assumeRoleExternalId)
if err != nil {
return nil, fmt.Errorf("%s: %s", region, err)
}
Expand Down