mirror of
https://github.com/alibaba/higress.git
synced 2026-02-23 04:00:51 +08:00
251 lines
7.9 KiB
Go
251 lines
7.9 KiB
Go
// 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"
|
|
"os"
|
|
"reflect"
|
|
"sync"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
"github.com/mholt/acmez"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zapcore"
|
|
istiomodel "istio.io/istio/pilot/pkg/model"
|
|
"k8s.io/client-go/kubernetes"
|
|
)
|
|
|
|
const (
|
|
EventCertObtained = "cert_obtained"
|
|
)
|
|
|
|
var (
|
|
cfg *certmagic.Config
|
|
)
|
|
|
|
type CertMgr struct {
|
|
cfg *certmagic.Config
|
|
client kubernetes.Interface
|
|
namespace string
|
|
mux sync.RWMutex
|
|
storage certmagic.Storage
|
|
cache *certmagic.Cache
|
|
myACME *certmagic.ACMEIssuer
|
|
ingressSolver acmez.Solver
|
|
configMgr *ConfigMgr
|
|
secretMgr *SecretMgr
|
|
XDSUpdater istiomodel.XDSUpdater
|
|
}
|
|
|
|
func InitCertMgr(opts *Option, clientSet kubernetes.Interface, config *Config, XDSUpdater istiomodel.XDSUpdater, configMgr *ConfigMgr) (*CertMgr, error) {
|
|
CertLog.Infof("certmgr init config: %+v", config)
|
|
// Init certmagic config
|
|
// First make a pointer to a Cache as we need to reference the same Cache in
|
|
// GetConfigForCert below.
|
|
var cache *certmagic.Cache
|
|
var storage certmagic.Storage
|
|
storage, _ = NewConfigmapStorage(opts.Namespace, clientSet)
|
|
renewalWindowRatio := float64(config.RenewBeforeDays) / float64(RenewMaxDays)
|
|
logger := zap.New(zapcore.NewCore(
|
|
zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig()),
|
|
os.Stderr,
|
|
zap.DebugLevel,
|
|
))
|
|
magicConfig := certmagic.Config{
|
|
RenewalWindowRatio: renewalWindowRatio,
|
|
Storage: storage,
|
|
Logger: logger,
|
|
}
|
|
cache = certmagic.NewCache(certmagic.CacheOptions{
|
|
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
|
|
// Here we use New to get a valid Config associated with the same cache.
|
|
// The provided Config is used as a template and will be completed with
|
|
// any defaults that are set in the Default config.
|
|
return cfg, nil
|
|
},
|
|
Logger: logger,
|
|
})
|
|
// init certmagic
|
|
cfg = certmagic.New(cache, magicConfig)
|
|
|
|
// Init certmagic acme
|
|
issuer := config.GetIssuer(IssuerTypeLetsencrypt)
|
|
if issuer == nil {
|
|
// should never happen here
|
|
return nil, fmt.Errorf("there is no Letsencrypt Issuer found in config")
|
|
}
|
|
|
|
myACME := certmagic.NewACMEIssuer(cfg, certmagic.ACMEIssuer{
|
|
//CA: certmagic.LetsEncryptStagingCA,
|
|
CA: certmagic.LetsEncryptProductionCA,
|
|
Email: issuer.Email,
|
|
Agreed: true,
|
|
DisableHTTPChallenge: false,
|
|
DisableTLSALPNChallenge: true,
|
|
})
|
|
// inject http01 solver
|
|
ingressSolver, _ := NewIngressSolver(opts.Namespace, clientSet, myACME)
|
|
myACME.Http01Solver = ingressSolver
|
|
// init issuers
|
|
cfg.Issuers = []certmagic.Issuer{myACME}
|
|
|
|
secretMgr, _ := NewSecretMgr(opts.Namespace, clientSet)
|
|
|
|
certMgr := &CertMgr{
|
|
cfg: cfg,
|
|
client: clientSet,
|
|
namespace: opts.Namespace,
|
|
myACME: myACME,
|
|
ingressSolver: ingressSolver,
|
|
configMgr: configMgr,
|
|
secretMgr: secretMgr,
|
|
cache: cache,
|
|
XDSUpdater: XDSUpdater,
|
|
}
|
|
certMgr.cfg.OnEvent = certMgr.OnEvent
|
|
return certMgr, nil
|
|
}
|
|
func (s *CertMgr) Reconcile(ctx context.Context, oldConfig *Config, newConfig *Config) error {
|
|
CertLog.Infof("cermgr reconcile old config:%+v to new config:%+v", oldConfig, newConfig)
|
|
// sync email
|
|
if oldConfig != nil && newConfig != nil {
|
|
oldIssuer := oldConfig.GetIssuer(IssuerTypeLetsencrypt)
|
|
newIssuer := newConfig.GetIssuer(IssuerTypeLetsencrypt)
|
|
if oldIssuer.Email != newIssuer.Email {
|
|
// TODO before sync email, maybe need to clean up cache and account
|
|
}
|
|
}
|
|
|
|
// sync domains
|
|
newDomains := make([]string, 0)
|
|
newDomainsMap := make(map[string]string, 0)
|
|
removeDomains := make([]string, 0)
|
|
|
|
if newConfig != nil {
|
|
for _, config := range newConfig.CredentialConfig {
|
|
if config.TLSIssuer == IssuerTypeLetsencrypt {
|
|
for _, newDomain := range config.Domains {
|
|
newDomains = append(newDomains, newDomain)
|
|
newDomainsMap[newDomain] = newDomain
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
if oldConfig != nil {
|
|
for _, config := range oldConfig.CredentialConfig {
|
|
if config.TLSIssuer == IssuerTypeLetsencrypt {
|
|
for _, oldDomain := range config.Domains {
|
|
if _, ok := newDomainsMap[oldDomain]; !ok {
|
|
removeDomains = append(removeDomains, oldDomain)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
if newConfig.AutomaticHttps == true {
|
|
newIssuer := newConfig.GetIssuer(IssuerTypeLetsencrypt)
|
|
// clean up unused domains
|
|
s.cleanSync(context.Background(), removeDomains)
|
|
// sync email
|
|
s.myACME.Email = newIssuer.Email
|
|
// sync RenewalWindowRatio
|
|
renewalWindowRatio := float64(newConfig.RenewBeforeDays) / float64(RenewMaxDays)
|
|
s.cfg.RenewalWindowRatio = renewalWindowRatio
|
|
// start cache
|
|
s.cache.Start()
|
|
// sync domains
|
|
s.configMgr.SetConfig(newConfig)
|
|
CertLog.Infof("certMgr start to manageSync domains:+v%", newDomains)
|
|
s.manageSync(context.Background(), newDomains)
|
|
CertLog.Infof("certMgr manageSync domains done")
|
|
} else {
|
|
// stop cache maintainAssets
|
|
s.cache.Stop()
|
|
s.configMgr.SetConfig(newConfig)
|
|
}
|
|
|
|
if oldConfig != nil && newConfig != nil {
|
|
if oldConfig.FallbackForInvalidSecret != newConfig.FallbackForInvalidSecret || !reflect.DeepEqual(oldConfig.CredentialConfig, newConfig.CredentialConfig) {
|
|
CertLog.Infof("ingress need to full push")
|
|
s.XDSUpdater.ConfigUpdate(&istiomodel.PushRequest{
|
|
Full: true,
|
|
Reason: istiomodel.NewReasonStats("higress-https-updated"),
|
|
})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *CertMgr) manageSync(ctx context.Context, domainNames []string) error {
|
|
CertLog.Infof("cert manage sync domains:%v", domainNames)
|
|
return s.cfg.ManageSync(ctx, domainNames)
|
|
}
|
|
|
|
func (s *CertMgr) cleanSync(ctx context.Context, domainNames []string) error {
|
|
//TODO implement clean up domains
|
|
CertLog.Infof("cert clean sync domains:%v", domainNames)
|
|
return nil
|
|
}
|
|
|
|
func (s *CertMgr) OnEvent(ctx context.Context, event string, data map[string]any) error {
|
|
CertLog.Infof("certmgr receive event:% data:%+v", event, data)
|
|
/**
|
|
event: cert_obtained
|
|
cfg.emit(ctx, "cert_obtained", map[string]any{
|
|
"renewal": true,
|
|
"remaining": timeLeft,
|
|
"identifier": name,
|
|
"issuer": issuerKey,
|
|
"storage_path": StorageKeys.CertsSitePrefix(issuerKey, certKey),
|
|
"private_key_path": StorageKeys.SitePrivateKey(issuerKey, certKey),
|
|
"certificate_path": StorageKeys.SiteCert(issuerKey, certKey),
|
|
"metadata_path": StorageKeys.SiteMeta(issuerKey, certKey),
|
|
})
|
|
*/
|
|
if event == EventCertObtained {
|
|
// obtain certificate and update secret
|
|
domain := data["identifier"].(string)
|
|
isRenew := data["renewal"].(bool)
|
|
privateKeyPath := data["private_key_path"].(string)
|
|
certificatePath := data["certificate_path"].(string)
|
|
privateKey, err := s.cfg.Storage.Load(context.Background(), privateKeyPath)
|
|
certificate, err := s.cfg.Storage.Load(context.Background(), certificatePath)
|
|
certChain, err := parseCertsFromPEMBundle(certificate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
notAfterTime := notAfter(certChain[0])
|
|
notBeforeTime := notBefore(certChain[0])
|
|
secretName := s.configMgr.GetConfig().GetSecretNameByDomain(IssuerTypeLetsencrypt, domain)
|
|
if len(secretName) == 0 {
|
|
CertLog.Errorf("can not find secret name for domain % in config", domain)
|
|
return nil
|
|
}
|
|
err2 := s.secretMgr.Update(domain, secretName, privateKey, certificate, notBeforeTime, notAfterTime, isRenew)
|
|
if err2 != nil {
|
|
CertLog.Errorf("update secretName %s for domain %s error: %v", secretName, domain, err2)
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|