// Copyright (c) 2022 Alibaba Group Holding Ltd. // // 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 cert import ( "context" "fmt" "strings" "sync/atomic" "time" "istio.io/istio/pkg/config/host" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "sigs.k8s.io/yaml" ) const ( ConfigmapCertName = "higress-https" ConfigmapCertConfigKey = "cert" DefaultRenewBeforeDays = 30 RenewMaxDays = 90 ) type IssuerName string const ( IssuerTypeAliyunSSL IssuerName = "aliyunssl" IssuerTypeLetsencrypt IssuerName = "letsencrypt" ) // Config is the configuration of automatic https. type Config struct { AutomaticHttps bool `json:"automaticHttps"` FallbackForInvalidSecret bool `json:"fallbackForInvalidSecret"` RenewBeforeDays int `json:"renewBeforeDays"` CredentialConfig []CredentialEntry `json:"credentialConfig"` ACMEIssuer []ACMEIssuerEntry `json:"acmeIssuer"` Version string `json:"version"` } func (c *Config) GetIssuer(issuerName IssuerName) *ACMEIssuerEntry { for _, issuer := range c.ACMEIssuer { if issuer.Name == issuerName { return &issuer } } return nil } func (c *Config) MatchSecretNameByDomain(domain string) string { for _, credential := range c.CredentialConfig { for _, credDomain := range credential.Domains { if host.Name(strings.ToLower(domain)).SubsetOf(host.Name(strings.ToLower(credDomain))) { return credential.TLSSecret } } } return "" } func (c *Config) GetSecretNameByDomain(issuerName IssuerName, domain string) string { for _, credential := range c.CredentialConfig { if credential.TLSIssuer == issuerName { for _, credDomain := range credential.Domains { if host.Name(strings.ToLower(domain)).SubsetOf(host.Name(strings.ToLower(credDomain))) { return credential.TLSSecret } } } } return "" } func ParseTLSSecret(tlsSecret string) (string, string) { secrets := strings.Split(tlsSecret, "/") switch len(secrets) { case 1: return "", tlsSecret case 2: return secrets[0], secrets[1] } return "", "" } func (c *Config) Validate() error { // check acmeIssuer if c.AutomaticHttps { if len(c.ACMEIssuer) == 0 { return fmt.Errorf("no acmeIssuer configuration found when automaticHttps is enable") } for _, issuer := range c.ACMEIssuer { switch issuer.Name { case IssuerTypeLetsencrypt: if issuer.Email == "" { return fmt.Errorf("acmeIssuer %s email is empty", issuer.Name) } if !ValidateEmail(issuer.Email) { return fmt.Errorf("acmeIssuer %s email %s is invalid", issuer.Name, issuer.Email) } default: return fmt.Errorf("acmeIssuer name %s is not supported", issuer.Name) } } } // check credentialConfig for _, credential := range c.CredentialConfig { if len(credential.Domains) == 0 { return fmt.Errorf("credentialConfig domains is empty") } if credential.TLSSecret == "" { return fmt.Errorf("credentialConfig tlsSecret is empty") } else { ns, secret := ParseTLSSecret(credential.TLSSecret) if ns == "" && secret == "" { return fmt.Errorf("credentialConfig tlsSecret %s is not supported", credential.TLSSecret) } } if credential.TLSIssuer == IssuerTypeLetsencrypt { if len(credential.Domains) > 1 { return fmt.Errorf("credentialConfig tlsIssuer %s only support one domain", credential.TLSIssuer) } } if credential.TLSIssuer != IssuerTypeLetsencrypt && len(credential.TLSIssuer) > 0 { return fmt.Errorf("credential tls issuer %s is not supported", credential.TLSIssuer) } } if c.RenewBeforeDays <= 0 { return fmt.Errorf("RenewBeforeDays should be large than zero") } if c.RenewBeforeDays >= RenewMaxDays { return fmt.Errorf("RenewBeforeDays should be less than %d", RenewMaxDays) } return nil } type CredentialEntry struct { Domains []string `json:"domains"` TLSIssuer IssuerName `json:"tlsIssuer,omitempty"` TLSSecret string `json:"tlsSecret,omitempty"` CACertSecret string `json:"cacertSecret,omitempty"` } type ACMEIssuerEntry struct { Name IssuerName `json:"name"` Email string `json:"email"` AK string `json:"ak"` // Only applicable for certain issuers like 'aliyunssl' SK string `json:"sk"` // Only applicable for certain issuers like 'aliyunssl' } type ConfigMgr struct { client kubernetes.Interface config atomic.Value namespace string } func (c *ConfigMgr) SetConfig(config *Config) { c.config.Store(config) } func (c *ConfigMgr) GetConfig() *Config { value := c.config.Load() if value != nil { if config, ok := value.(*Config); ok { return config } } return nil } func (c *ConfigMgr) InitConfig(email string) (*Config, error) { var defaultConfig *Config cm, err := c.GetConfigmap() if err != nil { if errors.IsNotFound(err) { if len(strings.TrimSpace(email)) == 0 { email = getRandEmail() } defaultConfig = newDefaultConfig(email) err2 := c.ApplyConfigmap(defaultConfig) if err2 != nil { return nil, err2 } } return nil, err } else { defaultConfig, err = c.ParseConfigFromConfigmap(cm) if err != nil { return nil, err } } return defaultConfig, nil } func (c *ConfigMgr) ParseConfigFromConfigmap(configmap *v1.ConfigMap) (*Config, error) { if _, ok := configmap.Data[ConfigmapCertConfigKey]; !ok { return nil, fmt.Errorf("no cert key %s in configmap %s", ConfigmapCertConfigKey, configmap.Name) } config := newDefaultConfig("") if err := yaml.Unmarshal([]byte(configmap.Data[ConfigmapCertConfigKey]), config); err != nil { return nil, fmt.Errorf("data:%s, convert to higress config error, error: %+v", configmap.Data[ConfigmapCertConfigKey], err) } // validate config if err := config.Validate(); err != nil { return nil, err } return config, nil } func (c *ConfigMgr) GetConfigFromConfigmap() (*Config, error) { var config *Config cm, err := c.GetConfigmap() if err != nil { return nil, err } else { config, err = c.ParseConfigFromConfigmap(cm) if err != nil { return nil, err } } return config, nil } func (c *ConfigMgr) GetConfigmap() (configmap *v1.ConfigMap, err error) { configmapName := ConfigmapCertName cm, err := c.client.CoreV1().ConfigMaps(c.namespace).Get(context.Background(), configmapName, metav1.GetOptions{}) return cm, err } func (c *ConfigMgr) ApplyConfigmap(config *Config) error { configmapName := ConfigmapCertName cm := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Namespace: c.namespace, Name: configmapName, }, } bytes, err := yaml.Marshal(config) if err != nil { return err } cm.Data = make(map[string]string, 0) cm.Data[ConfigmapCertConfigKey] = string(bytes) _, err = c.client.CoreV1().ConfigMaps(c.namespace).Get(context.Background(), configmapName, metav1.GetOptions{}) if err != nil { if errors.IsNotFound(err) { if _, err = c.client.CoreV1().ConfigMaps(c.namespace).Create(context.Background(), cm, metav1.CreateOptions{}); err != nil { return err } } else { return err } } else { if _, err = c.client.CoreV1().ConfigMaps(c.namespace).Update(context.Background(), cm, metav1.UpdateOptions{}); err != nil { return err } } return nil } func NewConfigMgr(namespace string, client kubernetes.Interface) (*ConfigMgr, error) { configMgr := &ConfigMgr{ client: client, namespace: namespace, } return configMgr, nil } func newDefaultConfig(email string) *Config { defaultIssuer := []ACMEIssuerEntry{ { Name: IssuerTypeLetsencrypt, Email: email, }, } defaultCredentialConfig := make([]CredentialEntry, 0) config := &Config{ AutomaticHttps: true, FallbackForInvalidSecret: false, RenewBeforeDays: DefaultRenewBeforeDays, ACMEIssuer: defaultIssuer, CredentialConfig: defaultCredentialConfig, Version: time.Now().Format("20060102030405"), } return config } func getRandEmail() string { num1 := rangeRandom(100, 100000) num2 := rangeRandom(100, 100000) return fmt.Sprintf("your%d@yours%d.com", num1, num2) }