istio.io/istio@v0.0.0-20240520182934-d79c90f27776/security/pkg/nodeagent/caclient/providers/citadel/client.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package citadel
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  
    22  	"google.golang.org/grpc"
    23  	"google.golang.org/grpc/credentials"
    24  	"google.golang.org/grpc/metadata"
    25  	"google.golang.org/protobuf/types/known/structpb"
    26  
    27  	pb "istio.io/api/security/v1alpha1"
    28  	istiogrpc "istio.io/istio/pilot/pkg/grpc"
    29  	"istio.io/istio/pkg/log"
    30  	"istio.io/istio/pkg/security"
    31  	"istio.io/istio/security/pkg/nodeagent/caclient"
    32  )
    33  
    34  const (
    35  	bearerTokenPrefix = "Bearer "
    36  )
    37  
    38  var citadelClientLog = log.RegisterScope("citadelclient", "citadel client debugging")
    39  
    40  type CitadelClient struct {
    41  	// It means enable tls connection to Citadel if this is not nil.
    42  	tlsOpts  *TLSOptions
    43  	client   pb.IstioCertificateServiceClient
    44  	conn     *grpc.ClientConn
    45  	provider credentials.PerRPCCredentials
    46  	opts     *security.Options
    47  }
    48  
    49  type TLSOptions struct {
    50  	RootCert string
    51  	Key      string
    52  	Cert     string
    53  }
    54  
    55  // NewCitadelClient create a CA client for Citadel.
    56  func NewCitadelClient(opts *security.Options, tlsOpts *TLSOptions) (*CitadelClient, error) {
    57  	c := &CitadelClient{
    58  		tlsOpts:  tlsOpts,
    59  		opts:     opts,
    60  		provider: caclient.NewDefaultTokenProvider(opts),
    61  	}
    62  
    63  	conn, err := c.buildConnection()
    64  	if err != nil {
    65  		citadelClientLog.Errorf("Failed to connect to endpoint %s: %v", opts.CAEndpoint, err)
    66  		return nil, fmt.Errorf("failed to connect to endpoint %s", opts.CAEndpoint)
    67  	}
    68  	c.conn = conn
    69  	c.client = pb.NewIstioCertificateServiceClient(conn)
    70  	return c, nil
    71  }
    72  
    73  func (c *CitadelClient) Close() {
    74  	if c.conn != nil {
    75  		c.conn.Close()
    76  	}
    77  }
    78  
    79  // CSRSign calls Citadel to sign a CSR.
    80  func (c *CitadelClient) CSRSign(csrPEM []byte, certValidTTLInSec int64) (res []string, err error) {
    81  	crMetaStruct := &structpb.Struct{
    82  		Fields: map[string]*structpb.Value{
    83  			security.CertSigner: {
    84  				Kind: &structpb.Value_StringValue{StringValue: c.opts.CertSigner},
    85  			},
    86  		},
    87  	}
    88  	req := &pb.IstioCertificateRequest{
    89  		Csr:              string(csrPEM),
    90  		ValidityDuration: certValidTTLInSec,
    91  		Metadata:         crMetaStruct,
    92  	}
    93  	// TODO(hzxuzhonghu): notify caclient rebuilding only when root cert is updated.
    94  	// It can happen when the istiod dns certs is resigned after root cert is updated,
    95  	// in this case, the ca grpc client can not automatically connect to istiod after the underlying network connection closed.
    96  	// Becase that the grpc client still use the old tls configuration to reconnect to istiod.
    97  	// So here we need to rebuild the caClient in order to use the new root cert.
    98  	defer func() {
    99  		if err != nil {
   100  			citadelClientLog.Errorf("failed to sign CSR: %v", err)
   101  			if err := c.reconnect(); err != nil {
   102  				citadelClientLog.Errorf("failed reconnect: %v", err)
   103  			}
   104  		}
   105  	}()
   106  
   107  	ctx := metadata.NewOutgoingContext(context.Background(), metadata.Pairs("ClusterID", c.opts.ClusterID))
   108  	resp, err := c.client.CreateCertificate(ctx, req)
   109  	if err != nil {
   110  		return nil, fmt.Errorf("create certificate: %v", err)
   111  	}
   112  
   113  	if len(resp.CertChain) <= 1 {
   114  		return nil, errors.New("invalid empty CertChain")
   115  	}
   116  
   117  	return resp.CertChain, nil
   118  }
   119  
   120  func (c *CitadelClient) getTLSOptions() *istiogrpc.TLSOptions {
   121  	if c.tlsOpts != nil {
   122  		return &istiogrpc.TLSOptions{
   123  			RootCert:      c.tlsOpts.RootCert,
   124  			Key:           c.tlsOpts.Key,
   125  			Cert:          c.tlsOpts.Cert,
   126  			ServerAddress: c.opts.CAEndpoint,
   127  			SAN:           c.opts.CAEndpointSAN,
   128  		}
   129  	}
   130  	return nil
   131  }
   132  
   133  func (c *CitadelClient) buildConnection() (*grpc.ClientConn, error) {
   134  	tlsOpts := c.getTLSOptions()
   135  	opts, err := istiogrpc.ClientOptions(nil, tlsOpts)
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  	opts = append(opts,
   140  		grpc.WithPerRPCCredentials(c.provider),
   141  		security.CARetryInterceptor(),
   142  	)
   143  	conn, err := grpc.Dial(c.opts.CAEndpoint, opts...)
   144  	if err != nil {
   145  		citadelClientLog.Errorf("Failed to connect to endpoint %s: %v", c.opts.CAEndpoint, err)
   146  		return nil, fmt.Errorf("failed to connect to endpoint %s", c.opts.CAEndpoint)
   147  	}
   148  
   149  	return conn, nil
   150  }
   151  
   152  func (c *CitadelClient) reconnect() error {
   153  	if err := c.conn.Close(); err != nil {
   154  		return fmt.Errorf("failed to close connection: %v", err)
   155  	}
   156  
   157  	conn, err := c.buildConnection()
   158  	if err != nil {
   159  		return err
   160  	}
   161  	c.conn = conn
   162  	c.client = pb.NewIstioCertificateServiceClient(conn)
   163  	citadelClientLog.Info("recreated connection")
   164  	return nil
   165  }
   166  
   167  // GetRootCertBundle: Citadel (Istiod) CA doesn't publish any endpoint to retrieve CA certs
   168  func (c *CitadelClient) GetRootCertBundle() ([]string, error) {
   169  	return []string{}, nil
   170  }