diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials/oidc_credential.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials/oidc_credential.go new file mode 100644 index 000000000000..2d23de3cd98b --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials/oidc_credential.go @@ -0,0 +1,37 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package credentials + +// OIDCCredential is a kind of credentials +type OIDCCredential struct { + RoleArn string + OIDCProviderArn string + OIDCTokenFilePath string + RoleSessionName string + RoleSessionExpiration int +} + +// NewOIDCRoleArnCredential returns OIDCCredential +func NewOIDCRoleArnCredential(roleArn, OIDCProviderArn, OIDCTokenFilePath, RoleSessionName string, RoleSessionExpiration int) *OIDCCredential { + return &OIDCCredential{ + RoleArn: roleArn, + OIDCProviderArn: OIDCProviderArn, + OIDCTokenFilePath: OIDCTokenFilePath, + RoleSessionName: RoleSessionName, + RoleSessionExpiration: RoleSessionExpiration, + } +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go index 424db3b3cd3d..3f732a36f7f5 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signer.go @@ -61,6 +61,10 @@ func NewSignerWithCredential(credential Credential, commonApi func(request *requ { signer, err = signers.NewEcsRamRoleSigner(instance, commonApi) } + case *credentials.OIDCCredential: + { + signer, err = signers.NewOIDCSigner(instance) + } case *credentials.BaseCredential: // deprecated user interface { signer, err = signers.NewAccessKeySigner(instance.ToAccessKeyCredential()) diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers/signer_oidc.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers/signer_oidc.go new file mode 100644 index 000000000000..ccb1a2b41eb2 --- /dev/null +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/signers/signer_oidc.go @@ -0,0 +1,247 @@ +/* +Copyright 2018 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package signers + +import ( + "encoding/json" + "fmt" + "github.com/google/uuid" + "github.com/jmespath/go-jmespath" + "io/ioutil" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/auth/credentials" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/errors" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/requests" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/responses" + "k8s.io/autoscaler/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/utils" + "net/http" + "os" + "strconv" + "strings" + "time" +) + +const ( + defaultOIDCDurationSeconds = 3600 +) + +// OIDCSigner is kind of signer +type OIDCSigner struct { + *credentialUpdater + roleSessionName string + sessionCredential *SessionCredential + credential *credentials.OIDCCredential + commonApi func(request *requests.CommonRequest, signer interface{}) (response *responses.CommonResponse, err error) +} + +// NewOIDCSigner returns OIDCSigner +func NewOIDCSigner(credential *credentials.OIDCCredential) (signer *OIDCSigner, err error) { + signer = &OIDCSigner{ + credential: credential, + } + + signer.credentialUpdater = &credentialUpdater{ + credentialExpiration: credential.RoleSessionExpiration, + buildRequestMethod: signer.buildCommonRequest, + responseCallBack: signer.refreshCredential, + refreshApi: signer.refreshApi, + } + + if len(credential.RoleSessionName) > 0 { + signer.roleSessionName = credential.RoleSessionName + } else { + signer.roleSessionName = "aliyun-go-sdk-" + strconv.FormatInt(time.Now().UnixNano()/1000, 10) + } + if credential.RoleSessionExpiration > 0 { + if credential.RoleSessionExpiration >= 900 && credential.RoleSessionExpiration <= 3600 { + signer.credentialExpiration = credential.RoleSessionExpiration + } else { + err = errors.NewClientError(errors.InvalidParamErrorCode, "Assume Role session duration should be in the range of 15min - 1Hr", nil) + } + } else { + signer.credentialExpiration = defaultOIDCDurationSeconds + } + return +} + +// GetName returns "HMAC-SHA1" +func (*OIDCSigner) GetName() string { + return "HMAC-SHA1" +} + +// GetType returns "" +func (*OIDCSigner) GetType() string { + return "" +} + +// GetVersion returns "1.0" +func (*OIDCSigner) GetVersion() string { + return "1.0" +} + +// GetAccessKeyId returns accessKeyId +func (signer *OIDCSigner) GetAccessKeyId() (accessKeyId string, err error) { + if signer.sessionCredential == nil || signer.needUpdateCredential() { + err = signer.updateCredential() + } + if err != nil && (signer.sessionCredential == nil || len(signer.sessionCredential.AccessKeyId) <= 0) { + return "", err + } + return signer.sessionCredential.AccessKeyId, nil +} + +// GetExtraParam returns params +func (signer *OIDCSigner) GetExtraParam() map[string]string { + if signer.sessionCredential == nil || signer.needUpdateCredential() { + signer.updateCredential() + } + if signer.sessionCredential == nil || len(signer.sessionCredential.StsToken) <= 0 { + return make(map[string]string) + } + return map[string]string{"SecurityToken": signer.sessionCredential.StsToken} +} + +// Sign create signer +func (signer *OIDCSigner) Sign(stringToSign, secretSuffix string) string { + secret := signer.sessionCredential.AccessKeySecret + secretSuffix + return ShaHmac1(stringToSign, secret) +} + +func (signer *OIDCSigner) buildCommonRequest() (request *requests.CommonRequest, err error) { + request = requests.NewCommonRequest() + request.Domain = "sts.aliyuncs.com" + request.Scheme = requests.HTTPS + request.Method = "POST" + request.QueryParams["Timestamp"] = utils.GetTimeInFormatISO8601() + request.QueryParams["Action"] = "AssumeRoleWithOIDC" + request.QueryParams["Format"] = "JSON" + request.FormParams["RoleArn"] = signer.credential.RoleArn + request.FormParams["OIDCProviderArn"] = signer.credential.OIDCProviderArn + request.FormParams["OIDCToken"] = signer.getOIDCToken(signer.credential.OIDCTokenFilePath) + request.QueryParams["RoleSessionName"] = signer.credential.RoleSessionName + request.QueryParams["Version"] = "2015-04-01" + request.QueryParams["SignatureNonce"] = uuid.New().String() + request.Headers["Host"] = request.Domain + request.Headers["Accept-Encoding"] = "identity" + request.Headers["content-type"] = "application/x-www-form-urlencoded" + return +} + +func (signer *OIDCSigner) getOIDCToken(OIDCTokenFilePath string) string { + tokenPath := OIDCTokenFilePath + _, err := os.Stat(tokenPath) + if os.IsNotExist(err) { + tokenPath = os.Getenv("ALIBABA_CLOUD_OIDC_TOKEN_FILE") + if tokenPath == "" { + return "" + } + } + + token, err := ioutil.ReadFile(tokenPath) + if err != nil { + return "" + } + return string(token) +} + +func (signer *OIDCSigner) refreshApi(request *requests.CommonRequest) (response *responses.CommonResponse, err error) { + requestUrl := request.BuildUrl() + var urlEncoded string + if request.FormParams != nil { + urlEncoded = utils.GetUrlFormedMap(request.FormParams) + } + + httpRequest, err := http.NewRequest(request.Method, requestUrl, strings.NewReader(urlEncoded)) + if err != nil { + fmt.Println("refresh RRSA token err", err) + return + } + + httpRequest.Proto = "HTTP/1.1" + httpRequest.Host = request.Domain + for key, value := range request.Headers { + if value != "" { + httpRequest.Header[key] = []string{value} + } + } + + httpClient := &http.Client{} + httpResponse, err := httpClient.Do(httpRequest) + if err != nil { + fmt.Println("refresh RRSA token err", err) + return + } + + response = responses.NewCommonResponse() + err = responses.Unmarshal(response, httpResponse, "") + return +} + +func (signer *OIDCSigner) refreshCredential(response *responses.CommonResponse) (err error) { + if response.GetHttpStatus() != http.StatusOK { + message := "refresh session token failed" + err = errors.NewServerError(response.GetHttpStatus(), response.GetHttpContentString(), message) + return + } + var data interface{} + err = json.Unmarshal(response.GetHttpContentBytes(), &data) + if err != nil { + fmt.Println("refresh RRSA token err, json.Unmarshal fail", err) + return + } + accessKeyId, err := jmespath.Search("Credentials.AccessKeyId", data) + if err != nil { + fmt.Println("refresh RRSA token err, fail to get AccessKeyId", err) + return + } + accessKeySecret, err := jmespath.Search("Credentials.AccessKeySecret", data) + if err != nil { + fmt.Println("refresh RRSA token err, fail to get AccessKeySecret", err) + return + } + securityToken, err := jmespath.Search("Credentials.SecurityToken", data) + if err != nil { + fmt.Println("refresh RRSA token err, fail to get SecurityToken", err) + return + } + expiration, err := jmespath.Search("Credentials.Expiration", data) + if err != nil { + fmt.Println("refresh RRSA token err, fail to get Expiration", err) + return + } + + if accessKeyId == nil || accessKeySecret == nil || securityToken == nil { + return + } + + expirationTime, err := time.Parse("2006-01-02T15:04:05Z", expiration.(string)) + signer.credentialExpiration = int(expirationTime.Unix() - time.Now().Unix()) + signer.sessionCredential = &SessionCredential{ + AccessKeyId: accessKeyId.(string), + AccessKeySecret: accessKeySecret.(string), + StsToken: securityToken.(string), + } + + return +} + +// GetSessionCredential returns SessionCredential +func (signer *OIDCSigner) GetSessionCredential() *SessionCredential { + return signer.sessionCredential +} + +// Shutdown doesn't implement +func (signer *OIDCSigner) Shutdown() {} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go index 9ba0123d0475..9870f1210587 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/sdk/client.go @@ -150,6 +150,18 @@ func (client *Client) InitWithEcsRamRole(regionId, roleName string) (err error) return client.InitWithOptions(regionId, config, credential) } +// InitWithRRSA need regionId,roleARN,oidcProviderARN,oidcTokenFilePath and roleSessionName +func (client *Client) InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (err error) { + config := client.InitClientConfig() + credential := &credentials.OIDCCredential{ + RoleArn: roleARN, + OIDCProviderArn: oidcProviderARN, + OIDCTokenFilePath: oidcTokenFilePath, + RoleSessionName: roleSessionName, + } + return client.InitWithOptions(regionId, config, credential) +} + // InitClientConfig init client config func (client *Client) InitClientConfig() (config *Config) { if client.config != nil { @@ -395,6 +407,13 @@ func NewClientWithEcsRamRole(regionId string, roleName string) (client *Client, return } +// NewClientWithRRSA create client with RRSA on ECS +func NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (client *Client, err error) { + client = &Client{} + err = client.InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + return +} + // NewClientWithRsaKeyPair create client with key-pair func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, sessionExpiration int) (client *Client, err error) { client = &Client{} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go index 3d43f1ff45df..170830bfe22f 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ecs/client.go @@ -80,3 +80,10 @@ func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, se err = client.InitWithRsaKeyPair(regionId, publicKeyId, privateKey, sessionExpiration) return } + +// NewClientWithRRSA is a shortcut to create sdk client with RRSA +func NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (client *Client, err error) { + client = &Client{} + err = client.InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + return +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go index cf19bb4bfdce..3be994bffe65 100755 --- a/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alibaba-cloud-sdk-go/services/ess/client.go @@ -80,3 +80,10 @@ func NewClientWithRsaKeyPair(regionId string, publicKeyId, privateKey string, se err = client.InitWithRsaKeyPair(regionId, publicKeyId, privateKey, sessionExpiration) return } + +// NewClientWithRRSA is a shortcut to create sdk client with RRSA +func NewClientWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName string) (client *Client, err error) { + client = &Client{} + err = client.InitWithRRSA(regionId, roleARN, oidcProviderARN, oidcTokenFilePath, roleSessionName) + return +} diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go index 8ccadf572478..3510ce3ab77b 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_auto_scaling.go @@ -52,7 +52,7 @@ func newAutoScalingWrapper(cfg *cloudConfig) (*autoScalingWrapper, error) { asw := &autoScalingWrapper{ cfg: cfg, } - if cfg.STSEnabled == true { + if cfg.STSEnabled || cfg.RRSAEnabled { go func(asw *autoScalingWrapper, cfg *cloudConfig) { timer := time.NewTicker(refreshClientInterval) defer timer.Stop() @@ -76,7 +76,7 @@ func newAutoScalingWrapper(cfg *cloudConfig) (*autoScalingWrapper, error) { func getEssClient(cfg *cloudConfig) (client *ess.Client, err error) { region := cfg.getRegion() - if cfg.STSEnabled == true { + if cfg.STSEnabled { auth, err := cfg.getSTSToken() if err != nil { klog.Errorf("Failed to get sts token from metadata,Because of %s", err.Error()) @@ -86,6 +86,11 @@ func getEssClient(cfg *cloudConfig) (client *ess.Client, err error) { if err != nil { klog.Errorf("Failed to create client with sts in metadata because of %s", err.Error()) } + } else if cfg.RRSAEnabled { + client, err = ess.NewClientWithRRSA(region, cfg.RoleARN, cfg.OIDCProviderARN, cfg.OIDCTokenFilePath, cfg.RoleSessionName) + if err != nil { + klog.Errorf("Failed to create ess client with RRSA, because of %s", err.Error()) + } } else { client, err = ess.NewClientWithAccessKey(region, cfg.AccessKeyID, cfg.AccessKeySecret) if err != nil { diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go index 36d518dac520..c547cc845482 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_cloud_config.go @@ -23,16 +23,25 @@ import ( ) const ( - accessKeyId = "ACCESS_KEY_ID" - accessKeySecret = "ACCESS_KEY_SECRET" - regionId = "REGION_ID" + accessKeyId = "ACCESS_KEY_ID" + accessKeySecret = "ACCESS_KEY_SECRET" + oidcProviderARN = "ALICLOUD_OIDC_PROVIDER_ARN" + oidcTokenFilePath = "ALICLOUD_OIDC_TOKEN_FILE_PATH" + roleARN = "ALICLOUD_ROLE_ARN" + roleSessionName = "ALICLOUD_SESSION_NAME" + regionId = "REGION_ID" ) type cloudConfig struct { - RegionId string - AccessKeyID string - AccessKeySecret string - STSEnabled bool + RegionId string + AccessKeyID string + AccessKeySecret string + OIDCProviderARN string + OIDCTokenFilePath string + RoleARN string + RoleSessionName string + RRSAEnabled bool + STSEnabled bool } func (cc *cloudConfig) isValid() bool { @@ -48,18 +57,40 @@ func (cc *cloudConfig) isValid() bool { cc.RegionId = os.Getenv(regionId) } - if cc.RegionId == "" || cc.AccessKeyID == "" || cc.AccessKeySecret == "" { + if cc.OIDCProviderARN == "" { + cc.OIDCProviderARN = os.Getenv(oidcProviderARN) + } + + if cc.OIDCTokenFilePath == "" { + cc.OIDCTokenFilePath = os.Getenv(oidcTokenFilePath) + } + + if cc.RoleARN == "" { + cc.RoleARN = os.Getenv(roleARN) + } + + if cc.RoleSessionName == "" { + cc.RoleSessionName = os.Getenv(roleSessionName) + } + + if cc.RegionId != "" && cc.AccessKeyID != "" && cc.AccessKeySecret != "" { + klog.V(2).Info("Using AccessKey authentication") + return true + } else if cc.RegionId != "" && cc.OIDCProviderARN != "" && cc.OIDCTokenFilePath != "" && cc.RoleARN != "" && cc.RoleSessionName != "" { + klog.V(2).Info("Using RRSA authentication") + cc.RRSAEnabled = true + return true + } else { klog.V(5).Infof("Failed to get AccessKeyId:%s,AccessKeySecret:%s,RegionId:%s from cloudConfig and Env\n", cc.AccessKeyID, cc.AccessKeySecret, cc.RegionId) + klog.V(5).Infof("Failed to get OIDCProviderARN:%s,OIDCTokenFilePath:%s,RoleARN:%s,RoleSessionName:%s,RegionId:%s from cloudConfig and Env\n", cc.OIDCProviderARN, cc.OIDCTokenFilePath, cc.RoleARN, cc.RoleSessionName, cc.RegionId) klog.V(5).Infof("Try to use sts token in metadata instead.\n") if cc.validateSTSToken() == true && cc.getRegion() != "" { //if CA is working on ECS with valid role name, use sts token instead. cc.STSEnabled = true return true } - } else { - cc.STSEnabled = false - return true } + return false } diff --git a/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go b/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go index 3f09394d78b9..e3c0e7afc3b5 100644 --- a/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go +++ b/cluster-autoscaler/cloudprovider/alicloud/alicloud_instance_types.go @@ -107,7 +107,7 @@ func newInstanceWrapper(cfg *cloudConfig) (*instanceWrapper, error) { return nil, fmt.Errorf("your cloud config is not valid") } iw := &instanceWrapper{} - if cfg.STSEnabled == true { + if cfg.STSEnabled || cfg.RRSAEnabled { go func(iw *instanceWrapper, cfg *cloudConfig) { timer := time.NewTicker(refreshClientInterval) defer timer.Stop() @@ -141,6 +141,11 @@ func getEcsClient(cfg *cloudConfig) (client *ecs.Client, err error) { if err != nil { klog.Errorf("failed to create client with sts in metadata,because of %s", err.Error()) } + } else if cfg.RRSAEnabled { + client, err = ecs.NewClientWithRRSA(region, cfg.RoleARN, cfg.OIDCProviderARN, cfg.OIDCTokenFilePath, cfg.RoleSessionName) + if err != nil { + klog.Errorf("Failed to create ess client with RRSA, because of %s", err.Error()) + } } else { client, err = ecs.NewClientWithAccessKey(region, cfg.AccessKeyID, cfg.AccessKeySecret) if err != nil {