github.com/cilium/cilium@v1.16.2/pkg/kvstore/etcd_debug.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package kvstore 5 6 import ( 7 "bytes" 8 "cmp" 9 "context" 10 "crypto/tls" 11 "crypto/x509" 12 "encoding/pem" 13 "errors" 14 "fmt" 15 "io" 16 "net" 17 "net/url" 18 "os" 19 "regexp" 20 "strings" 21 22 client "go.etcd.io/etcd/client/v3" 23 "go.uber.org/zap" 24 "google.golang.org/grpc" 25 "sigs.k8s.io/yaml" 26 27 "github.com/cilium/cilium/pkg/time" 28 ) 29 30 var etcdVersionRegexp = regexp.MustCompile(`"etcdserver":"(?P<version>.*?)"`) 31 32 // EtcdDbgDialer enables to override the LookupIP and DialContext functions, 33 // e.g., to support service name to IP address resolution when CoreDNS is not 34 // the configured DNS server --- for pods running in the host network namespace. 35 type EtcdDbgDialer interface { 36 LookupIP(ctx context.Context, hostname string) ([]net.IP, error) 37 DialContext(ctx context.Context, addr string) (net.Conn, error) 38 } 39 40 // DefaultEtcdDbgDialer provides a default implementation of the EtcdDbgDialer interface. 41 type DefaultEtcdDbgDialer struct{} 42 43 func (DefaultEtcdDbgDialer) LookupIP(ctx context.Context, hostname string) ([]net.IP, error) { 44 return net.DefaultResolver.LookupIP(ctx, "ip", hostname) 45 } 46 47 func (DefaultEtcdDbgDialer) DialContext(ctx context.Context, addr string) (net.Conn, error) { 48 return (&net.Dialer{}).DialContext(ctx, "tcp", addr) 49 } 50 51 // EtcdDbg performs a set of sanity checks concerning the connection to the given 52 // etcd cluster, and outputs the result in a user-friendly format. 53 func EtcdDbg(ctx context.Context, cfgfile string, dialer EtcdDbgDialer, w io.Writer) { 54 iw := newIndentedWriter(w, 0) 55 56 iw.Println("đ Configuration path: %s", cfgfile) 57 cfg, err := newConfig(cfgfile) 58 if err != nil { 59 iw.Println("â Cannot parse etcd configuration: %s", err) 60 return 61 } 62 63 iw.NewLine() 64 if len(cfg.Endpoints) == 0 { 65 iw.Println("â No available endpoints") 66 } else { 67 iw.Println("đ Endpoints:") 68 for _, ep := range cfg.Endpoints { 69 iiw := iw.WithExtraIndent(3) 70 iiw.Println("- %s", ep) 71 etcdDbgEndpoint(ctx, ep, cfg.TLS.Clone(), dialer, iiw.WithExtraIndent(2)) 72 } 73 } 74 75 iw.NewLine() 76 iw.Println("đ Digital certificates:") 77 etcdDbgCerts(cfgfile, cfg, iw.WithExtraIndent(3)) 78 79 iw.NewLine() 80 iw.Println("âī¸ Etcd client:") 81 iiw := iw.WithExtraIndent(3) 82 cfg.Context = ctx 83 cfg.Logger = zap.NewNop() 84 cfg.DialOptions = append(cfg.DialOptions, grpc.WithBlock(), grpc.WithContextDialer(dialer.DialContext)) 85 cfg.DialTimeout = 1 * time.Second // The client hangs in case the connection fails, hence set a short timeout. 86 87 cl, err := client.New(*cfg) 88 if err != nil { 89 iiw.Println("â Failed to establish connection: %s", err) 90 return 91 } 92 defer cl.Close() 93 94 // Try to retrieve the heartbeat key, as a basic authorization check. 95 // It doesn't really matter whether the heartbeat key exists or not. 96 out, err := cl.Get(ctx, HeartbeatPath) 97 if err != nil { 98 iiw.Println("â Failed to retrieve key from etcd: %s", err) 99 return 100 } 101 102 iiw.Println("â Etcd connection successfully established") 103 if out.Header != nil { 104 iiw.Println("âšī¸ Etcd cluster ID: %x", out.Header.GetClusterId()) 105 } 106 } 107 108 func etcdDbgEndpoint(ctx context.Context, ep string, tlscfg *tls.Config, dialer EtcdDbgDialer, iw *indentedWriter) { 109 u, err := url.Parse(ep) 110 if err != nil { 111 iw.Println("â Cannot parse endpoint: %s", err) 112 return 113 } 114 115 // Hostname resolution 116 hostname := u.Hostname() 117 if net.ParseIP(hostname) == nil { 118 ips, err := dialer.LookupIP(ctx, hostname) 119 if err != nil { 120 iw.Println("â Cannot resolve hostname: %s", err) 121 } else { 122 iw.Println("â Hostname resolved to: %s", etcdDbgOutputIPs(ips)) 123 } 124 } 125 126 // TCP Connection 127 conn, err := dialer.DialContext(ctx, u.Host) 128 if err != nil { 129 iw.Println("â Cannot establish TCP connection to %s: %s", u.Host, err) 130 return 131 } 132 133 iw.Println("â TCP connection successfully established to %s", conn.RemoteAddr()) 134 if u.Scheme != "https" { 135 conn.Close() 136 return 137 } 138 139 // TLS Connection 140 if tlscfg.ServerName == "" { 141 tlscfg.ServerName = hostname 142 } 143 144 // We use GetClientCertificate rather than Certificates to return an error 145 // in case the certificate does not match any of the requested CAs. One 146 // limitation, though, is that the match appears to be performed based on 147 // the distinguished name only, and it doesn't fail if two CAs have the same 148 // DN (which is typically the case with the default CA generated by Cilium). 149 var acceptableCAs [][]byte 150 tlscfg.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { 151 for _, chain := range tlscfg.Certificates { 152 if err := cri.SupportsCertificate(&chain); err == nil { 153 return &chain, nil 154 } 155 } 156 157 acceptableCAs = cri.AcceptableCAs 158 return nil, fmt.Errorf("client certificate is not signed by any acceptable CA") 159 } 160 161 tconn := tls.Client(conn, tlscfg) 162 defer tconn.Close() 163 164 err = tconn.HandshakeContext(ctx) 165 if err != nil { 166 iw.Println("â Cannot establish TLS connection to %s: %s", u.Host, err) 167 if len(acceptableCAs) > 0 { 168 // The output is suboptimal being DER-encoded, but there doesn't 169 // seem to be any easy way to parse it (the utility used by 170 // ParseCertificate is not exported). Better than nothing though. 171 var buf bytes.Buffer 172 for i, ca := range acceptableCAs { 173 if i != 0 { 174 buf.WriteString(", ") 175 } 176 buf.WriteRune('"') 177 buf.WriteString(string(ca)) 178 buf.WriteRune('"') 179 } 180 181 iw.Println("âšī¸ Acceptable CAs: %s", buf.String()) 182 } 183 return 184 } 185 186 iw.Println("â TLS connection successfully established to %s", tconn.RemoteAddr()) 187 iw.Println("âšī¸ Negotiated TLS version: %s, ciphersuite %s", 188 tls.VersionName(tconn.ConnectionState().Version), 189 tls.CipherSuiteName(tconn.ConnectionState().CipherSuite)) 190 191 // With TLS 1.3, the server doesn't acknowledge whether client authentication 192 // succeeded, and a possible error is returned only when reading some data. 193 // Hence, let's trigger a request, so that we see if it failed. 194 tconn.SetDeadline(time.Now().Add(1 * time.Second)) 195 data := fmt.Sprintf("GET /version HTTP/1.1\r\nHost: %s\r\n\r\n", u.Host) 196 _, err = tconn.Write([]byte(data)) 197 if err != nil { 198 iw.Println("â Failed to perform a GET /version request: %s", err) 199 return 200 } 201 202 buf := make([]byte, 1000) 203 _, err = tconn.Read(buf) 204 if err != nil { 205 opErr := &net.OpError{} 206 if errors.As(err, &opErr) && opErr.Op == "remote error" { 207 iw.Println("â TLS client authentication failed: %s", err) 208 } else { 209 iw.Println("â Failed to retrieve GET /version answer: %s", err) 210 } 211 return 212 } 213 214 matches := etcdVersionRegexp.FindAllStringSubmatch(string(buf), 1) 215 if len(matches) != 1 { 216 iw.Println("â ī¸ Could not retrieve etcd server version") 217 return 218 } 219 220 iw.Println("âšī¸ Etcd server version: %s", matches[0][etcdVersionRegexp.SubexpIndex("version")]) 221 } 222 223 func etcdDbgCerts(cfgfile string, cfg *client.Config, iw *indentedWriter) { 224 if cfg.TLS.RootCAs == nil { 225 iw.Println("â ī¸ Root CA unset: using system pool") 226 } else { 227 // Retrieve the RootCA path from the configuration, as it appears 228 // that we cannot introspect cfg.TLS.RootCAs. 229 certs, err := etcdDbgRetrieveRootCAFile(cfgfile) 230 if err != nil { 231 iw.Println("â Failed to retrieve Root CA path: %s", err) 232 } else { 233 iw.Println("â TLS Root CA certificates:") 234 for _, cert := range certs { 235 parsed, err := x509.ParseCertificate(cert) 236 if err != nil { 237 iw.Println("â Failed to parse certificate: %s", err) 238 continue 239 } 240 241 etcdDbgOutputCert(parsed, iw.WithExtraIndent(3)) 242 } 243 } 244 } 245 246 if len(cfg.TLS.Certificates) == 0 { 247 iw.Println("â ī¸ No available TLS client certificates") 248 } else { 249 iw.Println("â TLS client certificates:") 250 for _, cert := range cfg.TLS.Certificates { 251 if len(cert.Certificate) == 0 { 252 iw.Println("â The certificate looks invalid") 253 continue 254 } 255 256 leaf, err := x509.ParseCertificate(cert.Certificate[0]) 257 if err != nil { 258 iw.Println("â Failed to parse certificate: %s", err) 259 continue 260 } 261 262 iiw := iw.WithExtraIndent(3) 263 etcdDbgOutputCert(leaf, iiw) 264 iiw = iiw.WithExtraIndent(2) 265 266 // Print intermediate certificates, if any. 267 intermediates := x509.NewCertPool() 268 for _, cert := range cert.Certificate[1:] { 269 iiw.Println("Intermediates:") 270 271 intermediate, err := x509.ParseCertificate(cert) 272 if err != nil { 273 iw.Println("â Failed to parse intermediate certificate: %s", err) 274 continue 275 } 276 277 etcdDbgOutputCert(intermediate, iiw) 278 intermediates.AddCert(intermediate) 279 } 280 281 // Attempt to verify whether the given certificate can be validated 282 // using the configured root CAs. Although a failure is not necessarily 283 // an error, as the remote etcd server may be configured with a different 284 // root CA, it still signals a misconfiguration in most cases. 285 opts := x509.VerifyOptions{ 286 Roots: cfg.TLS.RootCAs, 287 Intermediates: intermediates, 288 } 289 290 _, err = leaf.Verify(opts) 291 if err != nil { 292 iiw.Println("â ī¸ Cannot verify certificate with the configured root CAs") 293 } 294 } 295 } 296 297 if cfg.Username != "" { 298 passwd := "unset" 299 if cfg.Password != "" { 300 passwd = "set" 301 } 302 303 iw.Println("â Username set to %s, password is %s", cfg.Username, passwd) 304 } 305 } 306 307 func etcdDbgOutputIPs(ips []net.IP) string { 308 var buf bytes.Buffer 309 for i, ip := range ips { 310 if i > 0 { 311 buf.WriteString(", ") 312 } 313 314 if i == 4 { 315 buf.WriteString("...") 316 break 317 } 318 319 buf.WriteString(ip.String()) 320 } 321 return buf.String() 322 } 323 324 func etcdDbgRetrieveRootCAFile(cfgfile string) (certs [][]byte, err error) { 325 var yc yamlConfig 326 327 b, err := os.ReadFile(cfgfile) 328 if err != nil { 329 return nil, err 330 } 331 332 err = yaml.Unmarshal(b, &yc) 333 if err != nil { 334 return nil, err 335 } 336 337 crtfile := cmp.Or(yc.TrustedCAfile, yc.CAfile) 338 if crtfile == "" { 339 return nil, errors.New("not provided") 340 } 341 342 data, err := os.ReadFile(crtfile) 343 if err != nil { 344 return nil, err 345 } 346 347 for { 348 block, rest := pem.Decode(data) 349 if block == nil { 350 if len(certs) == 0 { 351 return nil, errors.New("no certificate found") 352 } 353 354 return certs, nil 355 } 356 357 if block.Type == "CERTIFICATE" { 358 certs = append(certs, block.Bytes) 359 } 360 361 data = rest 362 } 363 } 364 365 func etcdDbgOutputCert(cert *x509.Certificate, iw *indentedWriter) { 366 sn := cert.SerialNumber.Text(16) 367 for i := 2; i < len(sn); i += 3 { 368 sn = sn[:i] + ":" + sn[i:] 369 } 370 371 iw.Println("- Serial number: %s", string(sn)) 372 iw.Println(" Subject: %s", cert.Subject) 373 iw.Println(" Issuer: %s", cert.Issuer) 374 iw.Println(" Validity:") 375 iw.Println(" Not before: %s", cert.NotBefore) 376 iw.Println(" Not after: %s", cert.NotAfter) 377 } 378 379 type indentedWriter struct { 380 w io.Writer 381 indent []byte 382 } 383 384 func newIndentedWriter(w io.Writer, indent int) *indentedWriter { 385 return &indentedWriter{w: w, indent: []byte(strings.Repeat(" ", indent))} 386 } 387 388 func (iw *indentedWriter) NewLine() { iw.w.Write([]byte("\n")) } 389 390 func (iw *indentedWriter) Println(format string, a ...any) { 391 iw.w.Write(iw.indent) 392 fmt.Fprintf(iw.w, format, a...) 393 iw.NewLine() 394 } 395 396 func (iw *indentedWriter) WithExtraIndent(indent int) *indentedWriter { 397 return newIndentedWriter(iw.w, len(iw.indent)+indent) 398 }