security/pkg/nodeagent/caclient/providers/citadel/client.go (212 lines of code) (raw):

// Copyright Istio 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 caclient import ( "context" "crypto/tls" "crypto/x509" "encoding/pem" "errors" "fmt" "os" "strings" "time" ) import ( "go.uber.org/atomic" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/structpb" pb "istio.io/api/security/v1alpha1" "istio.io/pkg/log" ) import ( "github.com/apache/dubbo-go-pixiu/pkg/security" "github.com/apache/dubbo-go-pixiu/security/pkg/nodeagent/caclient" ) const ( bearerTokenPrefix = "Bearer " ) var citadelClientLog = log.RegisterScope("citadelclient", "citadel client debugging", 0) type CitadelClient struct { // It means enable tls connection to Citadel if this is not nil. tlsOpts *TLSOptions client pb.IstioCertificateServiceClient conn *grpc.ClientConn provider *caclient.TokenProvider opts *security.Options usingMtls *atomic.Bool } type TLSOptions struct { RootCert string Key string Cert string } // NewCitadelClient create a CA client for Citadel. func NewCitadelClient(opts *security.Options, tlsOpts *TLSOptions) (*CitadelClient, error) { c := &CitadelClient{ tlsOpts: tlsOpts, opts: opts, provider: caclient.NewCATokenProvider(opts), usingMtls: atomic.NewBool(false), } conn, err := c.buildConnection() if err != nil { citadelClientLog.Errorf("Failed to connect to endpoint %s: %v", opts.CAEndpoint, err) return nil, fmt.Errorf("failed to connect to endpoint %s", opts.CAEndpoint) } c.conn = conn c.client = pb.NewIstioCertificateServiceClient(conn) return c, nil } func (c *CitadelClient) Close() { if c.conn != nil { c.conn.Close() } } // CSRSign calls Citadel to sign a CSR. func (c *CitadelClient) CSRSign(csrPEM []byte, certValidTTLInSec int64) ([]string, error) { crMetaStruct := &structpb.Struct{ Fields: map[string]*structpb.Value{ security.CertSigner: { Kind: &structpb.Value_StringValue{StringValue: c.opts.CertSigner}, }, }, } req := &pb.IstioCertificateRequest{ Csr: string(csrPEM), ValidityDuration: certValidTTLInSec, Metadata: crMetaStruct, } if err := c.reconnectIfNeeded(); err != nil { return nil, err } ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("ClusterID", c.opts.ClusterID)) resp, err := c.client.CreateCertificate(ctx, req) if err != nil { return nil, fmt.Errorf("create certificate: %v", err) } if len(resp.CertChain) <= 1 { return nil, errors.New("invalid empty CertChain") } return resp.CertChain, nil } func (c *CitadelClient) getTLSDialOption() (grpc.DialOption, error) { certPool, err := getRootCertificate(c.tlsOpts.RootCert) if err != nil { return nil, err } config := tls.Config{ GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { var certificate tls.Certificate key, cert := c.tlsOpts.Key, c.tlsOpts.Cert if cert != "" { var isExpired bool isExpired, err = c.isCertExpired(cert) if err != nil { citadelClientLog.Warnf("cannot parse the cert chain, using token instead: %v", err) return &certificate, nil } if isExpired { citadelClientLog.Warnf("cert expired, using token instead") return &certificate, nil } // Load the certificate from disk certificate, err = tls.LoadX509KeyPair(cert, key) if err != nil { return nil, err } c.usingMtls.Store(true) } return &certificate, nil }, RootCAs: certPool, } // For debugging on localhost (with port forward) // TODO: remove once istiod is stable and we have a way to validate JWTs locally if strings.Contains(c.opts.CAEndpoint, "localhost") { config.ServerName = "istiod.dubbo-system.svc" } if c.opts.CAEndpointSAN != "" { config.ServerName = c.opts.CAEndpointSAN } transportCreds := credentials.NewTLS(&config) return grpc.WithTransportCredentials(transportCreds), nil } func getRootCertificate(rootCertFile string) (*x509.CertPool, error) { if rootCertFile == "" { // No explicit certificate - assume the citadel-compatible server uses a public cert certPool, err := x509.SystemCertPool() if err != nil { return nil, err } citadelClientLog.Info("Citadel client using system cert") return certPool, nil } certPool := x509.NewCertPool() rootCert, err := os.ReadFile(rootCertFile) if err != nil { return nil, err } ok := certPool.AppendCertsFromPEM(rootCert) if !ok { return nil, fmt.Errorf("failed to append certificates") } citadelClientLog.Info("Citadel client using custom root cert: ", rootCertFile) return certPool, nil } func (c *CitadelClient) isCertExpired(filepath string) (bool, error) { var err error var certPEMBlock []byte certPEMBlock, err = os.ReadFile(filepath) if err != nil { return true, fmt.Errorf("failed to read the cert, error is %v", err) } var certDERBlock *pem.Block certDERBlock, _ = pem.Decode(certPEMBlock) if certDERBlock == nil { return true, fmt.Errorf("failed to decode certificate") } x509Cert, err := x509.ParseCertificate(certDERBlock.Bytes) if err != nil { return true, fmt.Errorf("failed to parse the cert, err is %v", err) } return x509Cert.NotAfter.Before(time.Now()), nil } func (c *CitadelClient) buildConnection() (*grpc.ClientConn, error) { var opts grpc.DialOption var err error // CA tls disabled if c.tlsOpts == nil { opts = grpc.WithTransportCredentials(insecure.NewCredentials()) } else { opts, err = c.getTLSDialOption() if err != nil { return nil, err } } conn, err := grpc.Dial(c.opts.CAEndpoint, opts, grpc.WithPerRPCCredentials(c.provider), security.CARetryInterceptor()) if err != nil { citadelClientLog.Errorf("Failed to connect to endpoint %s: %v", c.opts.CAEndpoint, err) return nil, fmt.Errorf("failed to connect to endpoint %s", c.opts.CAEndpoint) } return conn, nil } func (c *CitadelClient) reconnectIfNeeded() error { if c.opts.ProvCert == "" || c.usingMtls.Load() { // No need to reconnect, already using mTLS or never will use it return nil } _, err := tls.LoadX509KeyPair(c.tlsOpts.Cert, c.tlsOpts.Key) if err != nil { // Cannot load the certificates yet, don't both reconnecting return nil } if err := c.conn.Close(); err != nil { return fmt.Errorf("failed to close connection") } conn, err := c.buildConnection() if err != nil { return err } c.conn = conn c.client = pb.NewIstioCertificateServiceClient(conn) citadelClientLog.Errorf("recreated connection") return nil } // GetRootCertBundle: Citadel (Istiod) CA doesn't publish any endpoint to retrieve CA certs func (c *CitadelClient) GetRootCertBundle() ([]string, error) { return []string{}, nil }