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

add web identity token configuration to ProviderConfig spec #1148

Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions apis/v1beta1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions apis/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 36 additions & 1 deletion cmd/provider/opensearchserverless/zz_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ import (
"github.com/upbound/provider-aws/internal/features"
)

const (
webhookTLSCertDirEnvVar = "WEBHOOK_TLS_CERT_DIR"
tlsServerCertDirEnvVar = "TLS_SERVER_CERTS_DIR"
certsDirEnvVar = "CERTS_DIR"
tlsServerCertDir = "/tls/server"
)

func deprecationAction(flagName string) kingpin.Action {
return func(c *kingpin.ParseContext) error {
_, err := fmt.Fprintf(os.Stderr, "warning: Command-line flag %q is deprecated and no longer used. It will be removed in a future release. Please remove it from all of your configurations (ControllerConfigs, etc.).\n", flagName)
Expand All @@ -60,7 +67,13 @@ func main() {
essTLSCertsPath = app.Flag("ess-tls-cert-dir", "Path of ESS TLS certificates.").Envar("ESS_TLS_CERTS_DIR").String()
enableManagementPolicies = app.Flag("enable-management-policies", "Enable support for Management Policies.").Default("true").Envar("ENABLE_MANAGEMENT_POLICIES").Bool()

certsDir = app.Flag("certs-dir", "The directory that contains the server key and certificate.").Default("/tls/server").Envar("CERTS_DIR").String()
certsDirSet = false
// we record whether the command-line option "--certs-dir" was supplied
// in the registered PreAction for the flag.
certsDir = app.Flag("certs-dir", "The directory that contains the server key and certificate.").Default(tlsServerCertDir).Envar(certsDirEnvVar).PreAction(func(_ *kingpin.ParseContext) error {
certsDirSet = true
return nil
}).String()

// now deprecated command-line arguments with the Terraform SDK-based upjet architecture
_ = app.Flag("provider-ttl", "[DEPRECATED: This option is no longer used and it will be removed in a future release.] TTL for the native plugin processes before they are replaced. Changing the default may increase memory consumption.").Hidden().Action(deprecationAction("provider-ttl")).Int()
Expand Down Expand Up @@ -89,6 +102,28 @@ func main() {
cfg, err := ctrl.GetConfig()
kingpin.FatalIfError(err, "Cannot get API server rest config")

// Get the TLS certs directory from the environment variables set by
// Crossplane if they're available.
// In older XP versions we used WEBHOOK_TLS_CERT_DIR, in newer versions
// we use TLS_SERVER_CERTS_DIR. If an explicit certs dir is not supplied
// via the command-line options, then these environment variables are used
// instead.
if !certsDirSet {
// backwards-compatibility concerns
xpCertsDir := os.Getenv(certsDirEnvVar)
if xpCertsDir == "" {
xpCertsDir = os.Getenv(tlsServerCertDirEnvVar)
}
if xpCertsDir == "" {
xpCertsDir = os.Getenv(webhookTLSCertDirEnvVar)
}
// we probably don't need this condition but just to be on the
// safe side, if we are missing any kingpin machinery details...
if xpCertsDir != "" {
*certsDir = xpCertsDir
}
}

mgr, err := ctrl.NewManager(ratelimiter.LimitRESTConfig(cfg, *maxReconcileRate), ctrl.Options{
LeaderElection: *leaderElection,
LeaderElectionID: "crossplane-leader-election-provider-aws-opensearchserverless",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
19 changes: 17 additions & 2 deletions internal/clients/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down
88 changes: 83 additions & 5 deletions internal/clients/provider_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
Loading
Loading