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 }