diff --git a/apis/v1beta1/types.go b/apis/v1beta1/types.go index 0d8883ae55..04101eb466 100644 --- a/apis/v1beta1/types.go +++ b/apis/v1beta1/types.go @@ -77,6 +77,30 @@ type AssumeRoleWithWebIdentityOptions struct { // RoleSessionName is the session name, if you wish to uniquely identify this session. // +optional RoleSessionName string `json:"roleSessionName,omitempty"` + + // TokenConfig is the Web Identity Token config to assume the role. + // +optional + TokenConfig *WebIdentityTokenConfig `json:"tokenConfig,omitempty"` +} + +// WebIdentityTokenConfig is for configuring the token +// to be used for Web Identity authentication +// +// TODO: can be later expanded to use by inlining v1.CommonCredentialSelectors, +// Env configuration is intentionally left out to not cause ambiguity +// with the deprecated direct configuration with environment variables. +type WebIdentityTokenConfig struct { + // Source is the source of the web identity token. + // +kubebuilder:validation:Enum=Secret;Filesystem + Source xpv1.CredentialsSource `json:"source"` + // A SecretRef is a reference to a secret key that contains the credentials + // that must be used to obtain the web identity token. + // +optional + SecretRef *xpv1.SecretKeySelector `json:"secretRef,omitempty"` + // Fs is a reference to a filesystem location that contains credentials that + // must be used to obtain the web identity token. + // +optional + Fs *xpv1.FsSelector `json:"fs,omitempty"` } // Upbound defines the options for authenticating using Upbound as an identity diff --git a/apis/v1beta1/zz_generated.deepcopy.go b/apis/v1beta1/zz_generated.deepcopy.go index 20af3f5138..accf51c5ae 100644 --- a/apis/v1beta1/zz_generated.deepcopy.go +++ b/apis/v1beta1/zz_generated.deepcopy.go @@ -9,6 +9,7 @@ Copyright 2022 Upbound Inc. package v1beta1 import ( + "github.com/crossplane/crossplane-runtime/apis/common/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -57,6 +58,11 @@ func (in *AssumeRoleWithWebIdentityOptions) DeepCopyInto(out *AssumeRoleWithWebI *out = new(string) **out = **in } + if in.TokenConfig != nil { + in, out := &in.TokenConfig, &out.TokenConfig + *out = new(WebIdentityTokenConfig) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AssumeRoleWithWebIdentityOptions. @@ -391,3 +397,28 @@ func (in *Upbound) DeepCopy() *Upbound { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WebIdentityTokenConfig) DeepCopyInto(out *WebIdentityTokenConfig) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(v1.SecretKeySelector) + **out = **in + } + if in.Fs != nil { + in, out := &in.Fs, &out.Fs + *out = new(v1.FsSelector) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WebIdentityTokenConfig. +func (in *WebIdentityTokenConfig) DeepCopy() *WebIdentityTokenConfig { + if in == nil { + return nil + } + out := new(WebIdentityTokenConfig) + in.DeepCopyInto(out) + return out +} diff --git a/examples/providerconfig/v1beta1/web-identity-with-token-config-secret.yaml b/examples/providerconfig/v1beta1/web-identity-with-token-config-secret.yaml new file mode 100644 index 0000000000..fb85aa9037 --- /dev/null +++ b/examples/providerconfig/v1beta1/web-identity-with-token-config-secret.yaml @@ -0,0 +1,15 @@ +apiVersion: aws.upbound.io/v1beta1 +kind: ProviderConfig +metadata: + name: webidentity-example +spec: + credentials: + source: WebIdentity + webIdentity: + roleARN: arn:aws:iam::123456789012:role/providerexamplerole + tokenConfig: + source: Secret + secretRef: + key: token + name: example-web-identity-token-secret + namespace: upbound-system diff --git a/internal/clients/aws.go b/internal/clients/aws.go index 998b58182e..368cd28258 100644 --- a/internal/clients/aws.go +++ b/internal/clients/aws.go @@ -11,6 +11,7 @@ import ( "unsafe" "github.com/aws/aws-sdk-go-v2/aws" + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/resource" "github.com/crossplane/upjet/pkg/terraform" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -34,6 +35,7 @@ const ( keyRoleArn = "role_arn" keySessionName = "session_name" keyWebIdentityTokenFile = "web_identity_token_file" + keyWebIdentityToken = "web_identity_token" keySkipCredsValidation = "skip_credentials_validation" keyS3UsePathStyle = "s3_use_path_style" keySkipMetadataApiCheck = "skip_metadata_api_check" @@ -113,8 +115,21 @@ func pushDownTerraformSetupBuilder(ctx context.Context, c client.Client, pc *v1b return errors.New(`spec.credentials.webIdentity of ProviderConfig cannot be nil when the credential source is "WebIdentity"`) } webIdentityConfig := map[string]any{ - keyRoleArn: aws.ToString(pc.Spec.Credentials.WebIdentity.RoleARN), - keyWebIdentityTokenFile: os.Getenv(envWebIdentityTokenFile), + keyRoleArn: aws.ToString(pc.Spec.Credentials.WebIdentity.RoleARN), + } + if pc.Spec.Credentials.WebIdentity.TokenConfig != nil { + tokenSelector := xpv1.CommonCredentialSelectors{ + Fs: pc.Spec.Credentials.WebIdentity.TokenConfig.Fs, + SecretRef: pc.Spec.Credentials.WebIdentity.TokenConfig.SecretRef, + } + creds, err := resource.CommonCredentialExtractor(ctx, pc.Spec.Credentials.WebIdentity.TokenConfig.Source, c, tokenSelector) + if err != nil { + return errors.Wrap(err, "cannot extract token") + } + webIdentityConfig[keyWebIdentityToken] = string(creds) + } else { + // fallback to deprecated behavior with environment variables + webIdentityConfig[keyWebIdentityTokenFile] = os.Getenv(envWebIdentityTokenFile) } if pc.Spec.Credentials.WebIdentity.RoleSessionName != "" { webIdentityConfig[keySessionName] = pc.Spec.Credentials.WebIdentity.RoleSessionName diff --git a/internal/clients/provider_config.go b/internal/clients/provider_config.go index 13607fad85..a0888c8d5a 100644 --- a/internal/clients/provider_config.go +++ b/internal/clients/provider_config.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + v1 "github.com/crossplane/crossplane-runtime/apis/common/v1" "github.com/crossplane/crossplane-runtime/pkg/fieldpath" "github.com/crossplane/crossplane-runtime/pkg/resource" @@ -44,6 +45,7 @@ const ( authKeySecret = "Secret" envWebIdentityTokenFile = "AWS_WEB_IDENTITY_TOKEN_FILE" + envWebIdentityRoleARN = "AWS_ROLE_ARN" errRoleChainConfig = "failed to load assumed role AWS config" errAWSConfig = "failed to get AWS config" errAWSConfigIRSA = "failed to get AWS config using IAM Roles for Service Accounts" @@ -110,7 +112,7 @@ func GetAWSConfig(ctx context.Context, c client.Client, mg resource.Managed) (*a return nil, errors.Wrap(err, errAWSConfigIRSA) } case authKeyWebIdentity: - cfg, err = UseWebIdentityToken(ctx, region, &pc.Spec) + cfg, err = UseWebIdentityToken(ctx, region, &pc.Spec, c) if err != nil { return nil, errors.Wrap(err, errAWSConfigWebIdentity) } @@ -318,6 +320,26 @@ func GetAssumeRoleWithWebIdentityConfig(ctx context.Context, cfg *aws.Config, we return &awsConfig, nil } +// GetAssumeRoleWithWebIdentityConfigViaTokenRetriever returns an aws.Config capable of doing +// AssumeRoleWithWebIdentity using the token obtained from the supplied stscreds.IdentityTokenRetriever. +func GetAssumeRoleWithWebIdentityConfigViaTokenRetriever(ctx context.Context, cfg *aws.Config, webID v1beta1.AssumeRoleWithWebIdentityOptions, tokenRetriever stscreds.IdentityTokenRetriever) (*aws.Config, error) { + stsclient := sts.NewFromConfig(*cfg, stsRegionOrDefault(cfg.Region)) + awsConfig, err := config.LoadDefaultConfig( + ctx, + userAgentV2, + config.WithRegion(cfg.Region), + config.WithCredentialsProvider(aws.NewCredentialsCache( + stscreds.NewWebIdentityRoleProvider( + stsclient, + aws.ToString(webID.RoleARN), + tokenRetriever, + SetWebIdentityRoleOptions(webID), + )), + ), + ) + return &awsConfig, errors.Wrap(err, "failed to assume role via web identity") +} + // UseDefault loads the default AWS config with the specified region. func UseDefault(ctx context.Context, region string) (*aws.Config, error) { if region == GlobalRegion { @@ -338,18 +360,74 @@ func UseDefault(ctx context.Context, region string) (*aws.Config, error) { return &cfg, nil } +type xpWebIdentityTokenRetriever struct { + ctx context.Context + kube client.Client + tokenSource v1.CredentialsSource + tokenSelector v1.CommonCredentialSelectors +} + +func (x *xpWebIdentityTokenRetriever) GetIdentityToken() ([]byte, error) { + token, err := resource.CommonCredentialExtractor(x.ctx, x.tokenSource, x.kube, x.tokenSelector) + return token, errors.Wrap(err, "could not extract token from tokenSource") +} + // UseWebIdentityToken calls sts.AssumeRoleWithWebIdentity using // the configuration supplied in ProviderConfig's // spec.credentials.assumeRoleWithWebIdentity. -func UseWebIdentityToken(ctx context.Context, region string, pcs *v1beta1.ProviderConfigSpec) (*aws.Config, error) { +func UseWebIdentityToken(ctx context.Context, region string, pcs *v1beta1.ProviderConfigSpec, kube client.Client) (*aws.Config, error) { + if pcs.Credentials.WebIdentity == nil { + return nil, errors.New(`spec.credentials.webIdentity of ProviderConfig cannot be nil when the credential source is "WebIdentity"`) + } + + // this is to preserve backward compatibility with + // 0.x providers working with >=1.x ProviderConfig API + // TODO: when configuring via AWS environment variable support is removed + // tokenConfig should be mandatory and this should return an error + if pcs.Credentials.WebIdentity.TokenConfig == nil { + cfg, err := UseDefault(ctx, region) + if err != nil { + return nil, errors.Wrap(err, "failed to get default AWS config") + } + return GetAssumeRoleWithWebIdentityConfig(ctx, cfg, *pcs.Credentials.WebIdentity, os.Getenv(envWebIdentityTokenFile)) + } + + // new behavior with tokenConfig in + // spec.credentials.webIdentity.tokenConfig + // the new behavior with tokenConfig does not rely on + // the AWS environment variables AWS_WEB_IDENTITY_TOKEN_FILE + // and AWS_ROLE_ARN. + // However, we start by constructing a default AWS config and + // AWS SDK enforces that when AWS_WEB_IDENTITY_TOKEN_FILE environment + // variable is set AWS_ROLE_ARN must be present. + // Otherwise, constructing the default AWS config fails. + // Hence, either both env vars must be set + // (to support the case where the controller pod has extra AWS IRSA config + // which should be automatically injecting AWS_WEB_IDENTITY_TOKEN_FILE + // and AWS_ROLE_ARN environment variables already) + // or AWS_WEB_IDENTITY_TOKEN_FILE must not exist at all. + _, foundTokenEnv := os.LookupEnv(envWebIdentityTokenFile) + _, foundRoleArnEnv := os.LookupEnv(envWebIdentityRoleARN) + if foundTokenEnv && !foundRoleArnEnv { + return nil, errors.Errorf("if you intend to use IRSA together with WebIdentity auth, environment variable %s must be set together with %s. If only WebIdentity auth without any IRSA configuration is intended, %s must be unset", + envWebIdentityRoleARN, envWebIdentityTokenFile, envWebIdentityTokenFile) + } + cfg, err := UseDefault(ctx, region) if err != nil { return nil, errors.Wrap(err, "failed to get default AWS config") } - if pcs.Credentials.WebIdentity == nil { - return nil, errors.New(`spec.credentials.webIdentity of ProviderConfig cannot be nil when the credential source is "WebIdentity"`) + tokenRetriever := &xpWebIdentityTokenRetriever{ + ctx: ctx, + kube: kube, + tokenSource: pcs.Credentials.WebIdentity.TokenConfig.Source, + tokenSelector: v1.CommonCredentialSelectors{ + Fs: pcs.Credentials.WebIdentity.TokenConfig.Fs, + SecretRef: pcs.Credentials.WebIdentity.TokenConfig.SecretRef, + }, } - return GetAssumeRoleWithWebIdentityConfig(ctx, cfg, *pcs.Credentials.WebIdentity, os.Getenv(envWebIdentityTokenFile)) + + return GetAssumeRoleWithWebIdentityConfigViaTokenRetriever(ctx, cfg, *pcs.Credentials.WebIdentity, tokenRetriever) } // UseUpbound calls sts.AssumeRoleWithWebIdentity using the configuration diff --git a/package/crds/aws.upbound.io_providerconfigs.yaml b/package/crds/aws.upbound.io_providerconfigs.yaml index 0aac917a73..623e337605 100644 --- a/package/crds/aws.upbound.io_providerconfigs.yaml +++ b/package/crds/aws.upbound.io_providerconfigs.yaml @@ -157,6 +157,50 @@ spec: description: RoleSessionName is the session name, if you wish to uniquely identify this session. type: string + tokenConfig: + description: TokenConfig is the Web Identity Token config + to assume the role. + properties: + fs: + description: Fs is a reference to a filesystem location + that contains credentials that must be used to obtain + the web identity token. + properties: + path: + description: Path is a filesystem path. + type: string + required: + - path + type: object + secretRef: + description: A SecretRef is a reference to a secret + key that contains the credentials that must be used + to obtain the web identity token. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - key + - name + - namespace + type: object + source: + description: Source is the source of the web identity + token. + enum: + - Secret + - Filesystem + type: string + required: + - source + type: object type: object type: object webIdentity: @@ -170,6 +214,50 @@ spec: description: RoleSessionName is the session name, if you wish to uniquely identify this session. type: string + tokenConfig: + description: TokenConfig is the Web Identity Token config + to assume the role. + properties: + fs: + description: Fs is a reference to a filesystem location + that contains credentials that must be used to obtain + the web identity token. + properties: + path: + description: Path is a filesystem path. + type: string + required: + - path + type: object + secretRef: + description: A SecretRef is a reference to a secret key + that contains the credentials that must be used to obtain + the web identity token. + properties: + key: + description: The key to select. + type: string + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - key + - name + - namespace + type: object + source: + description: Source is the source of the web identity + token. + enum: + - Secret + - Filesystem + type: string + required: + - source + type: object type: object required: - source