Skip to content

Commit

Permalink
Merge pull request crossplane-contrib#1148 from erhancagirici/web-ide…
Browse files Browse the repository at this point in the history
…ntity-auth-tokenfile

add web identity token configuration to ProviderConfig spec
  • Loading branch information
erhancagirici authored Feb 15, 2024
2 parents 1b93401 + 0365316 commit 77871bb
Show file tree
Hide file tree
Showing 7 changed files with 294 additions and 8 deletions.
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

0 comments on commit 77871bb

Please sign in to comment.